вторник, 27 марта 2012 г.

[Перевод] Джон Скит. Реализация LINQ to Objects: Часть 6 – Repeat

Это перевод шестой части серии статей Джона Скита "Реализация LINQ to Objects". С оригинальной версией поста можно ознакомиться здесь.


Далее следует тривиальный метод, разговоров о котором будет еще меньше, чем об Empty. Этот пост только ради завершенности.

Что это?

Repeat – статический обобщенный метод с единственной перегрузкой, который не является методом расширения:

public static IEnumerable<TResult> Repeat<TResult>(
    TResult element,
    int count);

Он просто возвращает последовательность, содержащую определенный элемент, который повторяется «count» раз. Единственной проверкой аргумента является то, что count должен быть неотрицательным.

Что мы собираемся тестировать?

Здесь на самом деле не так много чего тестировать. Я подумал о четырёх различных сценариях:

  • Простая последовательность (повторение строки 3 раза)
  • Пустая последовательность (повторение элемента 0 раз)
  • Последовательность значений null (чтобы доказать, что «element» может быть null)
  • Отрицательное count, чтобы доказать, что валидация аргументов происходит, причем незамедлительно.

Боюсь, это нисколько не интересно.

Давайте имплементируем это!

Единственное, что мы могли бы сделать здесь неправильно, это разместить валидацию аргументов непосредственно в блоке итератора… и мы реализовывали паттерн «разделение метода» ранее столько раз, что мы не попадёмся в эту ловушку. Итак, вот код во всем своём скучном отсутствии великолепия:

public static IEnumerable<TResult> Repeat<TResult>(TResult element, int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count");
    }
    return RepeatImpl(element, count);
}

private static IEnumerable<TResult> RepeatImpl<TResult>(TResult element, int count)
{
    for (int i = 0; i < count; i++)
    {
        yield return element;
    }
} 

Вот и все. Хм, интересные моменты, которые стоит отметить ... их нет.

Заключение

Нет смысла затягивать. Это всё. Следующие на очереди: Count и LongCount - которые действительно имеют несколько интересных моментов.

[Перевод] Джон Скит. Реализация LINQ to Objects: Часть 5 – Empty

Это перевод пятой части серии статей Джона Скита "Реализация LINQ to Objects". С оригинальной версией поста можно ознакомиться здесь.


Продолжая с методами, не являющимися методами расширения, пришло время для, возможно, самого простого оператора LINQ – Empty.

Что это?

Empty – это обобщенный статический метод с единственной сигнатурой, не принимающий параметров:

public static IEnumerable<TResult> Empty<TResult>() 

Он возвращает пустую последовательность соответствующего типа. Это все, что он делает.

Есть только один интересный момент: документировано, что Empty кеширует пустую последовательность. Иначе говоря, он возвращает ссылку на ту же самую пустую коллекцию, каждый раз когда вы его вызываете (для того же самого аргумента типа, естественно).

Что мы собираемся тестировать?

Существуют только две вещи, которые мы можем здесь протестировать:

  • Конечная последовательность пуста
  • Конечная последовательность кешируется для каждого типа аргумента

Я использую тот же подход, что и с Range для вызова статического метода, но в этот раз с псевдонимом для EmptyClass. Вот тесты:

[Test]
public void EmptyContainsNoElements()
{
    using (var empty = EmptyClass.Empty<int>().GetEnumerator())
    {
        Assert.IsFalse(empty.MoveNext());
    }
}

[Test]
public void EmptyIsASingletonPerElementType()
{
    Assert.AreSame(EmptyClass.Empty<int>(), EmptyClass.Empty<int>());
    Assert.AreSame(EmptyClass.Empty<long>(), EmptyClass.Empty<long>());
    Assert.AreSame(EmptyClass.Empty<string>(), EmptyClass.Empty<string>());
    Assert.AreSame(EmptyClass.Empty<object>(), EmptyClass.Empty<object>());

    Assert.AreNotSame(EmptyClass.Empty<long>(), EmptyClass.Empty<int>());
    Assert.AreNotSame(EmptyClass.Empty<string>(), EmptyClass.Empty<object>());
}

Конечно, они не проверяют, что последовательность кэшируется для каждого потока отдельно, но сойдёт и так.

Давайте имплентируем это!

На самом деле, реализация немного интереснее, чем можно было предположить из описания. Если бы не аспекты кеширования, реализация могла бы выглядеть следующим образом:

