自己动手重新实现LINQ to Objects: 3 - Select
本文翻译自Jon Skeet的系列博文“Edulinq”。
本篇原文地址:
距离上次写完本系列博文的第一篇和第二篇已经有一段日子了,希望接下来的进度会快一些。
现在我给本项目在Google Code上建立了源码管理,现在就无需每篇博文包含一个zip文件了。创建项目时,我给它取了个显而易见的名字,叫做Edulinq。我修改了代码中的命名空间,而且现在这一系列博文的tag也修改为了Edulinq了。好了,闲话少叙...我们来开始重新实现LINQ吧,这次要实现Select操作符。
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)
其第二个重载让投影操作可以访问到序列元素的index。
先说简单的东西:Select方法把一个序列投影成为另一个序列:“selector”这个作为参数的委托会被依次应用到输入序列中的每一个元素上,并每次yield返回一个输出元素。Select的行为和Where很类似(实在是太类似了,以至于下面一段文字都是从上一篇文章中复制过来的,只是稍加修改):
l Select不会对输入序列做任何修改。
l Select是延迟执行的 - 在你开始读取输出序列中的元素之前,Select不会去输入序列中取元素。
l 不过也有一点不是延迟执行的,它会立即检查参数是否为null。
l 它以流式处理结果:它每次只处理一个结果元素。
l 你每在输出序列上迭代一次,Select方法就会在输入序列上迭代一次,这二者是严格对应的。
l 每次yield返回结果值的时候,“selector”这个委托就会被调用一次。
l 如果输出序列的迭代器被Dispose掉的话,对应的输入序列的迭代器也会被Dispose掉。
我们要测试什么?
对Select的测试和对Where的测试也是很类似的,之前我们是针对Where的过滤功能来做测试,现在我们是针对Select的投影功能来做测试。
有几个测试比较有趣。首先,你会发现Select方法是泛型的,而且有两个泛型参数,分别是TSource和TResult。虽然这两个参数的含义不言自明,不过还是得写一个单元测试来测一下TSource和TResult分别为不同类型的情况,比如说把int转换成string的情况。
[Test]
public void SimpleProjectionToDifferentType()
{
int[] source = { 1, 5, 2 };
var result = source.Select(x => x.ToString());
result.AssertSequenceEqual("1", "5", "2");
}
然后我们看另一个测试,这个测试给我们展示了使用LINQ有可能会遇到的奇怪的副作用。其实我们本可以在Where的单元测试中做这个例子的,不过针对Select做起来更清晰一些:
[Test]
public void SideEffectsInProjection()
{
int[] source = new int[3]; // Actual values won't be relevant
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一次,但是对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的方式差不多。我只是把Where的实现的代码复制过来,稍加修改,这二者真的就是如此的相似。详细说来就是:
l 我们利用迭代器代码块来轻松实现序列的返回。
l 要用到迭代器代码块就意味着必须要把参数校验的代码和核心实现代码分离开。(我写完上一篇博文之后了解到VB11中将会有匿名迭代器,匿名迭代器可以解决这个问题。哎。羡慕VB用户的感觉怪怪的,但是我会学着接受现实的。)
l 我们在迭代器代码块中使用foreach,这样就可以保证在输出序列的迭代器被Dispose时或者输入序列的元素被迭代完时,输入序列的迭代器可以被妥当的Dispose掉。
由于Select的实现和Where的实现实在是太类似了,下面我直接给出代码。Select方法的重载(含有index的那一个)的实现代码就不展示了,因为它和下面的代码差别实在太小了。
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很类似”,强调了那么多次,搞得都有点乏味了,不过这样才足以说明实现Select并没有你可能想象的那么复杂。
下次(我希望就在几天之内)我会写点不一样的东西。我还不确定下次要写哪个方法,待选的方法还有很多...