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

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

Комментариев нет:

Отправить комментарий