// Не кэшировать пустую последовательность
public static IEnumerable<TResult> Empty<TResult>()
{
    yield break;
}

… но мы должны соблюдать (хотя бы отчасти) документированный аспект кеширования. В конце концов, это не очень сложно. Существует одно очень удобное обстоятельство, которое мы можем использовать: пустые массивы неизменяемы. Массивы всегда имеют фиксированный размер, но обычно не существует способа сделать массивы доступными только для чтения… вы всегда можете изменить значение любого элемента. Но в пустом массиве нет элементов, поэтому в нём нечего менять. Исходя из этого, мы можем использовать один и тот же массив снова и снова, возвращая его напрямую… но только если у нас есть пустой массив подходящего типа.

Сейчас вы, возможно, ожидаете Dictionary<Type, Array>, или что-нибудь подобное… но существует другой полезный трюк, который мы можем использовать вместо этого. Если вам нужен кэш для каждого типа и тип специфичен так же, как и аргумент типа, вы можете использовать статические переменные в обобщенном классе, потому что каждый созданный тип будет иметь свой набор статических переменных.

К сожалению, Empty – обобщенный метод, а не необобщенный метод в обобщенном типе… поэтому мы вынуждены создать отдельный обобщенный тип, который будет вести себя как кэш для пустого массива. Сделать это несложно, и CLR позаботится об инициализации типа в потокобезопасном режиме. Поэтому, наша финальная реализация будет выглядеть следующим образом:

public static IEnumerable<TResult> Empty<TResult>()
{
    return EmptyHolder<TResult>.Array;
}

private static class EmptyHolder<T>
{
    internal static readonly T[] Array = new T[0];
} 

Реализация поддерживает нужное нам кэширование и является довольно простой… но это значит, что вы должны достаточно хорошо знать, как работают обобщения в .NET. В некотором смысле, это противоположность ситуации, возникшей в предыдущем посте – хитрая реализация вместо медленной, но возможно более простой, основанной на dictionary. В данном случае меня устраивает компромисс, потому что как только вы поймете, как работают обобщенные типы и статические переменные, код покажется простым. Это тот случай, когда простота для каждого своя.

Заключение

Итак, это Empty. Следующий оператор – Repeat – вероятно будет еще проще, но он будет иметь отдельную реализацию…

Дополнение

В связи с незначительным протестом против возврата массива (что, я думаю, хорошо), вот альтернативная реализация:

public static IEnumerable<TResult> Empty<TResult>()
{
    return EmptyEnumerable<TResult>.Instance;
}

#if AVOID_RETURNING_ARRAYS
private class EmptyEnumerable<T> : IEnumerable<T>, IEnumerator<T>
{
internal static IEnumerable<T> Instance = new EmptyEnumerable<T>();

// Предотвращаем создание в другом месте
private EmptyEnumerable()
{
}

public IEnumerator<T> GetEnumerator()
{
    return this;
}

IEnumerator IEnumerable.GetEnumerator()
{
    return this;
}

public T Current
{
    get { throw new InvalidOperationException(); }
}

object IEnumerator.Current
{
    get { throw new InvalidOperationException(); }
}

public void Dispose()
{
    // Пустой метод
}

public bool MoveNext()
{
    return false; // Следующего элемента никогда не будет
}

public void Reset()
{
    // Пустой метод
}
}

#else
private static class EmptyEnumerable<T>
{
    internal static readonly T[] Instance = new T[0];
}
#endif

Надеюсь, теперь каждый сможет собрать версию, которой он будет доволен :)

понедельник, 12 марта 2012 г.

[Перевод] Джон Скит. Реализация LINQ to Objects: Часть 4 – Range

Это перевод четвертой части серии статей Джона Скита "Реализация LINQ to Objects". С оригинальной версией поста можно ознакомиться здесь.


Это будет небольшой пост, и далее, возможно, будут еще более короткие. Я считаю, что имеет смысл описывать несколько операторов в одном посте, если они действительно подобны (вспоминаются Count и LongCount). Впрочем, я в ваших руках – если вы предпочитаете более «толстые» посты, пишите в комментариях.

Этот пост будет иметь дело с оператором генерации Range.

Что это?

Range имеет единственную сигнатуру:

public static IEnumerable<int> Range(
    int start,
    int count)

