• 序列
  • 延迟查询执行
  • 查询操作符
  • 查询表达式
  • 表达式树

(一) 序列

先上一段代码,

这段代码使用扩展方法实现下面的要求:

  • 取进程列表,进行过滤(取大于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实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树的更多相关文章

  1. LinQ实战学习笔记(四) LINQ to Object, 常用查询操作符

    这一篇介绍了下面的内容: 查询object数组 查询强类型数组 查询泛型字典 查询字符串 SelectMany 索引 Distinct操作符 排序 嵌套查询 分组 组连接 内连接 左外连接 交叉连接 ...

  2. LinQ实战学习笔记(一) LINQ to (Objects, XML, SQL) 入门初步

    LINQ对于笔者来说, 优美而浓缩的代码让人震惊. 研究LINQ就是在艺术化自己的代码. 之前只是走马观花学会了基本的语法, 但是经常在CSDN看到令人惊讶自叹不如的LINQ代码, 还是让人羡慕嫉妒恨 ...

  3. LinQ实战学习笔记(二) C#增强特性

    C# 为支持LINQ添加了许多语言特性: 隐式类型局部变量 对象初始化器 Lambda表达式 扩展方法 匿名类型 了解这些新特性是全面了解LINQ的重要先解条件,因此请不要忽视它们. (一)  隐式类 ...

  4. Oracle学习笔记三 SQL命令

    SQL简介 SQL 支持下列类别的命令: 1.数据定义语言(DDL) 2.数据操纵语言(DML) 3.事务控制语言(TCL) 4.数据控制语言(DCL)  

  5. java之jvm学习笔记三(Class文件检验器)

    java之jvm学习笔记三(Class文件检验器) 前面的学习我们知道了class文件被类装载器所装载,但是在装载class文件之前或之后,class文件实际上还需要被校验,这就是今天的学习主题,cl ...

  6. Liunx学习笔记(三) 文件权限

    一.文件权限 1.查看文件权限 (1)文件权限 在 Linux 中对于文件有四种访问权限,列举如下: 可读取:r,Readable 可写入:w,Writable 可执行:x,Execute 无权限:- ...

  7. MySql学习笔记三

    MySql学习笔记三 4.DML(数据操作语言) 插入:insert 修改:update 删除:delete 4.1.插入语句 语法: insert into 表名 (列名1,列名2,...) val ...

  8. iView学习笔记(三):表格搜索,过滤及隐藏列操作

    iView学习笔记(三):表格搜索,过滤及隐藏某列操作 1.后端准备工作 环境说明 python版本:3.6.6 Django版本:1.11.8 数据库:MariaDB 5.5.60 新建Django ...

  9. Redis in Action : Redis 实战学习笔记

    1 1 1 Redis in Action : Redis  实战学习笔记 1 http://redis.io/ https://github.com/antirez/redis https://ww ...

随机推荐

  1. STL容器删除元素的陷阱

    今天看Scott Meyers大师的stl的用法,看到了我前段时间犯的一个错误,发现我写的代码和他提到错误代码几乎一模一样,有关stl容器删除元素的问题,错误的代码如下:std::vector< ...

  2. glow

    原则是: 先把原场景渲染到fbo,然后渲染发光的物体 然后叠加,但是问题来了,发光物体是另外一个fbo里渲染的,他没和原场景进行深度测试,导致全部绘制了,叠到一起的时候原先不该显示的部分显示 然后我立 ...

  3. 通过 Storyboard 快速搭建一系列连贯性的视图控制器

    此例子只是一个简单的 Demo,这里没有过多介绍如何去实现,网上有很多关于 Storyboard 技术的介绍,请自行搜索. 效果如下: iPhone 5s   iPhone 6   iPhone 6 ...

  4. Codeforces Beta Round #13 C. Sequence (DP)

    题目大意 给一个数列,长度不超过 5000,每次可以将其中的一个数加 1 或者减 1,问,最少需要多少次操作,才能使得这个数列单调不降 数列中每个数为 -109-109 中的一个数 做法分析 先这样考 ...

  5. SQL 触发器 instead of | insert

    create trigger tgr_Insert on A instead of insert as print 'Hello World' go insert into A values('100 ...

  6. _set_invalid_parameter_handler异常处理函数

    VS2005之后的版本,微软增加了一些新的异常机制,新机制在出现错误时默认不通知应用程序,这时程序就崩溃了.所以这种情况下,必须调用_set_invalid_parameter_handler._se ...

  7. 【.Net底层剖析】2.stfld指令-给对象的字段赋值

    .Net底层剖析目录章节 1.[深入浅出.Net IL]1.一个For循环引发的IL 2.[.Net底层剖析]2.stfld指令-给对象的字段赋值 3.[.Net底层剖析]3.用IL来理解属性 引言: ...

  8. java中DatagramSocket连续发送多个数据报包时产生丢包现象解决方案

    try { //向指定的ip和端口发送数据~! //先说明一下数据是谁发送过来的! byte[] ip = InetAddress.getLocalHost().getHostAddress().ge ...

  9. bootstrap插件学习-bootstrap.tab.js

    先看bootstrap-tab.js的结构 var Tab = function ( element ) {} //构造器 Tab.prototype ={} //构造器的原型 $.fn.tab = ...

  10. 精品素材:WALK & RIDE 单页网站模板下载

    今天,很高兴能向大家分享一个响应式的,简约风格的 HTML5 单页网站模板.Walk & Ride 这款单页网站模板是现代风格的网页模板,简洁干净,像素完美,特别适合用于推广移动 APP 应用 ...