当前位置: 首页 > 编程日记 > 正文

自己动手重新实现LINQ to Objects: 9 - SelectMany

本文翻译自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子句或其他子句,编译器会把fileline包装在一个匿名类型中传递给投影操作。这是查询表达式转译中最令人难理解的一点,因为这涉及到了透明标识符(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实现了SelectWhereConcat

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);

}

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

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

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

结论

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

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

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

相关文章:

1.8 centos7 的PATH、cp/mv/文档查看命令介绍

环境变量PATH什么是环境变量&#xff1f;环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数&#xff0c;如&#xff1a;临时文件夹位置和系统文件夹位置等。[rootcentos7 ~]# echo $PATH #查看PATH环境变量 /usr/local/sbin:/usr/local/bin:/usr/sbin…

刻意练习:LeetCode实战 -- Task11. 删除链表的倒数第N个节点

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

https和http有什么区别?看下面介绍就知道了!

https和http有什么区别?相信很多还在学习软测的同学们&#xff0c;都会有遇到这种问题&#xff0c;下面就是小编给大家介绍的http相关的知识 。 一、http和https基本概念 1. HTTP&#xff1a;是互联网上应用最为广泛的一种网络协议&#xff0c;是一个客户端和服务器端请求和应…

C#图片灰度处理(位深度24→位深度8),用灰度数组byte[]新建一个8位灰度图像Bitmap 。...

原文:C#图片灰度处理(位深度24→位深度8) #region 灰度处理/// <summary>/// 将源图像灰度化&#xff0c;并转化为8位灰度图像。/// </summary>/// <param name"original"> 源图像。 </param>/// <returns> 8位灰度图像。 </return…

日期NSDate的使用

日期类NSDate,存储的是世界标准时(UTC)&#xff0c;输出时需要根据时区转换为本地时间方法description字符串以GMT0展示日期如&#xff1a;2011-11-16 07:02:25 0000测试的北京时间&#xff1a;2011-11-16 15:02:25.324/))))((((/格式化日期类型&#xff0c;使用NSDateFormatte…

刻意练习:LeetCode实战 -- Task12. 合并K个排序链表

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

软测培训机构哪个比较好

软件测试这个岗位是软件开发过程中非常重要的一步&#xff0c;一个软件的开发是少不了软测工程师的&#xff0c;近几年&#xff0c;软测的发展前景越来越可观&#xff0c;很多人都想学习软测技术&#xff0c;那么市面上软测培训机构哪个比较好呢?来看看下面的详细介绍。 软测培…

刻意练习:LeetCode实战 -- Task13. 罗马数字转整数

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

[翻译]ASP.NET MVC 3 开发的20个秘诀(十二)[20 Recipes for Programming MVC 3]:缩放图片尺寸创建缩略图...

议题 用户上传到网站上的大多数的图片都是大尺寸的照片&#xff0c;通常在用户想看完整图片之前网站会展示出这些图片或照片的缩略图。 解决方案 使用以下的类来调整上传的图片文件的宽和高&#xff1a;FileStream&#xff0c;Image&#xff0c;Bitmap和Graphics。 讨论 在下面…

Vue.js双向绑定的实现原理

Vue.js 最核心的功能有两个&#xff0c;一是响应式的数据绑定系统&#xff0c;二是组件系统。本文仅探究双向绑定是怎样实现的。先讲涉及的知识点&#xff0c;再用简化得不能再简化的代码实现一个简单的 hello world 示例。 一、访问器属性 访问器属性是对象中的一种特殊属性&a…

学习UI设计的一些小技巧你会了吗

最近有很多小伙伴都在学习UI设计技术&#xff0c;对于如今的互联网行业&#xff0c;UI设计这个岗位的需求量确实非常大&#xff0c;发展空间比较好&#xff0c;下面小编就为大家整理一些学习UI设计的一些小技巧&#xff0c;希望能够帮助到正在学习UI设计的同学。 学习UI设计的一…

30个精美的模板,贺卡,图形圣诞素材

圣诞节离我们越来越近了&#xff0c;当我们送礼物给我们所爱的&#xff0c;花时间与家人欢乐。我们都喜欢收到圣诞贺卡。&#xff0c;如果你有大量的生活很远的亲戚的话&#xff0c;你可以给他一个Email。本文介绍了从商场收集的30个圣诞素材&#xff0c;你可以创造圣诞的心情&…

刻意练习:LeetCode实战 -- Task14. 最长公共前缀

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

6.1.1 验证注解的使用