В отличие от большинства методов LINQ, это – не метод расширения, а старый добрый статический метод. Он возвращает объект, который будет выдавать “count” чисел, которые начинаются со "start", и каждый раз увеличиваются на 1 – так, к примеру, вызов Enumerable.Range(6, 3) вернет сначала 6, потом 7, а потом 8.

Поскольку он не работает с исходной последовательностью, нет смысла буферизировать или выдавать в потоке исходные данные, но:

  • Аргументы должны быть сразу проверены; count не может быть отрицательным, и оно не должно быть таким, чтобы любой элемент из области переполнил Int32.
  • Значения будут возвращены по требованию – Range должен быть дешевым, вместо создания, скажем, массива «count» элементов и их возврата.
Как мы собираемся это тестировать?

Тестирование простого статического метода приносит нам новые проблемы с точки зрения переключения между «нормальной» реализацией LINQ и Edulinq. Это артефакт пространств имён, которые я использую – тесты находятся в Edulinq.Tests, а имплементация – в Edulinq. «Родительские» пространства имен всегда предпочтительнее, когда компилятор пытается найти тип, и они имеют приоритет перед любой из директив using – даже директивы using, которая пытается явно указать псевдонимом (alias) типа.

Решение (слегка некрасивое), которое я выбрал – включать директивы using, чтобы создавать псевдонимы, которые не могу быть разрешены иначе – в этом случае, RangeClass. Директива using будет служить псевдонимом либо System.Linq.Enumerable либо Edulinq.Enumerable. Теперь все тесты будут использовать RangeClass.Range. Также я изменил способ переключения между имплементациями – теперь у меня есть две конфигурации проекта, одна из которых определяет символ препроцессора NORMAL_LINQ, а другая – нет. Следовательно, класс RangeTest теперь содержит:

#if NORMAL_LINQ
using RangeClass = System.Linq.Enumerable;
#else
using RangeClass = Edulinq.Enumerable;
#endif

Конечно, существуют и альтернативы:

  • Я бы мог переместить тесты в другое пространство имен
  • Я бы мог сделать так, чтобы ссылки на проект зависели от конфигурации, так, конфигурация «нормального LINQ» не ссылалась бы на проект Edulinq, а конфигурация «реализации Edulinq» не ссылалась бы на System.Core. Дале, я бы просто мог использовать Enumerable.Range с соответствующей директивой using для условия System.Linq, которое основывается на директиве препроцессора NORMAL_LINQ, как и для других тестов.

Мне нравится второй подход, но он подразумевает изменение файла тестового проекта - Visual Studio не располагает средствами условного включения ссылок. Я могу сделать это позже… идеи приветствуются.

Что мы будем тестировать?

Существует не так много ситуаций, на которые можно протестировать диапазон: у меня есть только восемь тестов, и не один из них не является по-настоящему увлекательным:

  • Простой правильный диапазон должен работать нормально, когда тестируется при помощи AssertSequenceEqual
  • Начальное значение может быть отрицательным
  • Range(Int32.MinValue, 0) является пустым диапазоном
  • Range(Int32.MaxValue, 1) возвращает только Int32.MaxValue
  • count не может быть отрицательным
  • count может равняться 0
  • start+count-1 не может превышать Int32.MaxValue (поэтому Range(Int32.MaxValue, 2) не является правильным)
  • start+count-1 может быть Int32.MaxValue (поэтому Range(Int32.MaxValue, 1) является правильным)

Последние два тестируются на нескольких разных примерах каждый – с большим start и маленьким count, с маленьким start и большим count, и «достаточно большими» значениями для обоих start и count.

Обратите внимание, что у меня нет тестов на отложенное выполнение – хотя я мог бы проверить, что возвращаемое значение не имплементирует никаких других интерфейсов коллекций, было бы странно делать так. С другой стороны, у нас есть тесты, в которых задано огромное значение count – такое, что если бы что-то действительно попробовало создать коллекцию такого размера, практически наверняка потерпело бы неудачу.

Давайте имлементируем это!

Не будет неожиданностью то, что мы собираемся использовать раздельную имплементацию, с публичным методом, который будет осуществлять мгновенную валидацию аргументов, а потом использовать приватный метод с блоком итератора, который осуществляет фактический обход коллекции.

Проверив аргументы, мы наверняка будем знать, что переполнение пределов Int32 никогда не произойдет, так что мы можем быть довольно свободны в главное части имплементации.

