深入理解C#(第3版)-- 【C#3】第9章 Lambda 表达式和表达式树(学习笔记)

9.1  作为委托的Lambda 表达式

与匿名方法相似,Lambda表达式有特殊的转换规则:表达式的类型本身并非委托类型,但它可以通过多种方式隐式或显式地转换成一个委托实例。

9.1.1  准备工作:Func<...>委托类型简介

9.1.2  第一次转换成Lambda 表达式

代码清单9-1  用匿名方法来创建委托实例

Func<string,int> returnLength;
returnLength = delegate (string text) { return text.Length; };
Console.WriteLine(returnLength("Hello"));

Lambda表达式最冗长的形式是:
(显式类型的参数列表) => { 语句}

代码清单9-2  冗长的第一个 Lambda表达式,和匿名方法相似

Func<string,int> returnLength;
returnLength = (string text) => { return text.Length; };
Console.WriteLine(returnLength("Hello"));

匿名方法中控制返回语句的规则同样适用于Lambda表达式:不能从Lambda表达式返回void类型;如果有一个非void的返回类型,那么每个代码路径都必须返回一个兼容的值。

9.1.3  用单一表达式作为主体

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

(显式类型的参数列表) => 表达式

在我们的例子中,这意味着Lambda表达式变成了:

(string text) => text.Length

9.1.4  隐式类型的参数列表

编译器大多数时候都能猜出参数类型,不需要你显式声明它们。在这些情况下,可以将Lambda表达式写成:

(隐式类型的参数列表) => 表达式

隐式类型的参数列表就是一个以逗号分隔的名称列表,没有类型。但隐式和显式类型的参数不能混合匹配——要么整个列表都是显式类型的,要么全部都是隐式类型的。除此之外,如果有out 或ref 参数,就只能使用显式类型。

我们的Lambda表达式可以继续简化成:

(text) => text.Length

9.1.5  单一参数的快捷语法

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

参数名 => 表达式

因此,我们的Lambda表达式的最终形式是: 

text => text.Length

9.2 使用List<T>和事件的简单例子

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

9.2.2  在事件处理程序中进行记录

代码清单9-5 使用Lambda表达式来记录事件

static void Log(string title, object sender, EventArgs e)
{
    Console.WriteLine("Event: {0}", title);
    Console.WriteLine(" Sender: {0}", sender);
    Console.WriteLine(" Arguments: {0}", e.GetType());
    foreach (PropertyDescriptor prop in
             TypeDescriptor.GetProperties(e))
    {
        string name = prop.DisplayName;
        object value = prop.GetValue(e);
        Console.WriteLine(" {0}={1}", name, value);
    }
}

...
Button button = new Button { Text = "Click me" };
button.Click += (src, e) => Log("Click", src, e);
button.KeyPress += (src, e) => Log("KeyPress", src, e);
button.MouseClick += (src, e) => Log("MouseClick", src, e);
Form form = new Form { AutoSize = true, Controls = { button } };
Application.Run(form);

9.3  表达式树

.NET 3.5 的表达式树提供了一种抽象的方式将一些代码表示成一个对象树。类似于CodeDOM

9.3.1  以编程方式构建表达式树

System.Linq.Expressions 命名空间包含了代表表达式的各个类,它们都继承自Expression,Expression类也包括两个属性。

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

代码清单9-6  一个非常简单的表达式树, 2和3相加

Expression firstArg = Expression.Constant(2);
Expression secondArg = Expression.Constant(3);
Expression add = Expression.Add(firstArg, secondArg);
Console.WriteLine(add);

值得注意的是,“叶”表达式在代码中是最先创建的:你自下而上构建了这些表达式。这是由“表达式不易变”这一事实决定的——创建好表达式后,它就永远不会改变。

9.3.2  将表达式树编译成委托

LambdaExpression 是从Expression派生的类型之一。泛型类Expression<TDelegate>又是从LambdaExpression 派生的。

 Expression和Expression<TDelegate> 类的区别在于,泛型类以静态类型的方式标识了它是什么种类的表达式,也就是说,它确定了返回类型和参数。很明显,这是用TDelegate类型参数来表示的,它必须是一个委托类型。

例如,假设我们的简单加法表达式就是一个不获取任何参数,并返回整数的委托。与之匹配的签名就是Func<int>,所以可以使用一个Expression <Func<int>> ,以静态类型的方式表示该表达式。

LambdaExpression 有一个Compile方法能创建恰当类型的委托。

代码清单9-7  编译并执行一个表达式树

Expression firstArg = Expression.Constant(2);
Expression secondArg = Expression.Constant(3);
Expression add = Expression.Add(firstArg, secondArg);
Func<int> compiled = Expression.Lambda<Func<int>>(add).Compile();
Console.WriteLine(compiled());

9.3.3 将C# Lambda表达式转换成表达式树

代码清单9-9  演示一个更复杂的表达式树

Expression<Func<string, string, bool>> expression =(x, y) => x.StartsWith(y);
var compiled = expression.Compile();
Console.WriteLine(compiled("First", "Second"));
Console.WriteLine(compiled("First", "Fir"));

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

9.3.4 位于LINQ核心的表达式树

没有Lambda表达式,表达式树几乎没有任何价值。

从一定程度上说,反过来说也是成立的:没有表达式树,Lambda表达式肯定就没那么有用了。

将Lambda表达式、表达式树和扩展方法合并到一起,会得到什么?答案是LINQ在C#语言这一层面的全部体现。

