среда, 22 августа 2012 г.

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

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


После нашего краткого визита к скалярным возвращаемым типам с Count и LongCount, мы вернулись к оператору, который возвращает последовательность: Concat.

Что это?

У Concat есть только одна сигнатура, что упрощает жизнь:

public static IEnumerable<TSource> Concat<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second) 

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

Иногда я думаю, жаль, что не существует методов Prepend/Append, которые делали бы ту же работу, но для одного элемента – этого было бы достаточно полезно в таких ситуациях, как когда у вас есть список стран с дополнительной опцией «страна не выбрана». Довольно просто для этих целей использовать Concat, создавая массив с одним элементом, но специфические методы были бы более читабельны, я полагаю. В MoreLINQ для этих целей существуют дополнительные методы Concat, но в Edulinq предполагается реализовать только методы, которые присутствуют в LINQ to Objects.

Как всегда, некоторые заметки о поведении Concat:

  • Аргументы валидируются мгновенно: они оба не должны быть null.
  • Результат использует отложенное выполнение, кроме валидации. При первом вызове метода аргументы не используются.
  • Каждая последовательность вычисляется только тогда, когда она должна быть вычислена. Если вы останавливаете обход конечной последовательности до того, как первая исходная последовательность исчерпалась, вторая исходная последовательность не будет затронута.

В основном, так.

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

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

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

В итоге, существует несколько тестов для определения моментов, в которых используется каждая входная последовательность. Это достигнуто использованием ThrowingEnumerable, который мы использовали в тестах Where:

[Test]
public void FirstSequenceIsntAccessedBeforeFirstUse()
{
    IEnumerable<int> first = new ThrowingEnumerable();
    IEnumerable<int> second = new int[] { 5 };
    // Пока исключения нет... 
    var query = first.Concat(second);
    // До сих пор никакого исключения... 
    using (var iterator = query.GetEnumerator())
    {
        // Теперь оно возникнет
        Assert.Throws<InvalidOperationException>(() => iterator.MoveNext());
    }
}

[Test]
public void SecondSequenceIsntAccessedBeforeFirstUse()
{
    IEnumerable<int> first = new int[] { 5 };
    IEnumerable<int> second = new ThrowingEnumerable();
    // Пока исключения нет... 
    var query = first.Concat(second);
    // До сих пор никакого исключения... 
    using (var iterator = query.GetEnumerator())
    {
        // С первым элементом всё впрорядке... 
        Assert.IsTrue(iterator.MoveNext());
        Assert.AreEqual(5, iterator.Current);
        // Исключение возникает, как только мы переходим ко 
        // второй последовательности 
        Assert.Throws<InvalidOperationException>(() => iterator.MoveNext());
    }
}

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

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

Реализация достаточно проста, но она заставляет меня жаждать F#... это - обычное разделение между проверкой аргументов и реализацией блоков итератором, каждая часть очень проста:

public static IEnumerable<TSource> Concat<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second)
{
    if (first == null)
    {
        throw new ArgumentNullException("first");
    }
    if (second == null)
    {
        throw new ArgumentNullException("second");
    }
    return ConcatImpl(first, second);
}

private static IEnumerable<TSource> ConcatImpl<TSource>(
    IEnumerable<TSource> first,
    IEnumerable<TSource> second)
{
    foreach (TSource item in first)
    {
        yield return item;
    }
    foreach (TSource item in second)
    {
        yield return item;
    }
}

Сейчас стоит напомнить, что было бы неприятно реализовывать это без использования блоков итераторов. Не так уж и сложно, но нам пришлось бы запоминать, по какой последовательности мы в данный момент проходим (если вообще проходим) и т.п.

Тем не менее, используя F#, мы могли бы сделать это проще при помощи выражения yield!, которое порождает целую последовательность, вместо одного элемента. Правда, для использования yield! в этом случае отсутствует существенный выигрыш в производительности (который наверняка может присутствовать в рекурсивных ситуациях), но было бы более элегантно иметь возможность порождать целые последовательности одним оператором. (Spec# имеет похожую конструкцию, которая называется вложенный итератор, и которая выражается с помощью yield foreach). Я не претендую на то, что знаю достаточно о деталях F# или Spec#, чтобы проводить более детальное сравнение, но мы увидим шаблон «получить элемент из последовательности при помощи foreach, вывести элемент при помощи yield» еще несколько раз, прежде чем мы закончим. Вспомните, что мы не можем вынести это в метод, поскольку ключевое слово «yield» требует специальной трактовки компилятором C#.