public static IEnumerable<int> Range(int start, int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count");
    }
    // Преобразовываем всё в long, чтобы избежать переполнения. Существуют и другие способы 
    // проверки на переполнение, но эта делает код правильным наиболее очевидным способом.
    if ((long)start + (long)count - 1L > int.MaxValue)
    {
        throw new ArgumentOutOfRangeException("count");
    }
    return RangeImpl(start, count);
}

private static IEnumerable<int> RangeImpl(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

Здесь есть несколько моментов:

  • Возможно, это комбинация «start» и «count» является неправильной во второй проверке, а не только count. Было бы неплохо позволить ArgumentOutOfRangeException (либо ArgumentException в общем) винить несколько аргументов, а не один из них. Как бы ни было, использование «count» совпадает с оригинальной имплементацией фреймворка.
  • Есть другие способы выполнения второй проверки, и я точно не был обязан делать все операнды выражения типа long. Несмотря на это, я считаю, что это самый простой код, просто и понятно основанный на документации. Я не обязан думать обо всех возможных ситуациях и проверять, что все они работают. Арифметика будет корректной, если использовать диапазон значений Int64, поэтому я не хочу беспокоиться о переполнении, и я не должен решать, когда использовать checked или unchecked контекст.
  • Существуют другие способы организации циклов в приватном методе блока итератора, но я считаю этот самым простым. Другой очевидной и простой альтернативой является хранение двух значений: одного для количества выведенных значений, и другого для следующего значения, которое следует вывести, и увеличение их на каждой итерации. Более сложным подходом может являться использование всего лишь одной переменной цикла – но вы не можете использовать «value < start + count» в случае, если конечное значение равно Int32.MaxValue, и вы не можете использовать «value <= start + count - 1» в случае, когда аргументы равны (int.MinValue, 0). Вместо того чтобы учитывать все крайние случаи, я пошел методом очевидно правильного решения. Если бы вас действительно, действительно беспокоила производительность Range, вы бы захотели исследовать многие другие случаи.


До написания этого поста у меня не было хороших тестов для Range(Int32.MaxValue, 1) и Range(Int32.MinValue, 0)… но поскольку они могли с легкостью провалиться, о чем писалось выше, я их добавил. Я нахожу интересным то, как рассмотрение альтернативных реализаций подсказывает дополнительные тесты.

Заключение

Реализация метода Range оказалась полезна для тестирования некоторых других операторов, таких как Count. Теперь, когда я затронул методы, не являющиеся методами расширения, я так же могу сделать остальные два (Empty и Repeat). Я уже имплементировал Empty и надеюсь написать о нём сегодня. «Repeat» не должен занять больше времени, а потом мы сможем перейти к Count и LongCount.

Я считаю, что этот код является хорошим примером ситуаций, когда стоит писать «тупой» код, который выглядит как документация, чем пытаться писать, возможно, более краткий, возможно, немного более эффективный код, в который сложно вникать. Без сомнения, в следующих постах будет много подобного.

воскресенье, 4 марта 2012 г.

[Перевод] Джон Скит. Реализация LINQ to Objects: Часть 3 – Select

Это перевод третьей части серии статей Джона Скита "Реализация LINQ to Objects". С оригинальной версией поста можно ознакомиться здесь.


Важным шагом вперед является то, что у проекта теперь есть репозиторий на Google Code, вместого того, чтобы быть обычным zip файлом в каждой записи блога. На этом шаге мне надо было дать проекту имя, и я выбрал Edulinq, из-за очевидных причин. Я изменил пространства имен и т.п. в коде, и тэг для каждого поста теперь также будет Edulinq. В любом случае, достаточно преамбулы… Давай продолжим имплементировать LINQ, в этот раз оператор Select.

Что это?

Как и Where, у Select тоже есть две перегрузки:


public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector)

public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, int, TResult> selector)

Опять же, оба оператора работают одинаково, но вторая перегрузка позволяет использовать индекс в последовательности как часть проекции.

Сперва о простом: метод проецирует одну последовательность на вторую: делегат-«селектор» применяется к каждому исходному элементу, чтобы получить конечный элемент. Детали поведения точно такие же, как у Where (я просто скопировал их из предыдущего раздела, немного подкорректировав):

  • Исходная последовательность никоим образом не модифицируется.
  • Метод использует отложенное выполнение – до того момента, как вы попытаетесь достать элементы из конечной последовательности, элементы не начнут извлекаться из исходной последовательности.
  • Несмотря на отложенное выполнение, он сразу проверит, не являются ли параметры null.
  • Он возвращает результаты в виде потока – ему всегда нужен только один результат в один момент времени, и он будет возвращать его без надобности хранить на него ссылку. Это значит, что вы можете применять этот метод к последовательностям бесконечной длины (к примеру, к последовательности случайных чисел.)
  • Он будет проходить по исходной последовательности единожды, каждый раз как вы будете проходить по конечной последовательности.
  • Для каждого элемента «Селектор» вызывается только один раз.
  • Освобождение итератора конечной последовательности освободит соответствующий итератор исходной поверхности.
