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/