Заключение

Даже имея простую реализацию, я могу найти возможность поворчать :) Было бы неплохо, чтобы в C# присутствовали встроенные итераторы, но, если быть честным, количество раз, когда я был расстроен их отсутствием, достаточно мало.

Concat – полезный оператор, но это, на самом деле, только очень частный случай другого оператора: SelectMany. В конце концов, Concat всего лишь соединяет две последовательности в одну… тогда как SelectMany может соединять целую последовательность последовательностей, с даже большей обобщенностью, если требуется. Далее я реализую SelectMany и покажу несколько примеров, как другие операторы могут быть объединены только с помощью SelectMany. (Мы увидим подобную возможность для операторов, возвращающих единое значение, когда будет имплементировать Aggregate.)

Дополнение. Избегание хранения ссылок без надобности

В комментарии было предположено, что мы должны присваивать первой последовательности «null» после того, как мы её используем. Таким образом, как только мы закончили проход по последовательности, она становится пригодной для сборщика мусора. Это ведет к следующей реализации:

private static IEnumerable<TSource> ConcatImpl<TSource>( 
    IEnumerable<TSource> first, 
    IEnumerable<TSource> second) 
{ 
    foreach (TSource item in first) 
    { 
        yield return item; 
    } 
    // Избегаем хранения ссылки, которая нам не нужна
    first = null; 
    foreach (TSource item in second) 
    { 
        yield return item; 
    } 
}

Сейчас я бы сказал – присвоение локальной переменной значения «null», когда она не используется в оставшейся части метода, не будет иметь смысла, когда CLR запущена в оптимизированном режиме, без подключенного отладчика: сборщик мусора заботится только о переменных, к которым может быть получен доступ в оставшейся части метода.

Тем не менее, в данном случае, это имеет смысл, потому что это не обычная локальная переменная. В итоге она становится переменной экземпляра скрытого класса, сгенерированного компилятором C#... и CLR не может сказать, будет ли переменная экземпляра использоваться снова.

Возможно, мы могли бы убрать нашу единственную ссылку на «first» в начале GetEnumerator. Мы можем написать метод в виде:

public static T ReturnAndSetToNull<T>(ref T value) where T : class 
{ 
    T tmp = value; 
    value = null; 
    return tmp; 
}

и потом вызвать его следующим образом:

foreach (TSource item in ReturnAndSetToNull(ref first))

Я, конечно, считаю это перебором, особенно потому что кажется очень вероятным, что итератор будет до сих пор иметь ссылку на саму коллекцию, но простая установка “first” в null после прохождения по ней, я считаю, имеет смысл.

Заметьте, я не верю, что «настоящая» реализация LINQ to Objects поступает подобным образом. (Когда-нибудь я проверю это при помощи коллекции, которая имеет финализатор.)

воскресенье, 10 июня 2012 г.

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

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


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

Кто они?

Count и LongCount имеют по две перегрузки каждый: одна с предикатом, а другая без. Вот все четыре сигнатуры:

public static int Count<TSource>(
    this IEnumerable<TSource> source)

public static int Count<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)

public static long LongCount<TSource>(
    this IEnumerable<TSource> source)

public static long LongCount<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate) 

Как вы можете видеть, сигнатура LongCount идентична Count, отличаясь лишь типом возвращаемого значения: long (Int64) вместо int (Int32).

Перегрузки без параметра-предиката просто возвращают количество элементов в исходной последовательности, а те, что с предикатом, возвращают количество элементов, для которых предикат возвращает true.

