Это перевод третьей части серии статей Джона Скита "Реализация 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» несколько раз совершенно сознательно – п
Что-нибудь слегка другое в следующий раз (что, надеюсь, будет через несколько дней). Я не уверен, что именно, но есть еще по-прежнему много методов на выбор…
Комментариев нет:
Отправить комментарий