你的位置:首页 > ASP.net教程

[ASP.net教程]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 numbers10            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 numbers5            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 * 10243            orderby s.WorkingSet64 descending4            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>和表达式树的组合将给我们带来更强大的可定制能力.