Что мы собираемся тестировать?

Тесты очень похожи на тесты для Where: кроме того, что в случаях, когда мы тестировали фильтрацию для Where, мы будем тестировать проекцию для Select.

Есть парочка интересных тестов. Во-первых, мы можем сказать, что метод является обобщенным и вместо одного параметра типа использует два: TSource и TResult. Они довольно очевидны, но это значит, что стоит иметь тест для случая, когда параметры типов разные – как в случае преобразования целого числа в строку:


[Test]
public void SimpleProjectionToDifferentType()
{
    int[] source = { 1, 5, 2 };
    var result = source.Select(x => x.ToString());
    result.AssertSequenceEqual("1", "5", "2");
}

Во-вторых, у меня есть тест, который показывает в какие странные ситуации вы можете попасть, если добавите побочные действия в ваш запрос. Мы могли бы сделать это и с Where, но с Select это гораздо понятнее:

[Test]
public void SideEffectsInProjection()
{
    int[] source = new int[3]; // Фактические значения не будут релевантны
    int count = 0;
    var query = source.Select(x => count++);
    query.AssertSequenceEqual(0, 1, 2);
    query.AssertSequenceEqual(3, 4, 5);
    count = 10;
    query.AssertSequenceEqual(10, 11, 12);
}

Обратите внимание, что мы вызываем Select только раз, но результаты обхода результата меняются каждый раз – потому что переменная «count» была захвачена, и модифицируется внутри проекции. Пожалуйста, не делайте так.

В-третьих, мы теперь можем написать выражение запроса, которое включает операторы «select» и «where»:


[Test]
public void WhereAndSelect()
{
    int[] source = { 1, 3, 4, 2, 8, 1 };
    var result = from x in source
                 where x < 4
                 select x * 2;
    result.AssertSequenceEqual(2, 6, 4, 2);
}

Здесь, конечно же, нет ничего шокирующего – надеюсь, если вы когда-либо использовали LINQ to Objects, это должно казаться очень комфортным и знакомым.

Давайте имплементируем это!

Сюрприз-сюрприз, мы собираемся имплементировать Select почти таким же образом, как и Where. Опять же, я просто скопировал имплементацию и немного её подправил – два метода действительно настолько похожи. В частности:

  • Мы используем блоки итераторов, чтобы упростить возврат последовательностей.
  • Семантика блоков итераторов подразумевает, что мы должны отделить валидацию аргументов от реальной работы. (С того момента, как я написал предыдущий пост, я узнал, что в VB11 появятся анонимные итераторы, что позволит избежать этих проблем. Фух. Мне кажется, что неправильно завидовать пользователям VB, но я научусь жить с этим.)
  • Мы используем foreach внутри блоков итераторов, чтобы удостовериться, что мы освобождаем итератор исходной последовательности надлежащим образом – пока итератор конечной последовательности не освобожден, или исходные элементы не закончились, естественно.

Я перейду прямо к коду, поскольку он очень похож на Where. Также я не буду показывать версию с индексом – потому что отличия тривиальны.


public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (selector == null)
    {
        throw new ArgumentNullException("selector");
    }
    return SelectImpl(source, selector);
}

private static IEnumerable<TResult> SelectImpl<TSource, TResult>(
            this IEnumerable<TSource> source,
            Func<TSource, TResult> selector)
{
    foreach (TSource item in source)
    {
        yield return selector(item);
    }
}

Просто, не правда ли? Опять же, метод с реальной «работой» короче, чем даже валидация аргументов.

Заключение

Поскольку я обычно не люблю быть надоедливыми по отношению к моим читателям (что может удивить некоторых из вас), я признаю: это был довольно однообразный пост. Я ставил ударение на «так же, как Where» несколько раз совершенно сознательно – п

Что-нибудь слегка другое в следующий раз (что, надеюсь, будет через несколько дней). Я не уверен, что именно, но есть еще по-прежнему много методов на выбор…