本文翻译自Jon Skeet的系列博文“Edulinq”。

本篇原文地址:

http://msmvps.com/blogs/jon_skeet/archive/2010/12/27/reimplementing-linq-to-objects-part-9-selectmany.aspx

我们接下来要实现的这个操作符是LINQ中最重要的操作符。大多数(或者是全部?)其他的返回一个序列的操作符都可以通过调用SelectMany来实现,这是后话按下不表。现在我们首先来实现它吧。

SelectMany是什么?

SelectMany有四个重载,看起来一个比一个吓人:

public static IEnumerable<TResult> SelectMany<TSource, TResult>(

this IEnumerable<TSource> source,

Func<TSource, IEnumerable<TResult>> selector)

public static IEnumerable<TResult> SelectMany<TSource, TResult>(

this IEnumerable<TSource> source,

Func<TSource, int, IEnumerable<TResult>> selector)

public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(

this IEnumerable<TSource> source,

Func<TSource, IEnumerable<TCollection>> collectionSelector,

Func<TSource, TCollection, TResult> resultSelector)

public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(

this IEnumerable<TSource> source,

Func<TSource, int, IEnumerable<TCollection>> collectionSelector,

Func<TSource, TCollection, TResult> resultSelector)

其实还不算太坏,这些重载只是同一个操作的不同形式而已。

无论是哪个重载,都需要一个输入序列。然后用一个委托来处理输入序列中的每个元素以生成一个子序列,这个委托可能会接受一个代表元素index的参数。

再然后,我们或者把每个子序列中的元素直接返回,或者再用另一个委托来做处理,这个委托接受输入序列中的元素并接受其对应的子序列中的元素。

以我的经验来说,使用index两个重载不太常用,而另外两个重载(上面列出的第一个和第三个)则比较常用。还有,当C#编译器处理一个含有多个from子句的查询表达式的时候,它会把出第一个from之外的其他from子句转译为上面的第三个重载。

为了把上面的说法放入实例中理解,我们假设有这样一个查询表达式:

var query = from file in Directory.GetFiles("logs")

from line in File.ReadLines(file)

select Path.GetFileName(file) + ": " + line;

上面的查询表达式会被转译为下面的“正常”调用:

var query = Directory.GetFiles("logs")

.SelectMany(file => File.ReadLines(file),

(file, line) => Path.GetFileName(file) + ": " + line);

这个例子中,编译器会把表达式中的select子句转译为投影操作;如果表达式后面还跟有where子句或其他子句,编译器会把file和line包装在一个匿名类型中传递给投影操作。这是查询表达式转译中最令人难理解的一点,因为这涉及到了透明标识符(transparent identifiers)。就现在来说,我们只分析上面给出的简单例子。

上例中的SelectMany接受三个参数:

l 输入序列,也就是一个字符串序列(Directory.GetFiles所返回的文件名)

l 一个初始投影操作,它把一个文件名转化为该文件中包含的一行行的字符串

l 一个结束投影操作,它把一个文件名和一行文件内容转化为一个由冒号分隔的字符串

表达式的最后结果会是一个字符串的序列,其中包含所有log文件的每一行,每一行会以文件名作为前缀。如果把结果打印出来,大概会是这样的:

test1.log: foo

test1.log: bar

test1.log: baz

test2.log: Second log file

test2.log: Another line from the second log file

要理解SelectMany可能会费点脑子,我当时理解它就费了点力,不过理解它是很重要的。

在讲测试之前,还有几点关于SelectMany的行为细节需要说明:

l 参数校验是立即执行的,每个参数都不能是null

l 整个过程都是流式处理的。每次只会从输入序列中读取一个元素,然后生成一个子序列。然后每次只会返回子序列中的一个元素,返回子序列中的全部元素之后再去读取输入序列中的下一个元素,用它来生成下一个子序列,如此循环往复。

l 每个迭代器在使用完之后都会被关闭,正如你会预期的一样。

我们要测试什么呢?

我有一点变懒了,我不想再写参数为null的测试了。我给SelectMany的每一个重载都写了一个测试。我发现我无法把这些测试写得很清晰,不过还是拿出一个例子来,下面的代码是针对SelectMany的最复杂的重载的测试:

[Test]

public void FlattenWithProjectionAndIndex()

{

int[] numbers = { 3, 5, 20, 15 };

var query = numbers.SelectMany((x, index) => (x + index).ToString().ToCharArray(),

(x, c) => x + ": " + c);

// 3 => "3: 3"

// 5 => "5: 6"

// 20 => "20: 2", "20: 2"

// 15 => "15: 1", "15: 8"

query.AssertSequenceEqual("3: 3", "5: 6", "20: 2", "20: 2", "15: 1", "15: 8");

}

给这个测试做一点解释:

l 每一个数字都和它的序号相加 (3+0, 5+1, 20+2, 15+3)