LINQ提供器的中心思想在于,我们可以从一个熟悉的源语言(如C#)生成一个表达式树,将结果作为一个中间格式,再将其转换成目标平台上的本地语言,比如SQL 。

9.3.5  LINQ之外的表达式树

1. 优化动态语言运行时

表达式树是C#动态类型架构的核心部分。它们具有三个特点对DLR 特别有吸引力:

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

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

infoof

3. 更简单的反射

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

类型推断和重载决策所涉及的步骤在C# 3中发生了变化,以适应Lambda表达式,并使匿名方法变得更有用。

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

代码清单9-11   需要新的类型推断规则的例子

static void PrintConvertedValue<TInput, TOutput>
(TInput input, Converter<TInput, TOutput> converter)
{
    Console.WriteLine(converter(input));
}

...
PrintConvertedValue("I'm a string", x => x.Length);

9.4.2  推断匿名函数的返回类型

代码清单9-12   尝试推断匿名方法的返回类型

delegate T MyFunc<T>(); //声明了.NET2.0中没有的Func<T>
static void WriteResult<T>(MyFunc<T> function) //声明带有委托参数的泛型方法
{
    Console.WriteLine(function());
}

...
WriteResult(delegate { return 5; }); //要求对T进行类型推断 

代码清单9-12 在C# 2 中编译会报错:

error CS0411: The type arguments for method
'Snippet.WriteResult<T>(Snippet.MyFunc<T>)' cannot be inferred from the
usage. Try specifying the type arguments explicitly.

可以采取两种方式修正这个错误:要么显式指定类型实参(就像编译器推荐的那样),要么将匿名方法强制转换为一个具体的委托类型:

WriteResult<int>(delegate { return 5; });
WriteResult((MyFunc<int>)delegate { return 5; });

代码清单9-13   根据一天当中的时间来选择返回 int 或object

delegate T MyFunc<T>();
static void WriteResult<T>(MyFunc<T> function)
{
    Console.WriteLine(function());
}

...
WriteResult(delegate
{
    if (DateTime.Now.Hour < 12)
    {
        return 10;
    }
    else
    {
        return new object();
    }
});

在这种情况下,编译器采用和处理隐式类型的数组时相同的逻辑来确定返回类型,详情可参见8.4节。它构造一个集合,其中包含了来自匿名函数主体中的return 语句的所有类型(本例是int 和object),并检查是否集合中的所有类型都能隐式转换成其中的一个类型。int 到object存在一个隐式转换(通过装箱),但object到int 就不存在了。所以,object被推断为返回类型。

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

固定类型变量(fixed)是指编译器能确定其值的变量,否则就是非固定类型变量。

代码清单9-11 需要推断的类型参数是TInput和TOutput。具体步骤如下。
(1) 阶段1开始。

(2) 第1个参数是TInput类型,第1个实参是string类型。我们推断出肯定存在从string到TInput 的隐式转换。
(3) 第2个参数是Converter<TInput,TOutput>类型,第2个实参是一个隐式类型的Lambda表达式。此时不执行任何推断,因为我们没有掌握足够的信息。
(4) 阶段2开始。
(5) TInput不依赖任何非固定的类型参数,所以它被确定为string。
(6) 第2 个实参现在有一个固定的输入类型,但有一个非固定的输出类型。我们可把它视为(string x) => x.Length ,并推断出其返回类型是int 。因此,从int 到TOutput必定会发生一个隐式转换。
(7) 重复“阶段2”。
(8) TOutput不依赖任何非固定的类型参数,所以它被确定为int 。
(9) 现在没有非固定的类型参数了,推断成功。

9.4.4  选择正确的被重载的方法

假定有以下方法签名,似乎它们是在同一个类型中声明的:

void Write(int x)
void Write(double y)

Write(1.5)的含义显而易见,因为不存在从double 到int 的隐式转换,但Write(1) 对应的调用就麻烦一些。由于存在从int 到double的隐式转换,所以以上两个方法似乎都合适。在这种情况下,编译器会考虑从int 到int 的转换,以及从int 到double的转换。

从任何类型“转换成它本身”被认为好于“转换成一个不同的类型”。这个规则称为“更好的转换”规则。

现给出一个简单的例子,假定现在有以下两个方法签名: 

void Write(int x, double y)
void Write(double x, int y)

对Write(1, 1) 的调用会产生歧义,编译器会强迫你至少为其中的一个参数添加强制类型转换,以明确你想调用的是哪个方法。每个重载都有一个更好的实参转换,因此都不是最好的。

代码清单9-16   委托返回类型影响了重载选择

static void Execute(Func<int> action)
{
    Console.WriteLine("action returns an int: " + action());
}

static void Execute(Func<double> action)
{
    Console.WriteLine("action returns a double: " + action());
}

...
Execute(() => 1);

如果一个匿名函数能转换成参数列表相同,但返回类型不同的两个委托类型,就根据从“推断的返回类型”到“委托的返回类型”的转换来判定哪个委托转换“更好”。

代码清单9-16是从一个无参数、推断返回类型为int 的Lambda表达式转换成Func<int>或Func<double> 。两个委托类型的参数列表是相同的(空),所以上述规则是适用的。然后,我们只需判断哪个转换“更好”就可以了:int 到int ,还是int 到double。

9.4.5  类型推断和重载决策

现在总结一下本节的重点:
 匿名函数(匿名方法和Lambda表达式)的返回类型是根据所有return语句的类型来推断的;
 Lambda表达式要想被编译器理解,所有参数的类型必须为已知;
 类型推断不要求根据不同的(方法)实参推断出的类型参数的类型完全一致,只要推断出来的结果是兼容的就好;
 类型推断现在分阶段进行,为一个匿名函数推断的返回类型可作为另一个匿名函数的参数类型使用;
 涉及匿名函数时,为了找出“最好”的重载方法,要将推断的返回类型考虑在内。

posted @ 2019-10-16 10:09  FH1004322  阅读(280)  评论(0)    收藏  举报