数据注解特性定义在名称空间System.ComponentModel.DataAnnotations 中(但接下来 将看到&#xff0c;有些特性不是定义在这个名称空间中)。它们提供了服务器端验证的功能&#xff0c;当在模 型的属性上使用这些特性之一时&#xff0c;框架也支持客户端验证。在名称空间DataAnno…

Javascript创建数组的方式你了解了吗

Javascript数组 数组(Array)是一种复杂的数据类型&#xff0c;它属于Object(对象)类型&#xff0c;用来将一组数组合在一起&#xff0c;通过一个变量就可以访问一组数据。在使用数组时&#xff0c;经常会搭配循环语句使用&#xff0c;从而很方便地对一组数据进行处理。 创建数组…

loadrunner另类玩法【测试帮日记公开课】

https://edu.51cto.com/course/10658.html

刻意练习:LeetCode实战 -- Task15. 有效的括号

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

对口令协议的几种攻击方式

1、窃听入侵者搭线窃听&#xff0c;试图从正在进行的通信中获得有用的信息。 2、重放入侵者记录过去通信中的消息并在以后的通信中重放它们。 3、中间人攻击入侵拦截各主体之间的消息&#xff0c;并用自己的消息来取代它们。在向服务器发送的消息中他假冒用户的身份&#xff0c…

初学者如何学Java开发

初学者如何学Java开发?这是很多人都比较关注的一个问题&#xff0c;尤其是对于零基础想要学习java的同学&#xff0c;java技术语言包含的知识点有很多&#xff0c;下面小编就给大家整理一些建议希望可以帮到初学者们。 初学者如何学Java开发? 1.教材的选择 学习Java书籍的选择…

[算法] [常微分方程] [欧拉法 改进欧拉法 经典R-K算法]

1 #include<iostream>2 #include<cmath>3 #include<cstdio>4 #include<iomanip>5 using namespace std;6 double h0.1;//步差7 double xi[11]{0};8 double ol_yi[11]{1};9 double gol_yi[11]{1}; 10 double rk_yi[11]{1}; 11 double real_yi[11]{1}; 1…

刻意练习:LeetCode实战 -- Task16. 无重复字符的最长子串

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

轻松实现QQ用户接入

1. 申请合作伙伴ID (PID),Key (PKey)2. 发送请求 https://graph.qq.com/oauth2.0/authorize?response_typecode&client_id100000353&redirect_urihttp://www.wodongni.com/loginReturn.aspx redirect_uri:回传URL client_id: 合作伙伴ID (PID) 返回&#xff1a; …

web前端培训之Javascript如何改变数组的长度?

修改数组长度 使用“数组名.length”可以获取或修改数组的长度。数组长度的计算方式为数组中元素的最大索引值加1&#xff0c;示例代码如下。 var arr [a, b, c]; console.log(arr.length); //输出结果:3 在上述代码中&#xff0c;数组中最后一个元素是c&#xff0c;该元素的索…

刻意练习:LeetCode实战 -- Task17. 最长回文子串

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

CvBlobTrackerCC 多目标跟踪算法简析

&#xff08;1&#xff09;跟踪器的建立&#xff1a;对新产生的目标&#xff0c;且宽&#xff08;高&#xff09;大于5时&#xff0c;建立跟踪器 &#xff08;2&#xff09;Kalman滤波&#xff1a;用Kalman滤波器对目标当前的方位、大小做出预测 目标特征矢量采用(x, y, dx, dy…

linux基础学习(二)

------------------------------------------------------------------------------------------------------------------------------------------------0.真机远程管理虚拟机telnet 明文传输 tcp 23ssh 加密传输 tcp 22ssh -X root172.25.0.11 //真机远程管理 ser…

ui设计培训需要什么基础?如何入门学习?

​ UI设计是一种直观面向用户的一个技术岗位&#xff0c;在互联网公司&#xff0c;UI设计岗位是不可或缺的&#xff0c;那么对于零基础想要学习UI设计的同学来说&#xff0c;ui设计培训需要什么基础?如何入门学习呢?我们来看看下面的详细介绍。 ​  ui设计培训需要什么基础…

SQL Server日志清除的两种方法 .

在使用过程中大家经常碰到数据库日志非常大的情况&#xff0c;在这里介绍了两种处理方法…… 方法一 一般情况下&#xff0c;SQL数据库的收缩并不能很大程度上减小数据库大小&#xff0c;其主要作用是收缩日志大小&#xff0c;应当定期进行此操作以免数据库日志过大。 1、设置数…

刻意练习:LeetCode实战 -- Task18. 正则表达式匹配

背景 本篇图文是LSGO软件技术团队组织的 第二期基础算法&#xff08;Leetcode&#xff09;刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式&#xff0c;即选择了五个知识点&#xff08;数组、链表、字符串、树、贪心算法&#xff09;&#xff0c;每个知识点选择了…

CentOS7启动图形界面

1.yum groupinstall "GNOME Desktop" -y 2.systemctl get-default 3.systemctl set-default graphical.target 4.systemctl get-default 5.reboot