l 相加的结果转成字符串,然后转成字符数组。(我们原本不需要调用ToCharArray的,因为String本身就实现了IEnumerable<char>,不过现在这样写比较清晰。)

l 然后把子序列中的每一个字符和原元素以“原元素:子序列字符”的形式组合在一起

注释部分是每一个输入元素对应的输出结果,测试最后一句代码给出了完整的输出序列。

是不是一团乱麻?希望你看了上面逐步分解的解释很清楚一点。好了,现在想办法让测试可以通过吧。

开始动手实现吧!

我们可以通过实现一个最复杂的重载并让其他的重载都调用它来实现SelectMany,或者也可以写一个没有参数校验的“Impl”方法,然后让四个重载都调用它。比如说,最简单重载可以这样实现:

public static IEnumerable<TResult> SelectMany<TSource, TResult>(

this IEnumerable<TSource> source,

Func<TSource, IEnumerable<TResult>> selector)

{

if (source == null)

{

throw new ArgumentNullException("source");

}

if (selector == null)

{

throw new ArgumentNullException("selector");

}

return SelectManyImpl(source,

(value, index) => selector(value),

(originalElement, subsequenceElement) => subsequenceElement);

}

不过我还是选择为每一重载写一个签名相同的“SelectManyImpl”方法。我觉得这样做可以让以后单步调试时更简单一些...而且这样让我们可以注意到不同重载之间的区别,代码是这样的:

// Simplest overload

private static IEnumerable<TResult> SelectManyImpl<TSource, TResult>(

IEnumerable<TSource> source,

Func<TSource, IEnumerable<TResult>> selector)

{

foreach (TSource item in source)

{

foreach (TResult result in selector(item))

{

yield return result;

}

}

}

// Most complicated overload:

// - Original projection takes index as well as value

// - There's a second projection for each original/subsequence element pair

private static IEnumerable<TResult> SelectManyImpl<TSource, TCollection, TResult>(

IEnumerable<TSource> source,

Func<TSource, int, IEnumerable<TCollection>> collectionSelector,

Func<TSource, TCollection, TResult> resultSelector)

{

int index = 0;

foreach (TSource item in source)

{

foreach (TCollection collectionItem in collectionSelector(item, index++))

{

yield return resultSelector(item, collectionItem);

}

}

}

这两个方法之间的相似性很是明显...不过我还是觉得保留着第一种形式很有用,如果我搞不清楚SelectMany的作用的话,通过第一种最简单的重载就可以很容易的弄懂。以此为基础再去理解余下的重载,跳跃性就不会那么大了。第一个重载在一定程度上起到了一个理解SelectMany的概念的垫脚石的作用。

有两点需要指出:

如果C#中可以使用“yield foreach selector(item)”这种表达式的话,上面的第一个方法就可以实现的稍简单一点。如果要在第二个方法中使用这种做法的话就会难一些,而且可能还要涉及到对Select的调用,这样的话就有点得不偿失了。

在第二个方法中,我没有显式的使用“checked”代码块,虽然说“index”是有可能溢出的。我没有看过BCL的实现是什么样的,但是我认为他们不会写“checked”的。考虑到前后一致性,我或许应该在每一个处理index的方法中都是用“checked”代码块,或者给整个程序集开启“checked”。

通过调用SelectMany来实现其他操作符

之前我提到过很多的LINQ操作符都可以通过调用SelectMany来实现。下面的代码就是这一观点的实例,我们通过调用SelectMany实现了Select,Where和Concat:

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 source.SelectMany(x => Enumerable.Repeat(selector(x), 1));

}

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 source.SelectMany(x => Enumerable.Repeat(x, predicate(x) ? 1 : 0));

}

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 new[] { first, second }.SelectMany(x => x);

}

Select和SelectMany使用Enumerable.Repeat来很方便的创建含有一个元素或不包含任何元素的序列。你也可以通过创建一个数组来代替使用Repeat的这种做法。Concat直接使用了一个数组:如果你理解了SelectMany的作用就是把多个序列组合为一个序列这一点的话,Concat这样实现看起来就很自然了。我估计Empty和Repeat可以通过递归来实现,尽管这样的话性能会很差。

现在,上面的代码是放在条件编译块里面的。如果大家希望我多写一些借助于SelectMany来实现的操作符的话,我可能会考虑把它单独分离一个项目出来。不过我感觉以上的代码已经足以显示SelectMany的灵活性了,再利用SelectMany来实现更多的其他操作符也未必能更加充分的说明这一点。

在理论的意义上,SelectMany也很重要,因为它为LINQ提供了monadic的特性。我不想在这一话题上说的更多,你可以读一读Wes Dyer的博客,或者直接搜索“bind monad SelectMany”就可以找到很多比我更聪明的人写的文章。

结论

