LINQ之路 6:延迟执行(Deferred Execution)
LINQ中大部分查询运算符都有一个非常重要的特性:延迟执行。这意味着,他们不是在查询创建的时候执行,而是在遍历的时候执行(换句话说,当enumerator的MoveNext方法被调用时)。让我们考虑下面这个query:
static void TestDeferredExecution()
{
var numbers = new List<int>();
numbers.Add();
IEnumerable<int> query = numbers.Select(n => n * ); // Build query numbers.Add(); // Add an extra element after the query
foreach (int n in query)
Console.Write(n + "|"); // 10|20|
}
可以看出,我们在查询创建之后添加的number也包含在查询结果中了,这是因为直到foreach语句对query进行遍历时,LINQ查询才会执行,这时,数据源numbers已经包含了我们后来添加的元素2,LINQ的这种特性就是延迟执行。除了下面两种查询运算符,所有其他的运算符都是延迟执行的:
- 返回单个元素或者标量值的查询运算符,如First、Count等。
- 下面这些转换运算符:ToArray、ToList、ToDictionary、ToLookup。
上面两种运算符会被立即执行,因为他们的返回值类型没有提供延迟执行的机制,比如下面的查询会被立即执行。
int matches = numbers.Where(n => (n % ) == ).Count(); //
对于LINQ来说,延迟执行时非常重要的,因为它把查询的创建与查询的执行解耦了,这让我们可以向创建SQL查询那样,分成多个步骤来创建我们的LINQ查询。
重复执行
延迟执行带来的一个影响是,当我们重复遍历查询结果时,查询会被重复执行:
static void TestReevaluation()
{
var numbers = new List<int>() { , }; IEnumerable<int> query = numbers.Select(n => n * ); // Build query
foreach (int n in query) Console.Write(n + "|"); // 10|20| numbers.Clear();
foreach (int n in query) Console.Write(n + "|"); // <nothing>
}
有时候,重复执行对我们说可不是一个优点,理由如下:
- 当我们需要在某一个给定的点保存查询的结果时。
- 有些查询比较耗时,比如在对一个非常大的sequence进行查询或者从远程数据库获取数据时,为了性能考量,我们并不希望一个查询会被反复执行。
这个时候,我们就可以利用之前介绍的转换运算符,比如ToArray、ToList来避开重复执行,ToArray把查询结果保存至一个Array,而ToList把结果保存至泛型List<>:
static void TestDefeatReevaluation()
{
var numbers = new List<int>() { , }; List<int> timesTen = numbers
.Select(n => n * )
.ToList(); // Executes immediately into a List<int> numbers.Clear();
Console.Write(timesTen.Count); // Still 2
}
变量捕获
延迟执行还有一个不好的副作用。如果查询的lambda表达式引用了程序的局部变量时,查询会在执行时对变量进行捕获。这意味着,如果在查询定义之后改变了该变量的值,那么查询结果也会随之改变。
static void TestCapturedVariable()
{
int[] numbers = { , }; int factor = ;
IEnumerable<int> query = numbers.Select(n => n * factor);
factor = ;
foreach (int n in query)
Console.Write(n + "|"); // 20|40|
}
这个特性在我们通过foreach循环创建查询时会变成一个真正的陷阱。假如我们想要去掉一个字符串里的所有元音字母,我们可能会写出如下的query:
IEnumerable<char> query = "How are you, friend."; query = query.Where(c => c != 'a');
query = query.Where(c => c != 'e');
query = query.Where(c => c != 'i');
query = query.Where(c => c != 'o');
query = query.Where(c => c != 'u'); foreach (char c in query) Console.Write(c); //Hw r y, frnd.
尽管程序结果正确,但我们都能看出,如此写出来的程序不够优雅。所以我们会自然而然的想到使用foreach循环来重构上面这段程序:
IEnumerable<char> query = "How are you, friend."; foreach(char vowel in "aeiou")
query = query.Where(c => c != vowel); foreach (char c in query) Console.Write(c); //How are yo, friend.
结果中只有字母u被过滤了,咋一看,有没有吃一惊呢!但只要仔细一想就能知道原因:因为vowel定义在循环之外,所以每个lambda表达式都捕获了同一变量。当我们的query执行时,vowel的值是什么呢?不正是被过滤的字母u嘛。要解决这个问题,我们只需把循环变量赋值给一个内部变量即可,如下面的temp变量作用域只是当前的lambda表达式。
IEnumerable<char> query = "How are you, friend."; foreach (char vowel in "aeiou")
{
char temp = vowel;
query = query.Where(c => c != temp);
} foreach (char c in query) Console.Write(c); //Hw r y, frnd.
延迟执行的实现原理
查询运算符通过返回装饰者sequence(decorator sequence)来支持延迟执行。
和传统的集合类型如array,linked list不同,一个装饰者sequence并没有自己用来存放元素的底层结构,而是包装了我们在运行时提供的另外一个sequence。此后当我们从装饰者sequence中请求数据时,它就会转而从包装的sequence中请求数据。
比如调用Where会创建一个装饰者sequence,其中保存了输入sequence的引用、lambda表达式还有其他提供的参数。下面的查询对应的装饰者sequence如图所示:
IEnumerable<int> lessThanTen = new int[] { , , }.Where(n => n < );
当我们遍历lessThanTen时,实际上我们是在通过Where装饰者从Array中查找数据。
而查询运算符链接创建了一个多层的装饰者,每个查询运算符都会实例化一个装饰者来包装前一个sequence,比如下面的query和对应的多层装饰者sequence:
IEnumerable<int> query = new int[] { , , }
.Where(n => n < )
.OrderBy(n => n)
.Select(n => n * );
在我们遍历query时,我们其实是在通过一个装饰者链来查询最初的array。
需要注意的是,如果在上面的查询后面加上一个转换运算符如ToList,那么query会被立即执行,这样,单个list就会取代上面的整个对象模型。
LINQ之路 6:延迟执行(Deferred Execution)的更多相关文章
- LINQ之路 6:延迟执行(Deferred Execution) 笔记
这里刚看的时候不理解. 这个特性在我们通过foreach循环创建查询时会变成一个真正的陷阱.假如我们想要去掉一个字符串里的所有元音字母,我们可能会写出如下的query: IEnumerable< ...
- Linq基础知识之延迟执行
Linq中的绝大多数查询运算符都有延迟执行的特性,查询并不是在查询创建的时候执行,而是在遍历的时候执行,也就是在enumerator的MoveNext()方法被调用的时候执行,大说数Linq查询操作实 ...
- LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法
本篇将是关于LINQ Operators的最后一篇,包括:集合运算符(Set Operators).Zip操作符.转换方法(Conversion Methods).生成器方法(Generation M ...
- LINQ 学习路程 -- 查询操作 Deferred Execution of LINQ Query 延迟执行
延迟执行是指一个表达式的值延迟获取,知道它的值真正用到. 当你用foreach循环时,表达式才真正的执行. 延迟执行有个最重要的好处:它总是给你最新的数据 实现延迟运行 你可以使用yield关键字实现 ...
- linq 延迟执行带来的困扰
有这样一个案例: var filteredResult = from f in orgFileList select f; ; i < WorkStatusFilters.ListWorkSta ...
- Linq延迟执行
LINQ中大部分查询运算符都有一个非常重要的特性:延迟执行.这意味着,他们不是在查询创建的时候执行,而是在遍历的时候执行(换句话说,当enumerator的MoveNext方法被调用时).让我们考虑下 ...
- C#中Linq延迟执行问题
本文来自:http://msdn.microsoft.com/zh-cn/library/bb399393(v=vs.110).aspx http://www.cnblogs.com/zhanglin ...
- LINQ 的查询执行何时是延迟执行,何时是立即执行,以及查询的复用
延迟执行的经典例子: 我们用 select ++i 就可以看到在foreach 时候,查询才被执行. public static void Linq99(){ int[] numbers = n ...
- LINQ之路 9:LINQ to SQL 和 Entity Framework(上)
在上一篇中,我们从理论和概念上详细的了解了LINQ的第二种架构“解释查询”.在这接下来的二个篇章中,我们将使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术的 ...
随机推荐
- PHP小总结
<?php //1.php基础语法 //输出语句 echo print print_r var_dump() //2.php是弱类型语言 //强制转换类型:(类型)变量 settype(变量,类 ...
- React常用的API说明
楼主刚开始学习react,感受到了他的博大精深,看到很多莫名的用法,不知云云,找了很多没有找到参考手册,只有在中文社区和react官方看了一些,收集了一些比较常用的API,有补充的可以楼下评论补充.后 ...
- TCPReplay使用---张子芳
TCPReplay主要功能是将PCAP包重新发送,用于性能或者功能测试.但是在测试环境与原转包系统结构一般是不同的.比如被测试机的二层MAC地址与抓包机器的MAC不同,所以被测试机在二层处理时发现目的 ...
- .net之工作流工程展示及代码分享(预告)
最近在帮公司做一个工作流程序模块,要求是可以嵌入到各种现有的程序中去.我想把自己制作的思路和过程同大家分享. 先上一张结构图: 由于该项目我一个人做,所以系统结构不能太复杂. 用到的技术主要有:DDD ...
- 原生态PHP+mysql 实现分页
<?php/** * 数据分页 * 时间:2016-07-06 *//**1传入页码**/ $page = $_GET['p'];/**2根据页码取数据:php->mysql处理**/$h ...
- 大规模web 服务开发技术
<大规模web 服务开发技术> 是一本讲解大型Web 应用的入门级书籍,能够让我们接触到大应用的知识点. 目录如下: 第1章 大规模Web服务的开发定位——掌握整体第2章 大规模数据处 ...
- wex5 实战 登陆帐号更换与用户id一致性
一 前言 关于用户登陆,注册,页面跳转,我讲了很多,都是页面框架级别的.有人问到这个问题,索性总结一下,今天是收尾最后一篇. 以往初学时的设计是: 1 注册 2 登陆 3 进入页面. 这种很简单,没有 ...
- Centos7下dnscrypt-proxy安装
DNS劫持指的是"一些设备"会拦截DNS解析请求,并将错误的DNS信息返回给主机:DNS污染指的是"一些设备"会将错误的域名信息下发至公共的DNS服务器中(比如 ...
- CSS水平垂直居中的方法
原文链接:http://caibaojian.com/370.html 水平垂直居中,特别是使用在列表的时候经常会用到的,以前有需求的时候我也做过类似的代码,是使用display:table-cell ...
- 《编写可维护的JavaScript》——JavaScript编码规范(一)
缩进层级 代码如何缩进通常有两种主张: 使用制表符缩进 每一个缩进层级都用单独的制表符表示.这种方法的主要缺点是:系统对制表符的解释不一致.这些差异会导致不同的开发者对同一段代码有不同的看法的,这正是 ...