Интересные аспекты поведения:

  • Все они - методы расширения для IEnumerable<T>. Вы могли бы сказать, что для версий без предиката было бы лучше расширить необобщенный IEnumerable, потому что от типа элемента ничего не зависит.
  • Там, где нет предиката, Count оптимизирован под ICollection<T> и (в .NET 4) ICollection – оба которые имеют свойство Count, которое предположительно быстрее, чем обход последовательности. LongCount не оптимизирован подобным образом в .NET – я расскажу об этом в разделе реализации.
  • В перегрузках с предикатами не происходит никакой оптимизации, потому что не существует способа определить заранее, сколько элементов «пройдёт» предикат, не проверив их.
  • Все методы выполняются немедленно – ничего не отложено. (Если подумать, то тут действительно ничего не может быть отложено, когда мы просто возвращаем int или long.)
  • Все аргументы проверяются только на то, что они не null.
  • Оба метода должны бросать OverflowException, если в исходной последовательности больше элементов, чем то, количество которых они могут возвратить… тем не менее очевидно, что в случае LongCount это гораздо большее число, чем в случае Count.

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

В некотором смысле, здесь затронуты только два «успешных» теста: с предикатом и без. С ними довольно легко справиться, но мы также хотим использовать оптимизированные случаи. Это несколько хитрее, чем могло бы показаться, поскольку мы можем проверить четыре ситуации:

  • Источник, который реализует ICollection<T> и ICollection (просто: используйте List<T>)
  • Источник, который реализует ICollection<T>, но не ICollection (довольно просто, после несложного поиска подходящего типа: используйте HashSet<T>)
  • Источник, который реализует ICollection, но не ICollection<T>, но до сих пор реализует IEnumerable<T> (так, что мы можем наследовать их) – хитро…
  • Источник, которые не реализует ни ICollection ни ICollection<T> (просто: используйте Enumerable.Range, который мы уже имплементировали)

Третий вариант довольно неприятный. Очевидно, существует множество реализаций интерфейса ICollection, но не ICollection<T> (например, ArrayList), но поскольку он не имплементирует IEnumerable<T>, мы не можем вызвать на нём метод расширения Count. В конце концов, я написал собственный класс SemiGenericCollection.

В тот момент, когда у нас будут источники для всех этих тестов, мы должны будем решить, что же конкретно мы собираемся тестировать. Вероятно, мы должны проверить то, что результат оптимизирован, например, проверяя, что никогда в действительности не проходим по последовательности. Это потребует написания собственных коллекций с методами GetEnumerator(), которые бросают исключения, но, тем не менее, возвращают количество в свойстве Count. Так далеко я не заходил, но это другой шаг, который мы могли бы совершить.

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

Сбойные случаи для аргументов null очень просты, но существует другой случай, который следует взять во внимание: переполнение. Для Count я имплементировал прецедент для проверки поведения переполнения. К сожалению, мы пока что не можем запустить этот тест для реализации Edulinq, поскольку он требует Enumerable.Concat, но тут он представлен для справки:


[Test]
[Ignore("Занимает колоссальное количество времени!")]
public void Overflow()
{
    var largeSequence = Enumerable.Range(0, int.MaxValue)
        .Concat(Enumerable.Range(0, 1));
       Assert.Throws<OverflowException>(() => largeSequence.Count());
}

Это предохраняет от плохой имплементации, которая переполняется, просто оборачивая счетчик в Int32.MinValue, вместо вызова исключения.

Как вы видите, этот тест будет отключен даже если он будет раскомментирован после реализации Concat, поскольку он нуждается в счете до двух миллиардов – что нехорошо для быстрой подборки модульных тестов. Хотя это не очень плохо в сравнении с эквивалентом LongCount, который потребует подсчета 2^63 элементов. Генерация такой последовательности не является сложной, но её обход займет очень длительное время. Нам также нужен подобный тест для перегрузки с предикатом - кое-что, чем я пренебрег до написания этого поста, а также нахождение ошибки в реализации, как сделал я :)

Для LongCount, у меня есть только эквивалентный предыдущему тест, который проверяет, что такая-же последовательность может иметь длину, представленную значением long.

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

Сначала посмотрим на перегрузку, которая принимает предикат – она проще:

