zoukankan      html  css  js  c++  java
  • 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/

  • 相关阅读:
    自己写的jQuery放大镜插件效果(一)(采用一张大图和一张小图片的思路)
    javascript 节点操作拷贝节点cloneNode()
    javascript节点操作移出节点removeChild()
    写的一个封拆包代码
    C#_socket拆包_封包_模拟乱序包
    VS2010使用DX报错 VS报错之混合模式程序集是针对“v1.1.4322”版的运行时生成的,在没有配置其他信息的情况下,无法在 4.0 运行时中加载该程序集。
    C#_C++_SDK_WM_KEYDOWN人物卡顿延迟解决方法
    MYSQL游标的使用
    MYSQL异常和错误机制
    CRM中的一个函数,保存一下,别系统被ぅ崩坏就麻烦了.
  • 原文地址:https://www.cnblogs.com/hackpig/p/5821055.html
Copyright © 2011-2022 走看看