LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树

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

 

(一) 序列

先上一段代码,

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

  • 取进程列表,进行过滤(取大于10M的进程)
  • 列表进行排序(按内存占用)
  • 只保留列表中指定的信息(ID,进程名)
1             var res = Process.GetProcesses()
2                 .Where(s => s.WorkingSet64 > 10 * 1024 * 1024)
3                 .OrderByDescending(s => s.WorkingSet64)
4                 .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扩展方法的实现代码:

 1         public static IEnumerable<TSource> Where<TSource>(
 2             this IEnumerable<TSource> source,
 3             Func<TSource, Boolean> predicate)
 4         {
 5             foreach (TSource element in source)
 6             {
 7                 if (predicate(element))
 8                     yield return element;
 9             }
10         }

其第一参数中的this关键字就证明了它是一个扩展方法,参数类型就是IEnumerable<T>.

关键字yield return 就构成了一个迭代器.

我们来看一下迭代器的背景知识.

 

2. 迭代器

 从结果的角度看,迭代器与一个返回集合数据的传统方法没有什么区别,因为都是返回按一序列排列的值.

比如下面的代码,就返回一个集合的值.

1         int[] OneTwoThree()
2         {
3             return new[] { 1, 2, 3 };
4         }

不过,C#中的迭代器的行为却非常特殊.迭代器将不会一次性返回整个集合中的所有值.而是每次返回一个.这样的设计减少了内存需求.

我们构建一个迭代器的例子,看一看这个特性.

 1   private void button2_Click(object sender, EventArgs e)
 2         {
 3             foreach (var m in OneTwoThree())
 4             {
 5                 Console.WriteLine(m);
 6             }
 7         }
 8         static IEnumerable<int> OneTwoThree()
 9         {
10             Console.WriteLine("returning 1");
11             yield return 1;
12             Console.WriteLine("returning 2");
13             yield return 2;
14             Console.WriteLine("returning 3");
15             yield return 3;
16         }

 运行结果如下图

 

可以看到,函数OneTwoThree直到执行完最后一条语句之后才完整退出.

每次遇到yield return语句时,该方法都向调用者返回一个值.

foreach循环收到这个值后进行了处理,然后控制权又交回给迭代器方法OneTwoThree方法,由它给出下一个元素.

看起来好像两个方法在同时运行.这也正是可以将.NET中的迭代器当作是一类轻量级的协同程序(coroutine)的原因.

 

(二) 延迟查询执行

LINQ查询语句非常依赖于延迟查询执行机制,惹是缺少了这个机制,LINQ的执行效率将会大大降低.

来看一段代码:

 1  static double Square(double n)
 2         {
 3             Console.WriteLine("计算 Square(" + n + ")...");
 4             return Math.Pow(n, 2);
 5         }
 6         private void button3_Click(object sender, EventArgs e)
 7         {
 8             int[] numbers = { 1, 2, 3 };
 9             var res = from n in numbers
10                       select Square(n);
11             foreach (var m in res)
12                 Console.WriteLine(m);
13         }

运行结果如下:

结果可以看到,明显该查询并不是一次性执行完毕的.只有在迭代到某一项时,查询才开始求出这一项的值.

这就是所谓的查询延迟执行的机制在发挥作用.

我们来讨论一下其中的原理:

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方法来实现.

我们把上面的代码改动一下:

1   private void button4_Click(object sender, EventArgs e)
2         {
3             int[] numbers = { 1, 2, 3 };
4             var res = from n in numbers
5                       select Square(n);
6             foreach (var m in res.ToList())
7                 Console.WriteLine(m);
8         }

可以看到结果就不同了:

可以见到是先得到查询的结果,最后才把结果迭代输出的.

 

(三) 查询操作符

上面代码所示的 Where,OrderByDescending, Select这些扩展方法 包含有共同的特性:

  • 操作于可被迭代的集合对象之上
  • 允许管道形式的数据处理
  • 依赖于延迟执行

正是上面这些特征让这些扩展方法能用于编写查询.因此这些扩展方法也称为"查询操作符"

查询操作符是LINQ的核心,甚至比语言方面的特性(比如查询表达式)更重要.

 下图是按照操作类型分组的标准查询操作符:

 

(四) 查询表达式

开往篇的程序是使用查询操作符实现的.再次引用一下:

1   var res = Process.GetProcesses()
2                 .Where(s => s.WorkingSet64 > 10 * 1024 * 1024)
3                 .OrderByDescending(s => s.WorkingSet64)
4                 .Select(s => new { ID = s.Id, Name = s.ProcessName });

另一种语法则让LINQ查询更像是SQL的查询语句.

1   var res = from s in Process.GetProcesses()
2                       where s.WorkingSet64 > 10 * 1024 * 1024
3                       orderby s.WorkingSet64 descending
4                       select new { ID = s.Id, Name = s.ProcessName };