public static int Count<TSource>(this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    if (source == null)
    {
        new ArgumentNullException("source");
    }
    if (predicate == null)
    {
        throw new ArgumentNullException("predicate");
    }

    // Нет способа оптимизировать это
    checked
    {
        int count = 0;
        foreach (TSource item in source)
        {
            if (predicate(item))
            {
                count++;
            }
        }
        return count;
    }
} 

Заметим, что в этот раз мы не используем блок итератора (мы не возвращаем последовательность), поэтому нам не нужно разделять реализацию на две разные части, чтобы выполнить мгновенную валидацию аргументов.

После валидации аргументов, главная часть метода относительно проста, но с одной уловкой: мы производим обход в контексте «checked». Это значит, что если count переполнится, то будет брошен OverflowException вместо переноса к отрицательному числу. Существуют еще несколько альтернатив:

  • В checked мы бы могли производить только инкремент count вместо всей второй части метода.
  • Мы бы могли явно протестировать count == int.MaxValue перед инкрементом, и бросить исключение.
  • Мы могли бы построить всю сборку в контексте checked.

Я считаю, что эта секция должна быть явно checked – это делает очевидным, что это – основное требование к общей корректности. Вы можете предпочесть сделать checked только инкрементирующую часть – я лично считаю, что текущий подход привлекает больше внимания к checked, но это субъективно. Также возможно, что явная проверка может быть быстрее, хотя я сомневаюсь – я не сравнивал эти подходы.

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

public static int Count<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }

    // Оптимизация для ICollection<T>
    ICollection<TSource> genericCollection = source as ICollection<TSource>;
    if (genericCollection != null)
    {
        return genericCollection.Count;
    }

    // Оптимизация для ICollection
    ICollection nonGenericCollection = source as ICollection;
    if (nonGenericCollection != null)
    {
        return nonGenericCollection.Count;
    }

    // Делаем это медленным способом, и убеждаемся, что переполнение 
    // обрабатывается должным образом
    checked
    {
        int count = 0;
        using (var iterator = source.GetEnumerator())
        {
            while (iterator.MoveNext())
            {
                count++;
            }
         }
         return count;
    }
}

Единственный «новый» код здесь - это оптимизация. Присутствуют два практически эквивалентных блока, которые проверяют только разные типы интерфейсов коллекций, и используется один, который найден первым (если найден). Я не знаю, что .NET проверяет первым: ICollection или ICollection<T> - я мог бы проверить это, имплементируя оба интерфейса, каждый из которых возвращает разные значения count, но это крайность. На самом деле для хорошо себя ведущих коллекций не важно легкое различие в производительности – мы хотим сначала проверить «более возможный» интерфейс, которым, как я считаю, является обобщенный.

Оптимизировать или не оптимизировать?

Реализация LongCount точно такая же, как и реализация Count, за исключением использования long вместо int.

Стоит заметить, что я до сих пор использую оптимизации для ICollection и ICollection<T> , но я не верю, что .NET имплементация так делает. (Это легко проверить, создав огромный список байтов и сравнив время, затраченное Count и LongCount.)

Существует аргумент для использования Array.GetLongLength, когда источник является массивом… но я не считаю, что текущая CLR поддерживает массивы с длиной более Int32.MaxValue, поэтому это скорее вопрос перспективы. Далее, я не уверен, почему .NET имплементация не оптимизирована. Если быть честным, то непонятно, что реализация ICollection/ICollection<T> должна возвращать из её свойства Count, если в ней находится более Int32.MaxValue элементов.

Предложения, что я должен был сделать, приветствуются, но я должен предупредить, что LongCount скорее может быть использован по отношению к Queryable чем к Enumerable – легко представить сервис, представляющий коллекцию (такую, как таблицу базы данных), которая может быстро вернуть вам количество элементов даже если она очень большая. Я предполагаю, что существует относительно мало случаев, когда у вас была бы коллекция, которую вы действительно хотели бы обойти, чтобы только определить количество элементов.

Заключение

