LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树
- 序列
- 延迟查询执行
- 查询操作符
- 查询表达式
- 表达式树
(一) 序列
先上一段代码,
这段代码使用扩展方法实现下面的要求:
- 取进程列表,进行过滤(取大于10M的进程)
- 列表进行排序(按内存占用)
- 只保留列表中指定的信息(ID,进程名)
var res = Process.GetProcesses()
.Where(s => s.WorkingSet64 > * * )
.OrderByDescending(s => s.WorkingSet64)
.Select(s => new { ID = s.Id, Name = s.ProcessName });
为了能清楚理解上面代码的内部动作,我们需要介绍几组概念.
1. IEnumerable<T>接口
Process.GetProcesses()的返回值是一个Process的数组,而在C#中,所有数组对象均实现了IEnumerable<T>接口.
IEnumerable<T>接口之所以重要,是因为 上面代码中的Where, OrderByDescending, Select 等LINQ中的标准查询操作符都需要使用该类型的对象做为参数.
那么,上面代码中的Where, OrderByDescending, Select 是哪里来的呢? 它们是扩展方法, 基于IEnumerable<T>接口类型的扩展方法.
在LINQ中, 术语"序列" 就是指所有实现了IEnumerable<T>接口的对象.
我们给出Where扩展方法的实现代码:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, Boolean> predicate)
{
foreach (TSource element in source)
{
if (predicate(element))
yield return element;
}
}
其第一参数中的this关键字就证明了它是一个扩展方法,参数类型就是IEnumerable<T>.
关键字yield return 就构成了一个迭代器.
我们来看一下迭代器的背景知识.
2. 迭代器
从结果的角度看,迭代器与一个返回集合数据的传统方法没有什么区别,因为都是返回按一序列排列的值.
比如下面的代码,就返回一个集合的值.
int[] OneTwoThree()
{
return new[] { , , };
}
不过,C#中的迭代器的行为却非常特殊.迭代器将不会一次性返回整个集合中的所有值.而是每次返回一个.这样的设计减少了内存需求.
我们构建一个迭代器的例子,看一看这个特性.
private void button2_Click(object sender, EventArgs e)
{
foreach (var m in OneTwoThree())
{
Console.WriteLine(m);
}
}
static IEnumerable<int> OneTwoThree()
{
Console.WriteLine("returning 1");
yield return ;
Console.WriteLine("returning 2");
yield return ;
Console.WriteLine("returning 3");
yield return ;
}
运行结果如下图
可以看到,函数OneTwoThree直到执行完最后一条语句之后才完整退出.
每次遇到yield return语句时,该方法都向调用者返回一个值.
foreach循环收到这个值后进行了处理,然后控制权又交回给迭代器方法OneTwoThree方法,由它给出下一个元素.
看起来好像两个方法在同时运行.这也正是可以将.NET中的迭代器当作是一类轻量级的协同程序(coroutine)的原因.
(二) 延迟查询执行
LINQ查询语句非常依赖于延迟查询执行机制,惹是缺少了这个机制,LINQ的执行效率将会大大降低.
来看一段代码:
static double Square(double n)
{
Console.WriteLine("计算 Square(" + n + ")...");
return Math.Pow(n, );
}
private void button3_Click(object sender, EventArgs e)
{
int[] numbers = { , , };
var res = from n in numbers
select Square(n);
foreach (var m in res)
Console.WriteLine(m);
}
运行结果如下:
结果可以看到,明显该查询并不是一次性执行完毕的.只有在迭代到某一项时,查询才开始求出这一项的值.
这就是所谓的查询延迟执行的机制在发挥作用.
我们来讨论一下其中的原理:
var res = from n in numbers
select Square(n);
上面的LINQ查询在编译后,实际上变成了这样的:
IEnumerable<double> res = Enumerable.Select<int, double>(numbers, n => Square(n));
也就是LINQ查询转为一系列扩展方法的调用,其中的Enumerable.Select方法正是一个迭代器--这也就是其实现了延迟执行的原理.
如果我们需要查询强制立即执行,可以通过调用ToList方法来实现.
我们把上面的代码改动一下:
private void button4_Click(object sender, EventArgs e)
{
int[] numbers = { , , };
var res = from n in numbers
select Square(n);
foreach (var m in res.ToList())
Console.WriteLine(m);
}
可以看到结果就不同了:
可以见到是先得到查询的结果,最后才把结果迭代输出的.
(三) 查询操作符
上面代码所示的 Where,OrderByDescending, Select这些扩展方法 包含有共同的特性:
- 操作于可被迭代的集合对象之上
- 允许管道形式的数据处理
- 依赖于延迟执行
正是上面这些特征让这些扩展方法能用于编写查询.因此这些扩展方法也称为"查询操作符"
查询操作符是LINQ的核心,甚至比语言方面的特性(比如查询表达式)更重要.
下图是按照操作类型分组的标准查询操作符:
(四) 查询表达式
开往篇的程序是使用查询操作符实现的.再次引用一下:
var res = Process.GetProcesses()
.Where(s => s.WorkingSet64 > * * )
.OrderByDescending(s => s.WorkingSet64)
.Select(s => new { ID = s.Id, Name = s.ProcessName });
另一种语法则让LINQ查询更像是SQL的查询语句.
var res = from s in Process.GetProcesses()
where s.WorkingSet64 > * *
orderby s.WorkingSet64 descending
select new { ID = s.Id, Name = s.ProcessName };
上面的这种写法就叫做查询表达式,或者查询式语法.
这两种代码的写法从语义上来讲是完全相同的,而且实现的功能也一致.
查询表达式是由C#语言提供的语言级特性,一种语法糖,这种语法类似于SQL,它可以操作于一个或者多个数据源之上,并为这些数据源应用若干个标准或者自定义的查询操作符.在上面的示例代码中,使用了3个标准的查询操作符:Where, orderByDescending以及Select.
在使用查询表达式语法时,编译器会自动将其转化为对标准查询操作符的调用.
查询表达式存在的最主要意义在于,它能够大大简化查询所需要的代码,并提高查询语句的可读性(类似熟悉的SQL).
下图是查询表达式的完整语法:
标准查询操作符与查询表达式的关系,见下表所示:
通过上表可以看到,不是每一个操作符都有与之对应的C#关键字.在前面那个简单的查询中,我们当然完全可以使用语言所提供的关键字实现.不过对于那些较为复杂的查询来说,我们将不得不直接调用查询操作符完成.
因为查询表达式最终都会被编译成各个标准操作符的调用.因此如果愿意的话,完全可以只用查询操作符编写所有查询语句,根本不理会查询表达式的存在.
(五) 表达式树
Lambda表达式在前面提到过它的主要作用之一是实现匿名委托.如下例:
Func<int,bool> isOdd=i=>(i & )==;
但是,Lambda表达式也能够以数据的形式使用,这正是表达式树所要求的.
当把代码改成下面这样时,我们就无法以委托的形式来使用isOdd了.因为在这里isOdd并不是委托,而是个表达式树.
Expression<Func<int,bool>> isOdd =i => (i & ) ==;
编译器不会把上面的Lambda表达式换成IL代码,而是会构造出一个树状的,用来表示该表达式的对象.
但是需要注意的是:只有那些带有表达式体的Lambda表达式才能够用于表达式树.主体部分是语句的Lambda表达式则没有类似的支持.
例如,下面第1行代码可以用来生成一颗表达式树,因为其带有表达式体.
第2行的就不能,因为它的主体部分是一个语句.
Expression<Func<Object, Object>> identity = o=>o;
Expression<Func<Object, Object>> identity = o=>{ return o;};
当编译器看到某个Lambda表达式赋值给类型为Expression<>的变量时,就会将其编译成一系列工厂方法的调用,这些工厂方法将在程序运行时动态地构造出表达式树.
下面就是编译器为上述表达式自动生成的代码:
ParameterExpression i = Expression.Parameter(typeof(int), "i");
Expression<Func<int, bool>> isOdd =
Expression.Lambda<Func<int, bool>>(
Expression.Equal(
Expression.And(
i,
Expression.Constant(, typeof(int))),
Expression.Constant(, typeof(int))),
new ParameterExpression[] { i });
上面的代码是可以手工编写的,但是编译器可以代劳.
表达式树将在程序运行中动态构造,不过一旦构造完成,则无法被再次修改.
表达式树在第5章中用以创建动态查询这种高级场景上得到了应用.
上面的表达式树,在内存中以树的数据结构存储,它表示解析了后的Lambda表达式,如下图:
上面的表达式树,还可以"逆向"编译成委托方法:
Expression<Func<int, bool>> isOddExpression = i => (i & ) == ;
Func<int, bool> isOddCompiledExpression = isOddExpression.Compile();
这时候,上面的isOddCompiledExpression和下面的委托isOdd就完全相同了,它们生成的IL代码就没有任何区别了.
Func<int,bool> isOdd=i=>(i & )==;
为什么要使用表达式树呢?
实际上,表达式树就是一颗抽象语法树(AST).抽象语法树用来表示一段经过解析的代码.在上面例子中,这颗树就是C#对于Lambda表达式解析后的结果.这样做的目的是便于其它代码对该表达式树进行分析,并执行一些必要的操作.
表达式树可以在运行时传递给其它的工具,随后这些工具可以根据该树开始执行查询,或者是将其转化为其它形式的代码,例如LINQ to SQL中的SQL语句.
最后我们来看看表达式树执行延迟查询执行的方法:
引用之前LINQ to SQL例子中的代码:
var contacts =
from contact in db.GetTable<HelloLinqToSql.Contact>()
where contact.City == "武汉"
select contact; Console.WriteLine("查找在武汉的联系人"+Environment.NewLine);
foreach (var contact in contacts)
Console.WriteLine("联系人: " + contact.Name.Trim()+" ID:"+contact.ContactID);
我们知道使用IEnumerable<T>迭代器可以产生延迟查询的行为,在上面代码中 contacts变量的类型不是IEnumerable<T>,而是IQueryable<Contact>.
处理IQueryable<Contact>数据与处理序列完全不同.IQueryable<Contact>的实例将要接受一棵表达式树,由些分析出下一步将要进行的操作.
在上面代码中,一旦我们开始遍历contacts变量,那么程序就会开始分析其中包含的表达式树,随后生成SQL语句并执行,最后该SQL语句的返回结果以Contact对象集合的形式给出.
与基于IEnumerable<T>的序列相比, IQueryable<Contact>更加强大,因为程序可以根据表达式树的分析结果进行智能地处理.通过查看某个查询的表达式树,编译器即可智能地进行推断并进行大量的优化.IQueryable<Contact>和表达式树的组合将给我们带来更强大的可定制能力.
原创文章,出自"博客园, 猪悟能'S博客" : http://www.cnblogs.com/hackpig/
LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树的更多相关文章
- LinQ实战学习笔记(四) LINQ to Object, 常用查询操作符
这一篇介绍了下面的内容: 查询object数组 查询强类型数组 查询泛型字典 查询字符串 SelectMany 索引 Distinct操作符 排序 嵌套查询 分组 组连接 内连接 左外连接 交叉连接 ...
- LinQ实战学习笔记(一) LINQ to (Objects, XML, SQL) 入门初步
LINQ对于笔者来说, 优美而浓缩的代码让人震惊. 研究LINQ就是在艺术化自己的代码. 之前只是走马观花学会了基本的语法, 但是经常在CSDN看到令人惊讶自叹不如的LINQ代码, 还是让人羡慕嫉妒恨 ...
- LinQ实战学习笔记(二) C#增强特性
C# 为支持LINQ添加了许多语言特性: 隐式类型局部变量 对象初始化器 Lambda表达式 扩展方法 匿名类型 了解这些新特性是全面了解LINQ的重要先解条件,因此请不要忽视它们. (一) 隐式类 ...
- Oracle学习笔记三 SQL命令
SQL简介 SQL 支持下列类别的命令: 1.数据定义语言(DDL) 2.数据操纵语言(DML) 3.事务控制语言(TCL) 4.数据控制语言(DCL)
- java之jvm学习笔记三(Class文件检验器)
java之jvm学习笔记三(Class文件检验器) 前面的学习我们知道了class文件被类装载器所装载,但是在装载class文件之前或之后,class文件实际上还需要被校验,这就是今天的学习主题,cl ...
- Liunx学习笔记(三) 文件权限
一.文件权限 1.查看文件权限 (1)文件权限 在 Linux 中对于文件有四种访问权限,列举如下: 可读取:r,Readable 可写入:w,Writable 可执行:x,Execute 无权限:- ...
- MySql学习笔记三
MySql学习笔记三 4.DML(数据操作语言) 插入:insert 修改:update 删除:delete 4.1.插入语句 语法: insert into 表名 (列名1,列名2,...) val ...
- iView学习笔记(三):表格搜索,过滤及隐藏列操作
iView学习笔记(三):表格搜索,过滤及隐藏某列操作 1.后端准备工作 环境说明 python版本:3.6.6 Django版本:1.11.8 数据库:MariaDB 5.5.60 新建Django ...
- Redis in Action : Redis 实战学习笔记
1 1 1 Redis in Action : Redis 实战学习笔记 1 http://redis.io/ https://github.com/antirez/redis https://ww ...
随机推荐
- STL容器删除元素的陷阱
今天看Scott Meyers大师的stl的用法,看到了我前段时间犯的一个错误,发现我写的代码和他提到错误代码几乎一模一样,有关stl容器删除元素的问题,错误的代码如下:std::vector< ...
- glow
原则是: 先把原场景渲染到fbo,然后渲染发光的物体 然后叠加,但是问题来了,发光物体是另外一个fbo里渲染的,他没和原场景进行深度测试,导致全部绘制了,叠到一起的时候原先不该显示的部分显示 然后我立 ...
- 通过 Storyboard 快速搭建一系列连贯性的视图控制器
此例子只是一个简单的 Demo,这里没有过多介绍如何去实现,网上有很多关于 Storyboard 技术的介绍,请自行搜索. 效果如下: iPhone 5s iPhone 6 iPhone 6 ...
- Codeforces Beta Round #13 C. Sequence (DP)
题目大意 给一个数列,长度不超过 5000,每次可以将其中的一个数加 1 或者减 1,问,最少需要多少次操作,才能使得这个数列单调不降 数列中每个数为 -109-109 中的一个数 做法分析 先这样考 ...
- SQL 触发器 instead of | insert
create trigger tgr_Insert on A instead of insert as print 'Hello World' go insert into A values('100 ...
- _set_invalid_parameter_handler异常处理函数
VS2005之后的版本,微软增加了一些新的异常机制,新机制在出现错误时默认不通知应用程序,这时程序就崩溃了.所以这种情况下,必须调用_set_invalid_parameter_handler._se ...
- 【.Net底层剖析】2.stfld指令-给对象的字段赋值
.Net底层剖析目录章节 1.[深入浅出.Net IL]1.一个For循环引发的IL 2.[.Net底层剖析]2.stfld指令-给对象的字段赋值 3.[.Net底层剖析]3.用IL来理解属性 引言: ...
- java中DatagramSocket连续发送多个数据报包时产生丢包现象解决方案
try { //向指定的ip和端口发送数据~! //先说明一下数据是谁发送过来的! byte[] ip = InetAddress.getLocalHost().getHostAddress().ge ...
- bootstrap插件学习-bootstrap.tab.js
先看bootstrap-tab.js的结构 var Tab = function ( element ) {} //构造器 Tab.prototype ={} //构造器的原型 $.fn.tab = ...
- 精品素材:WALK & RIDE 单页网站模板下载
今天,很高兴能向大家分享一个响应式的,简约风格的 HTML5 单页网站模板.Walk & Ride 这款单页网站模板是现代风格的网页模板,简洁干净,像素完美,特别适合用于推广移动 APP 应用 ...