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

(一) 序列

先上一段代码,

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

  • 取进程列表,进行过滤(取大于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/

最新文章

  1. 【原创】新手入门一篇就够:从零开发移动端IM
  2. [转] Linux下 config/configure/Configure、make 、make test/make check、sudo make install 的作用
  3. Ubuntu下安装Django
  4. bzoj 2435: [Noi2011]道路修建 树上 dp
  5. centos Minicom通信终端
  6. 学习RAC小记-适合给新手看的RAC用法总结(转)
  7. jQuery笔记(1)
  8. node-koa搭建MVC/RESTful API项目
  9. SLAM+语音机器人DIY系列:(三)感知与大脑——1.ydlidar-x4激光雷达
  10. Java通过JDBC连接数据库的三种方式!!!并对数据库实现增删改查
  11. MyBatis(五)select返回list数据
  12. [Swift]LeetCode421. 数组中两个数的最大异或值 | Maximum XOR of Two Numbers in an Array
  13. PROJ.4学习——坐标系转换
  14. HttpWebResponse Post 前端控件数据,后台如何接收?
  15. django ORM的总结
  16. delphi “div”、“mod”、“\”除法运算符的区别与使用方法(附带FORMAT使用方法)
  17. Python中的yield生成器的简单介绍
  18. mysql 查询所有子节点的相关数据
  19. SVG.js Marker标记和自定义标签
  20. NLP总览

热门文章

  1. C# 实现字符串去重
  2. MAC自带的SVN进行升级
  3. php 连接redis,并登录验证
  4. U盘FAT32转换NTFS格式
  5. 关于castle和Could not find the dialect in the configuration错误
  6. 配置svn
  7. jQuery---EasyUI小案列
  8. cron语法
  9. zk框架销毁Page上的Component
  10. ruby -- 进阶学习(十二)fragment cache