Это наши первые операторы LINQ, которые возвращают скалярные значения вместо последовательностей – из чего естественно следует, что их легче понять в смыслах управляющей логики и времени выполнения. Методы просто выполняются – возможно, с оптимизацией – и возвращают свои результаты. Просто и красиво. Тем не менее, мы увидели, что может существовать несколько интересных аспектов для рассмотрения, включая вопросы оптимизации, на которые не всегда существует хороший ответ.

Я думаю, что в следующий раз имплементирую Concat: скорее всего я раскомментирую тесты переполнения для Count. Это вернет нас к операторам, которые возвращают последовательности, но этот - действительно простой.

вторник, 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» несколько раз совершенно сознательно – п

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

воскресенье, 19 февраля 2012 г.

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

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


Предупреждение: этот пост достаточно длинный. Несмотря на то, что я выбрал простой оператор для реализации, в пути мы столкнемся с несколькими исключительными случаями и важными принципами, касающимися LINQ. Это будет отчасти экспериментальный, в плане формата, пост, поскольку я стараюсь выработать наилучший способ подачи материала.

Мы собираемся реализовать выражение/метод/оператор "Where". Его достаточно просто понять в общем, но отложенное выполнение и потоковый вывод могут вызвать проблемы. Он обобщенный, но использует только один параметр типа (что, на мой взгляд, важно: чем больше параметров типа имеет метод, тем сложнее его понять в общем). О, и это стартовая точка для выражений запросов, что является бонусом.

Что это такое?

У «Where» есть 2 перегрузки:

public static IEnumerable<TSource> Where(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)

public static IEnumerable<TSource> Where(
    this IEnumerable<TSource> source,
    Func<TSource, int, bool> predicate)

Перед тем, как я расскажу, что этот код действительно делает, я укажу на пару вещей, которые будут общими для практически всех операторов LINQ, которые мы будем имплементировать:

  • Это – методы расширения. Они объявляются в невложенном статическом классе высшего уровня, и первый параметр содержит модификатор «this». Они могут быть вызваны, как будто являются методами объекта типа первого параметра.
  • Это – обобщенные методы. В данном случае присутствует всего лишь один параметр типа (TSource), который указывает тип последовательности, с которой мы работаем. Поэтому для, скажем, списка строк, TSource будет типа string.
  • Они принимают обобщенные делегаты семейства Func<...>. Они часто определяются при помощи лямбда выражений, но также можно предоставить и делегат.
  • Они работают с последовательностями, которые представлены интерфейсом IEnumerable<T>, с итератором, который представляется интерфейсом IEnumerable<T>./li>

Я ожидаю, что большинство читателей будут знакомы со всеми этими понятиями, поэтому я не буду вдаваться в детали. Если что-то из вышеперечисленного заставляет вас нервничать, пожалуйста, ознакомьтесь с этим перед продолжением, иначе со временем у вас возникнут сложности.

