zoukankan      html  css  js  c++  java
  • 《深入理解C#》整理5-Lambda表达式和表达式树

    一、 作为委托的Lambda表达式

    Lambda表达式都可以看做是C# 2的匿名方法的一种演变。匿名方法能做的几乎一切事情都可以用Lambda表达式来完成。与匿名方法相似,Lambda表达式有特殊的转换规则:表达式的类型本身并非委托类型,但它可以通过多种方式隐式或显式地转换成一个委托实例。匿名函数这个术语同时涵盖了匿名方法和Lambda表达式——在很多情况下,两者可以使用相同的转换规则。

    1、Func<...>委托类型简介

    Func委托的每个委托签名都获取0~4个参数,其类型是用类型参数来指定的。最后一个类型参数用作每种情况下的返回类型。当你想返回void时,可使用Action<...>系列的委托,其功能相同。.NET 4将Action<...>和Func<...>家族扩展为拥有16个参数。

    2、转换成Lambda表达式

    Lambda表达式最冗长的形式是:(显示类型的参数列表)=>{语句}。=>部分是C# 3新增的,它告诉编译器我们正使用一个Lambda表达式。匿名方法中控制返回语句的规则同样适用于Lambda表达式:不能从Lambda表达式返回void类型;如果有一个非void的返回类型,那么每个代码路径都必须返回一个兼容的值。

    3、用单一表达式作为主体

    大多数时候,都可以用一个表达式来表示整个主体,该表达式的值是Lambda的结果。在这些情况下,可以只指定那个表达式,不使用大括号,不使用return语句,也不添加分号。格式随即变成:(显式类型的参数列表) => 表达式

    4、隐式类型的参数列表

    编译器大多数时候都能猜出参数类型,不需要你显式声明它们。在这些情况下,可以将Lambda表达式写成:(隐式类型的参数列表) => 表达式。隐式类型的参数列表就是一个以逗号分隔的名称列表,没有类型。但隐式和显式类型的参数不能混合匹配——要么整个列表都是显式类型的,要么全部都是隐式类型的。除此之外,如果有out或ref参数,就只能使用显式类型。

    5、单一参数的快捷语法

    如果Lambda表达式只需一个参数,而且那个参数可以隐式指定类型,C# 3就允许省略圆括号。这种格式的Lambda表达式是:参数名 => 表达式。

    转变过程如下图所示:

    image-20201025184905093

    二、使用List和事件的简单例子

    1、列表的过滤、排序和操作

    还记得List的FindAll方法吗,它获取一个Predicate,并返回一个新列表,包含原始列表中与谓词匹配的所有元素。Sort方法获取一个Comparison,并相应地对列表进行排序。最后,ForEach方法获取一个Action,对每个元素执行特定的行为。

    2、事件处理程序中进行记录

    假如我们既想记录事件的本质,又想记录与发送者和实参有关的信息,又该怎么办呢?Lambda表达式允许我们以简洁的方式解决这个问题,如下图,代码清单9-5使用Lambda表达式将事件名称和事件的参数传给记录事件细节的Log方法

    image-20201025190200725

    三、表达式树

    .NET3.5的表达式树提供了一种抽象的方式将一些代码表示成一个对象树。表达式树主要用于LINQ,C#3对于将Lambda表达式转换成表达式树提供了内建的支持

    1、编程方式构建表达式树

    ①表达式树是对象构成的树,树中每个节点本身都是一个表达式。不同的表达式类型代表能在代码中执行的不同操作:二元操作(例如加法),一元操作(例如获取一个数组的长度),方法调用,构造函数调用,等等

    ②System.Linq.Expressions命名空间包含了代表表达式的各个类,它们都继承自Expression,一个抽象的主要包含一些静态工厂方法的类,这些方法用于创建其他表达式类的实例。Expression类也包括两个属性:

    • Type属性代表表达式求值后的.NET类型,可把它视为一个返回类型。例如,如果一个表达式要获取一个字符串的Length属性,该表达式的类型就是int。
    • NodeType属性返回所代表的表达式的种类。它是ExpressionType枚举的成员,包括LessThan、Multiply和Invoke等。仍然使用上面的例子,对于myString.Length这个属性访问来说,其节点类型是MemberAccess

    ③创建一个表达式树来表示2+3,如下:

    image-20201025191857093

    值得注意的是,“叶”表达式在代码中是最先创建的:你自下而上构建了这些表达式。这是由“表达式不易变”这一事实决定的——创建好表达式后,它就永远不会改变。这样就可以随心所欲地缓存和重用表达式

    image-20201025192005935

    2、将表达式树编译成委托

    ①LambdaExpression是从Expression派生的类型之一。泛型类Expression又是从LambdaExpression派生的。下图展示了其类型层次结构,使继承关系更加清晰。

    image-20201025192422457

    ②Expression和Expression类的区别在于,后者以静态类型的方式标识了它是什么种类的表达式,也就是说,它确定了返回类型和参数。假设我们的简单加法表达式就是一个不获取任何参数,并返回整数的委托。与之匹配的签名就是Func,所以可以使用一个Expression <Func>,以静态类型的方式表示该表达式。

    这样做的意义何在呢?LambdaExpression有一个Compile方法能创建恰当类型的委托。Expression也有一个同名的方法,但它静态类型化后返回TDelegate类型的委托。该委托现在可以采用普通方式执行,就好像它是用一个普通方法或者其他方式来创建的一样。因而上面的名为add的表达式树我们可以表示成:Func<int> compiled=Expression.Lambda<Func<int>>(add).Compile();

    3、将C# Lambda表达式转换成表达式树

    ①Lambda表达式能显式或隐式地转换成恰当的委托实例。然而,这并非唯一能进行的转换。还可以要求编译器通过你的Lambda表达式构建一个表达式树,在执行时创建Expression的一个实例。如上述的2+3示例如下:

    Expression<Func<int>> return5=()=>5;
    Func<int> compile;
    Console.WriteLine(compiled());
    

    ②并非所有Lambda表达式都能转换成表达式树。不能将带有一个语句块(即使只有一个return语句)的Lambda转换成表达式树——只有对单个表达式进行求值的Lambda才可以。表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。

    ③考虑写一个获取两个字符串的谓词,并验证第1个字符串是否以第2个字符串开头的方法。用Lambda表达式来表示是非常简单的,表达式树本身则要复杂得多,尤其是等到我们把它转换成LambdaExpression的实例时。为了构造最终的方法调用表达式,我们需要知道方法调用的几个部件,其中包括:方法的目标(也就是调用StartsWith的字符串);方法本身(MethodInfo);参数列表(本例只有一个参数)。将方法调用构造成一个表达式之后,接着需要把它转换成Lambda表达式,并绑定参数。

    image-20201025194825992

    4、位于LINQ核心的表达式树

    ①没有Lambda表达式,表达式树几乎没有任何价值。从一定程度上说,反过来说也是成立的:没有表达式树,Lambda表达式肯定就没那么有用了。当可以用一种更简便的方式创建委托实例,并向更函数化的开发模式进行转移,即在与扩展方法组合使用的时候,Lambda表达式的效率尤其高。而将Lambda表达式、表达式树和扩展方法合并到一起后,我们会得到LINQ

    ②Lambda表达式提供了编译时检查的能力,而表达式树可以将执行模型从你所需的逻辑中提取出来。LINQ提供器的中心思想在于,我们可以从一个熟悉的源语言(如C#)生成一个表达式树,将结果作为一个中间格式,再将其转换成目标平台上的本地语言,比如SQL。如下图:

    image-20201025195555749

    表达式树确保了大量的编译时安全性,但编译器只能检查以确保Lambda表达式能转换成一个有效的表达式树,它不能保证表达式树最后的使用是否合适。

    5、LINQ之外的表达式树

    尽管表达式树主要是为LINQ而引入.NET的,但微软和社区都发现了它的一些其他用法。

    ①优化动态语言运行时

    表达式树是动态语言运行时的核心部分。它们具有三个特点对DLR特别有吸引力:

    • 它们是不易变的,因此可以安全地缓存;
    • 它们是可组合的,因此可以在简单的块中构建出复杂的行为;
    • 它们可以编译为委托,后者可以像平常那样进一步JIT编译为本地代码

    ②可以放心地对成员的引用进行重构

    ③更简单的反射

    四、类型推断和重载决策的改变

    类型推断和重载决策所涉及的步骤在C# 3中发生了变化,以适应Lambda表达式,并使匿名方法变得更有用。这些虽然不算是C#的新特性,但在理解编译器所做的事情方面,这些变化是相当重要的。

    1、改变的起因:精简泛型方法调用

    C# 2的类型推断规则颇为简单,随着Lambda表达式的引入,C# 3中的情况变得更复杂——如果用一个Lambda表达式来调用一个泛型方法,同时传递一个隐式类型的参数列表,编译器就必须先推断出你想要的是什么类型,然后才能检查Lambda表达式的主体。

    C# 2的类型推断规则是单独针对每一个实参来进行的,从一个实参推断出的类型无法直接用于另一个实参。那么在存在多个形参的泛型方法中,编译器又是如何推断出类型的呢?

    2、推断匿名函数的返回类型

    编译器通常会像对非委托类型所做的那样,执行相同的类型推断,也就是根据返回的表达式的类型来推断T的类型。但如果返回值存在多个数据类型的情况下,编译器又会如何处理呢?在这种情况下,编译器采用和处理隐式类型的数组时相同的逻辑来确定返回类型,它构造一个集合,其中包含了来自匿名函数主体中的return语句的所有类型,并检查是否集合中的所有类型都能隐式转换成其中的一个类型。

    我们现在知道了怎样确定匿名函数的返回类型,但是,参数类型可以隐式定义的Lambda表达式又如何呢?

    3、分两个阶段进行的类型推断

    第一个阶段处理的是“普通”的实参,其类型是一开始便知道的。这包括那些参数列表是显式类型的匿名函数。第二个阶段是推断隐式类型的Lambda表达式和方法组的类型。其思想是,根据我们迄今为止拼凑起来的信息,判断是否足够推断出Lambda表达式(或方法组)的参数类型。如果能,编译器就可以检查Lambda表达式的主体并推断返回类型——这个返回类型通常能帮助我们确定当前正在推断的另一个类型参数。如果第二个阶段提供了更多的信息,就重复执行上述过程,直到我们用光了所有线索,或者最终推断出涉及的所有类型参数。如下:

    image-20201025203148977

    Lambda表达式的主体只有在输入参数的类型已知之后才能进行检查。当参数类型是显式声明的时候,这并不是一个问题,但对于一个隐式(类型的)参数列表,编译器就必须等待,直到它执行了相应的类型推断之后,才能尝试去理解Lambda表达式的含义。

    4、选择正确的被重载的方法

    ①如果多个方法的名字相同但签名不同,就会发生重载。有时,具体该用哪个方法是显而易见的,因为只有它的参数数量是正确的,或者只有用它,所有实参才能转换成对应的参数类型。但是,假如多个方法看起来都合适,就比较麻烦了。

    举个例子:void Write(int x)和void Write(double y),在这种情况下使用Write(1)编译器会考虑从int到int的转换,以及从int到double的转换。从任何类型“转换成它本身”被认为好于“转换成一个不同的类型”。这个规则称为“更好的转换”规则。所以对于这种特殊的调用,Write(int x)方法被认为好于Write(double y)

    ②如果方法有多个参数,编译器需要确保存在最适合的方法。如果一个方法所涉及的所有实参转换都至少与其他方法中相应的转换“一样好”,并且至少有一个转换严格优于其他方法,我们就认为这个方法要比其他方法好。

    ③同样的逻辑在C# 3中仍然适用,但额外添加了与匿名方法和Lambda表达式有关的一个规则:如果一个匿名函数能转换成参数列表相同,但返回类型不同的两个委托类型,就根据从“推断的返回类型”到“委托的返回类型”的转换来判定哪个委托转换“更好”。

    5、型推断和重载决策

    总结一下本节的重点:

    • 匿名函数 (匿名方法和Lambda表达式)的返回类型是根据所有return语句的类型来推断的;
    • Lambda表达式要想被编译器理解,所有参数的类型必须为已知;
    • 类型推断不要求根据不同的(方法)实参推断出的类型参数的类型完全一致,只要推断出来的结果是兼容的就好;
    • 类型推断现在分阶段进行,为一个匿名函数推断的返回类型可作为另一个匿名函数的参数类型使用;
    • 涉及匿名函数时,为了找出“最好”的重载方法,要将推断的返回类型考虑在内。
    作者:Jscroop
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Informix IDS 11琐细管理(918测验)认证指南,第8局部:面向管理员的SQL特征(3)
    Informix IDS 11体系办理(918检验)认证指南,第8部门:面向办理员的SQL特征(1)
    DB2 9 根基(730 考试)认证指南,第 2 局部: 安然性(2)
    漫衍式 DB2 9.5 数据效能器对比
    Informix IDS 11零碎筹划(918检验)认证指南,第8部分:面向筹划员的SQL特性(1)
    Informix IDS 11系统打点(918测验)认证指南,第 7 部门: IDS复制(23)
    Informix IDS 11琐细管理(918测验)认证指南,第 7 部门: IDS复制(21)
    Informix IDS 11琐细经管(918考试)认证指南,第8局部:面向经管员的SQL特性(8)
    Informix IDS 11系统治理(918测验)认证指南,第 7 部门: IDS复制(25)
    Informix IDS 11体系管理(918检修)认证指南,第8部门:面向管理员的SQL特性(5)
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13893904.html
Copyright © 2011-2022 走看看