上面的这种写法就叫做查询表达式,或者查询式语法.

这两种代码的写法从语义上来讲是完全相同的,而且实现的功能也一致.

查询表达式是由C#语言提供的语言级特性,一种语法糖,这种语法类似于SQL,它可以操作于一个或者多个数据源之上,并为这些数据源应用若干个标准或者自定义的查询操作符.在上面的示例代码中,使用了3个标准的查询操作符:Where, orderByDescending以及Select.

在使用查询表达式语法时,编译器会自动将其转化为对标准查询操作符的调用.

查询表达式存在的最主要意义在于,它能够大大简化查询所需要的代码,并提高查询语句的可读性(类似熟悉的SQL).

 

下图是查询表达式的完整语法:

标准查询操作符与查询表达式的关系,见下表所示:

通过上表可以看到,不是每一个操作符都有与之对应的C#关键字.在前面那个简单的查询中,我们当然完全可以使用语言所提供的关键字实现.不过对于那些较为复杂的查询来说,我们将不得不直接调用查询操作符完成.

因为查询表达式最终都会被编译成各个标准操作符的调用.因此如果愿意的话,完全可以只用查询操作符编写所有查询语句,根本不理会查询表达式的存在.

 

(五) 表达式树

 Lambda表达式在前面提到过它的主要作用之一是实现匿名委托.如下例:

Func<int,bool> isOdd=i=>(i & 1)==1;

但是,Lambda表达式也能够以数据的形式使用,这正是表达式树所要求的.

当把代码改成下面这样时,我们就无法以委托的形式来使用isOdd了.因为在这里isOdd并不是委托,而是个表达式树.

Expression<Func<int,bool>> isOdd =i => (i & 1) ==1;

编译器不会把上面的Lambda表达式换成IL代码,而是会构造出一个树状的,用来表示该表达式的对象.

但是需要注意的是:只有那些带有表达式体的Lambda表达式才能够用于表达式树.主体部分是语句的Lambda表达式则没有类似的支持.

例如,下面第1行代码可以用来生成一颗表达式树,因为其带有表达式体.

第2行的就不能,因为它的主体部分是一个语句.

1 Expression<Func<Object, Object>> identity = o=>o;
2 Expression<Func<Object, Object>> identity = o=>{ return o;};

当编译器看到某个Lambda表达式赋值给类型为Expression<>的变量时,就会将其编译成一系列工厂方法的调用,这些工厂方法将在程序运行时动态地构造出表达式树.

下面就是编译器为上述表达式自动生成的代码:

1   ParameterExpression i = Expression.Parameter(typeof(int), "i");
2             Expression<Func<int, bool>> isOdd =
3                 Expression.Lambda<Func<int, bool>>(
4                 Expression.Equal(
5                 Expression.And(
6                 i,
7                 Expression.Constant(1, typeof(int))),
8                 Expression.Constant(1, typeof(int))),
9                 new ParameterExpression[] { i });

上面的代码是可以手工编写的,但是编译器可以代劳.

表达式树将在程序运行中动态构造,不过一旦构造完成,则无法被再次修改.

表达式树在第5章中用以创建动态查询这种高级场景上得到了应用.

上面的表达式树,在内存中以树的数据结构存储,它表示解析了后的Lambda表达式,如下图:

上面的表达式树,还可以"逆向"编译成委托方法:

1    Expression<Func<int, bool>> isOddExpression = i => (i & 1) == 1;
2             Func<int, bool> isOddCompiledExpression = isOddExpression.Compile();

这时候,上面的isOddCompiledExpression和下面的委托isOdd就完全相同了,它们生成的IL代码就没有任何区别了.

Func<int,bool> isOdd=i=>(i & 1)==1;

为什么要使用表达式树呢?

实际上,表达式树就是一颗抽象语法树(AST).抽象语法树用来表示一段经过解析的代码.在上面例子中,这颗树就是C#对于Lambda表达式解析后的结果.这样做的目的是便于其它代码对该表达式树进行分析,并执行一些必要的操作.

表达式树可以在运行时传递给其它的工具,随后这些工具可以根据该树开始执行查询,或者是将其转化为其它形式的代码,例如LINQ to SQL中的SQL语句.

 

最后我们来看看表达式树执行延迟查询执行的方法:

引用之前LINQ to SQL例子中的代码:

1  var contacts =
2               from contact in db.GetTable<HelloLinqToSql.Contact>()
3               where contact.City == "武汉"
4               select contact;
5 
6             Console.WriteLine("查找在武汉的联系人"+Environment.NewLine);
7             foreach (var contact in contacts)
8                 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/

 

posted @ 2016-08-30 14:40  猪悟能  阅读(1847)  评论(2编辑  收藏  举报