Теперь, несколько важных деталей о поведении:

  • Исходная последовательность никоим образом не модифицируется: это, к примеру, не похоже на List<T>.RemoveAll.
  • Метод использует отложенное выполнение – до того момента, пока вы не попытаетесь достать элементы из конечной последовательности, элементы не начнут извлекаться из исходной последовательности.
  • Несмотря на отложенное выполнение, он сразу проверит, не являются ли параметры null.
  • Он возвращает результаты в потоке – ему всегда нужен только один результат в один момент времени, и он будет возвращать его, не храня на него ссылку. Это значит, что вы можете применять этот метод к последовательностям бесконечной длины (например, к последовательности случайных чисел.)
  • Каждый раз, когда вы будете обходить конечную последовательность, он будет проходить по исходной последовательности точно один раз.
  • Освобождение итератора конечной последовательности освободит соответствующий итератор исходной последовательности. (В случае если вы не знали, оператор foreach в C# использует блок try/finally, чтобы удостовериться, что итератор всегда освобождается, как бы ни закончился цикл).

Многие из этих деталей также будут справедливы и для большинства остальных операторов.

Перегрузка, которая принимает Func<TSource, int, bool> позволяет предикату использовать индекс внутри последовательности так же, как и значение. Индекс всегда начинается с 0, и увеличивается на 1 каждый раз вне зависимости от предыдущих результатов предиката.

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

Было бы идеально, если бы мы протестировали все вышеуказанные аспекты. Детали потокового вывода и количество обходов последовательности являются вещами, с которыми, к сожалению, сложно работать. Зная, сколько нам уже предстоит имплементировать, мы вернемся к этому позже.

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

[Test]
public void SimpleFiltering()
{
    int[] source = { 1, 3, 4, 2, 8, 1 };
    var result = source.Where(x => x < 4);
    result.AssertSequenceEqual(1, 3, 2, 1);
}

Я оставил TestExtensions из MoreLINQ, несмотря на то, что NUnit поставляется с CollectionAssert. Я считаю, что с методами расширения легче работать по трём причинам:

  • Они - методы расширения, и это помогает избегать беспорядка.
  • Они могут использовать массив-параметр для ожидаемого вывода, что делает тест более выразительным.
  • Более понятное сообщение при провале теста.

В основном, AssertSequenceEqual делает то, что вы от него ожидаете – он проверяет, что результат (обычно представленный как переменная, на которой вы вызываете метод) совпадает с ожидаемым результатом (обычно представленный массивом-параметром).

Пока всё идет хорошо. Теперь давайте проверим валидацию аргументов:

[Test]
public void NullPredicateThrowsNullArgumentException()
{
    int[] source = { 1, 3, 7, 9, 10 };
    Func<int, bool> predicate = null;
    Assert.Throws<ArgumentNullException>(() => source.Where(predicate));
}

[Test]
public void WithIndexNullSourceThrowsNullArgumentException()
{
    IEnumerable<int> source = null;
    Assert.Throws<ArgumentNullException>(() => source.Where((x, index) => x > 5));
}

Меня не беспокоит проверка имени внутри ArgumentNullException, я тестирую то, что аргументы проверяются мгновенно. Я не перечисляю конечную последовательность – поэтому, если валидация является отложенной, тест провалится.

Последний интересный тест на данный момент также проверяет отложенное выполнение. Он использует вспомогательный класс ThrowingEnumerable. Это последовательность, которая бросает исключение InvalidOperationException, если вы когда-либо попытаетесь пройти по ней. По существу, мы хотим проверить две вещи:

  • Вызов только Where не начинает процесс обхода исходной последовательности.
  • Когда мы вызываем GetEnumerator() чтобы получить итератор, а потом MoveNext() на этом итераторе, мы должны начать обход не бросая исключения

Мы должны будем сделать что-то подобное для других операторов, поэтому я написал небольшой вспомогательный метод в ThrowingEnumerable:

internal static void AssertDeferred<T>(
Func<IEnumerable<int>, IEnumerable<T>> deferredFunction)
{
    ThrowingEnumerable source = new ThrowingEnumerable();
    var result = deferredFunction(source);
    using (var iterator = result.GetEnumerator())
    {
        Assert.Throws<InvalidOperationException>(() => iterator.MoveNext());
    }
}

Теперь мы можем проверить, действительно ли Where откладывает выполнение:

[Test]
public void ExecutionIsDeferred()
{
    ThrowingEnumerable.AssertDeferred(src => src.Where(x => x > 0));
}

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

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

Имея тесты, которые проходят с настоящим LINQ to Objects, пришло время имплементировать его в нашем производственном коде. Мы собираемся использовать блоки итераторов, которые были представлены в C# 2, чтобы облегчить имплементацию IEnumerable. У меня есть несколько статей, которые вы можете прочесть, если вы хотите получить более глубокие знания… или прочесть шестую главу C# in Depth (любое из изданий). Они дают нам возможность отложенного выполнения бесплатно… но это может быть как хорошо, так и плохо, в чем мы скоро убедимся.

В действительности имплементация будет выглядеть следующим образом:

// Наивная имплементация
public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    foreach (TSource item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
} 

Легко, не правда ли? Блоки итераторов позволяют писать код так, как мы бы его описывали: мы проходим по каждому элементу в исходной последовательности, и если предикат возвращает true для текущего элемента, мы включаем его в конечную последовательность.

Подумать только, некоторые тесты уже проходят. Теперь нам осталось добавить только валидацию аргументов. Это легко, правда? Давайте так и сделаем:

public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source,
            Func<TSource, bool> predicate)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (predicate == null)
    {
        throw new ArgumentNullException("predicate");
    }

    foreach (TSource item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Хмм. Наши тесты валидации до сих пор не проходят, и установка точки прерывания на операторе «throw» не помогает… на ней не происходит остановки. Что же не так?

Я уже дал несколько довольно ясных намёков. Проблемой является отложенное выполнение. До тех пор, пока мы не начнем проходить по результату, наш код не будет выполнен. Наши тесты преднамеренно не проходят по конечной последовательности, поэтому валидация никогда не происходит.

Мы только что натолкнулись на изъян проектирования C#. Блоки итераторов в C# попросту не работают красиво, когда вы хотите разделить выполнение между «мгновенным» (обычно для валидации) и «отложенным».Вместо этого, мы должны разделить нашу имплементацию на две: нормальный метод с валидацией, который потом вызывает метод итератора для отложенной обработки:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (predicate == null)
    {
        throw new ArgumentNullException("predicate");
    }
    return WhereImpl(source, predicate);
}