SelectMany是LINQ中的基础之一,初看上去它很是令人生畏。但是一旦你理解了SelectMany的作用就是把多个序列组合起来这一点之后,它就很容易搞懂了。

下一次我们讨论All和Any,这两个操作符很适合放在一起来讲解。

转载于:https://www.cnblogs.com/cuipengfei/archive/2011/12/15/2289564.html

自己动手重新实现LINQ to Objects: 9 - SelectMany相关推荐

  1. Does the “LINQ to Objects” provider have built-in performance optimization?

    让我们从基础开始,可能会重复一些你已经知道的信息.在LINQ性能优化中最重要的一点,当然是延迟执行.那便意味着当你声明一个变量并分配给它一个查询字符串,其查询字符串并没有立即执行 // Query i ...

  2. 【翻译】Pro LINQ Language Integrated Query in C# 2008 -- 第三章 (LINQ TO Objects) 第一节

    LINQ到Objects LINQ到Objects介绍 示例 3-1. 一个LINQ到Objects查询的示例. string[] presidents = {     "Adams&quo ...

  3. 从LINQ开始之LINQ to Objects(上)

    LINQ概述 LINQ,语言集成查询(Language Integrated Query),它允许使用C#或VB代码以查询数据库相同的方式来操作不同的数据源. LINQ体系结构 从上图可以看出,LIN ...

  4. 从LINQ开始之LINQ to Objects(下)

    前言 上一篇<从LINQ开始之LINQ to Objects(上)>主要介绍了LINQ的体系结构.基本语法以及LINQ to Objects中标准查询操作符的使用方法. 本篇则主要讨论LI ...

  5. [翻译]“LINQ to Objects”提供程序是否内置性能优化?

    原文来自Alexandra Rusina在CSharpFAQ的:Does the "LINQ to Objects" provider have built-in performa ...

  6. 查询表达式和LINQ to Objects

    查询表达式实际上是由编译器"预处理"为"普通"的C#代码,接着以完全普通的方式进行编译.这种巧妙的发式将查询集合到了语言中,而无须把语义改得乱七八糟 LINQ的 ...

  7. LINQ: Reconciling objects, relations and XML in the .NET framework

    自己译的,感觉意译挺多,不中足之处还请见谅,也算帮找这篇文章的朋友一个忙,转载前没有见过相关协议与说明,如果此篇翻译侵犯了作者权益,请与本人联系,本人将关闭此文章 LINQ: Reconciling  ...

  8. Linq to objects示例

    与linq to sql类似,所有继承了IEnumerable的类型均可使用LINQ,如下string[]数组基类为Array,而Array实现了IEnumerable,所以也可使用linq stat ...

  9. C# LINQ的Select与SelectMany函数

    Select扩展函数: 将序列中的每个元素投影到新表单. 返回结果::       System.Collections.Generic.IEnumerable`1 其元素是调用转换函数的每个元素的结 ...

最新文章

  1. jQuery操作标签--样式、文本、属性操作, 文档处理
  2. Linux命令(10)——vim编辑器
  3. Android 4.4(KitKat)中的设计模式-Graphics子系统
  4. C++(STL):09---vector迭代器失效问题
  5. iOS开发隐藏tabBar的问题
  6. Vue 第一天:模板语法
  7. SpringBoot加载静态资源
  8. TCP/IP协议详解、TCP三次握手
  9. Git分布式版本控制
  10. laravel 分页和共多少条 加参数的分页链接
  11. Struts2 - Action no cache
  12. Matlab系列教程_基础知识_数据类型
  13. spring学习之spring框架介绍
  14. ps怎么把图片透明化
  15. 淘宝省钱攻略–人人可以做到
  16. Web指纹识别技术研究与优化实现(CMS)
  17. application octet stream java_Java servlet句柄application/octet-stream用于图像上传
  18. 拉链表断链、交叉链判断及处理方式
  19. 阿里云SSL域名证书申请和宝塔配置
  20. Stack frame omission (FPO) optimization part1

热门文章

  1. yii2 模型中set_Day184:人脸识别中open-set与close-set
  2. 笛卡尔积 php,PHP自定义函数生成笛卡尔积
  3. android在线切图工具,9Cut切图工具
  4. 我的世界服务器ess配置信息,[搬运贴]超详细的Ess基础插件介绍
  5. python 整数逆位运算_python训练营:注释、运算符、数据类型与位运算
  6. 北航与西安交大计算机专业对比,北航和西安交大,谁的工科实力更强,该上哪一所大学呢?...
  7. matlab中的mkdir函数_科学网—Matlab中计算函数运行时间的三种方法及判断新建文件夹 - 张伟的博文...
  8. 机器学习四剑客4——PIL
  9. 剑指offer:面试题13. 机器人的运动范围
  10. Python模块MySQLdb操作mysql出现2019错误:Can't initialize character set utf-8