private static IEnumerable<TSource> WhereImpl<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    foreach (TSource item in source)
    {
        if (predicate(item))
        {
            yield return item;
        }
    }
}

Это некрасиво, но это работает: все наши безиндексные тесты проходят. Требуется небольшой шаг, чтобы имплементировать версию, использующую индексы:

public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source,
            Func<TSource, int, bool> predicate)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (predicate == null)
    {
        throw new ArgumentNullException("predicate");
    }
    return WhereImpl(source, predicate);
}

private static IEnumerable<TSource> WhereImpl<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, int, bool> predicate)
{
    int index = 0;
    foreach (TSource item in source)
    {
        if (predicate(item, index))
        {
            yield return item;
        }
        index++;
    }
}

Теперь тесты зеленые, и мы закончили. Хотя подождите… мы еще не использовали имплементацию всеми возможными способами.

Выражения запросов

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

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

Это сгенерирует точно такой же код, что и наш предыдущий тест. Компилятор, в сущности, транслирует эту форму в предыдущую, оставляя условие (x < 4) в виде лямбда-выражения, позже переводя его соответствующим образом (в данном случае, в делегат). Вы можете быть удивлены, что это работает, ведь у нас пока нет метода Select… но в этом случае мы имеем «холостую» Select-проекцию; на самом деле мы не проводим никакой трансформации. Пока в запросе присутствует что-либо еще (в нашем случае - это условие «where») - компилятор эффективно опускает оператор «select», поэтому не имеет значения, имплементирован он или нет. Если бы мы изменили «select x» на «select x * 2», компиляция нашей только-Where имплементации LINQ не прошла бы.

Факт того, что выражения запросов базируются только на шаблонах типа этого, является очень мощной возможностью для расширения – так, к примеру, LINQ to Rx имплементирует только те операторы, которые актуальны в данной среде. Таким же образом, когда дело касается выражений запросов, компилятор C# ничего не «знает» о IEnumerable - поэтому полностью обособленные интерфейсы, такие, как IObservable, работают как положено.

Чему мы научились?

Многое было затронуто здесь, в обоих смыслах имплементации и основных принципов LINQ:

  • LINQ to Objects основан на методах расширения, делегатах и IEnumerable<T>
  • Операторы используют отложенное выполнение, где это свойственно и выдают свои данных в виде потока, где это возможно.
  • Операторы не меняют источник, вместо этого они возвращают новую последовательность, которая будет содержать нужные данные.
  • Выражения запросов основываются на трансляции паттернов компилятором; вы не должны имплементировать больше паттерном, чем требует соответствующее выражение запроса.
  • Блоки итераторов великолепно подходят для имплементации отложенного выполнения...
  • …но затрудняют валидацию аргументов
Загрузка кода

Linq-To-Objects-2.zip

Многие спрашивали меня о репозитории для кода проекта, и это имеет смысл. Я как раз сейчас создаю репозиторий; скорее всего это будет сделано до того, как я опубликую следующий пост.