LINQ 的基本功能就是创建操作管道,以及这些操作需要的任何状态。 这些操作表示了各种关于数据的逻辑:如何过滤,如何排序以及如何将不同的数据源连接在一起。
9.1 作为委托的Lambda表达式
- 从许多方面,Lambda表达式都可以看作是C#2.0的匿名方法的一种演变。匿名方法能做的几乎一切事情都可以用Lambda表达式来完成。
- 表达式的类型本身并非委托类型,但它可以通过多种方式隐式或显示地转换成一个委托实例。
9.1.1 准备工作: Func<...>委托类型简介
如果我们需要获取一个string参数,并返回一个int, 所以我们将使用 Func<string, int>
9.1.2 第一次转换成Lambda表达式
用匿名方法来创建委托
class SimpleAnonymousMethod { static void Main() { FuncreturnLength; returnLength = delegate(string text) { return text.Length; }; Console.WriteLine(returnLength("Hello")); } }
冗长的第一个Lambda表达式,和匿名方法相似 (可以将=>读成 goes to )
class FirstLambdaExpression { static void Main() { FuncreturnLength; returnLength = (string text) => { return text.Length; }; Console.WriteLine(returnLength("Hello")); } }
9.1.3 用单一表达式作为主体
(string text) => { return text.Length; };可以转变为 (string text) => text.Length;
9.1.4 隐式类型的参数列表
编译器大多数时候都能猜出参数类型,不需要你显示声明它们。在这些情况下,可以将Lambda表达式写成:
(隐式类型的参数列表)=>表达式
(string text) => text.Length; 可以转换为(text) =>text.Length;
9.1.5 单一参数的快捷与法
最终可以把那个小括号也去掉
class FirstLambdaExpression { static void Main() { FuncreturnLength; returnLength = (string text) => { return text.Length; }; Console.WriteLine(returnLength("Hello")); } }
9.2 使用List<T>和事件的简单例子
9.2.1 列表的过滤,排序和操作
class FilmFilteringAndSorting { class Film { public string Name { get; set; } public int Year { get; set; } } static void Main() { var films = new List{ new Film {Name="Jaws", Year=1975}, new Film {Name="Singing in the Rain", Year=1952}, new Film {Name="Some like it Hot", Year=1959}, new Film {Name="The Wizard of Oz", Year=1939}, new Film {Name="It's a Wonderful Life", Year=1946}, new Film {Name="American Beauty", Year=1999}, new Film {Name="High Fidelity", Year=2000}, new Film {Name="The Usual Suspects", Year=1995} }; Action print = film => Console.WriteLine("Name={0}, Year={1}", film.Name, film.Year); // Note: extra lines added for clarity when running Console.WriteLine("All films"); films.ForEach(print); Console.WriteLine(); Console.WriteLine("Oldies"); films.FindAll(film => film.Year < 1960) .ForEach(print); Console.WriteLine(); Console.WriteLine("Sorted"); films.Sort((f1, f2) => f1.Name.CompareTo(f2.Name)); films.ForEach(print); } }
9.2.2 在事件处理程序中进行记录
class EventLogging { 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); } } static void Main() { Button button = new Button(); 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(); form.AutoSize = true; form.Controls.Add(button); Application.Run(form); } }
9.3 表达式树
9.3.1 以编程方式构建表达式树
- 表达式树是对象构成的树,数中每个节点本身都是一个表达式。不同的表达式类型代表能在代码中执行的不同操作:二元操作(例如加法),一元操作(例如获取一个数组的长度),方法调用,构造函数调用,等等。
- System.Linq.Expressions命名空间包含了代表表达式的各个类,它们都继承自Expression,一个抽象的主要包含一些静态工厂方法的类,这些方法用于创建其他表达式的实例。然而,Expression类也包括两个属性。
- Type属性代表表达式求值后的.Net类型,可把它视为一个返回类型。例如,如果一个表达式要获取一个字符串的Length属性,该表达式的类型就是int。
- NodeType属性返回所代表的表达式的种类。它是ExpressionType枚举的成员,包括LessThan, Multipy和Invoke等。 仍然使用上面的例子,对于mystring.Length这个属性访问来说,其节点类型是MemberAccess。
class FirstExpressionTree { static void Main() { Expression firstArg = Expression.Constant(2); Expression secondArg = Expression.Constant(3); Expression add = Expression.Add(firstArg, secondArg); Console.WriteLine(add); } }
add BinaryExpression NodeType =Add Type= System.Int32 | ||
firsttArg ConstantExpression NodeType=Constant Type=System.Int32 Value=2 | secondArg ConstantExpression NodeType=Constant Type=System.Int32 Value=3 |
9.3.2 将表达式树编译成委托
(继承关系)
Expression | ||
LambdaExpression | BinaryExpression | 其他类型(ConstantExpression) |
Expression<TDelegate> |
- Expression 和 Expression<TDelegate>类的区别在于,泛型类以静态类型的方式标示了它是什么种类的表达式,也就是说,它确定了返回类型的参数。很明显TDelegate必须是一个委托
- 那么,这样做的意义何在呢?LambdaExpression有一个Compile方法能创建恰当类型的委托。Expression<TDelegate>也有一个同名的方法,但它静态类型化后返回TDelegate类型的委托。该委托现在可以采用普通方式执行,就好像它是用一个普通方法或者其他方式来创建的一样。
class CompiledExpressionTree { static void Main() { Expression firstArg = Expression.Constant(2); Expression secondArg = Expression.Constant(3); Expression add = Expression.Add(firstArg, secondArg); Func compiled = Expression.Lambda>(add).Compile(); Console.WriteLine(compiled()); } }
9.3.3 将C# Lambda表达式转换成表达式树
class LambdaExpressionToExpressionTree { static void Main() { Expression> return5 = () => 5;//编译器可以自动将lambda表达式转换成表达式树。 Func compiled = return5.Compile(); Console.WriteLine(compiled()); } }
- 有一些限制:
- 不能将带有一个语句块(即使只有一个return语句)的lambda表达式转换成表达式树
表达式中还不能包含赋值操作,因为在表达式树中表示不了这种操作。
class LambdaExpressionWithParametersToExpressionTree { static void Main() { Expression> expression = (x, y) => x.StartsWith(y); var compiled = expression.Compile(); Console.WriteLine(compiled("First", "Second")); Console.WriteLine(compiled("First", "Fir")); } }
用代码来构造一个方法调用表达式树
class MethodCallExpressionTree { static void Main() { MethodInfo method = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); var target = Expression.Parameter(typeof(string), "x"); var methodArg = Expression.Parameter(typeof(string), "y"); Expression[] methodArgs = new[] { methodArg }; //1.以上为构造方法调用的各个部件 Expression call = Expression.Call(target, method, methodArgs); //2.从以上部件创建CallExpression var lambdaParameters = new[] { target, methodArg }; var lambda = Expression.Lambda>(call, lambdaParameters); var compiled = lambda.Compile(); //3.以上将Call转换成Lambda表达式 Console.WriteLine(compiled("First", "Second")); Console.WriteLine(compiled("First", "Fir")); } }
- 为了构造最终的方法调用表达式,我们需要知道方法调用的几个部件(1.) 其中包括: 方法的目标(也就是调用StartsWith的字符串);方法本身(MethodInfo);参数列表(本例只有一个参数)。
- 将方法调用构造成一个表达式之后(2.),接着需要把它转换成Lambda表达式(3.),并绑定参数。
P.217 图9-4
9.3.4 位于LINQ核心的表达式树
- Lambda表达式提供了编译时检查的能力,而表达式树可以将执行模型从你所需的逻辑中提取出来。
- LINQ提供器的中心思想在于,我们可以从一个熟悉的源语言(如C#)生成一个表达式树,将结果作为一个中间格式,再将其转换成目标平台上的本地语言。比如SQL。
编译时 | 含有Lambada表达式的C#查询代码 | 含有Lambada表达式的C#查询代码 |
(C#编译器) | (C#编译器) | |
使用委托的IL | 使用表达式树的IL | |
执行时 | 委托代码直接在CLR中执行 | (LINQ to SQL 提供器) |
动态SQL | ||
(在数据库处执行,并取回结果) | ||
查询结果 | 查询结果 | |
LINQ to Objects | LINQ to SQL |
无论LINQ to Objects还是LINQ to SQL都是始于C#代码,结束于查询结果。表达式树提供了远程执行代码的能力。
- 编译器并不能做所有的检查 :例如:索然可以将string.StartWith调用转换成类似的SQL表达式,但string.IsInterned的调用在数据库环境中是没有意义的。表达式树确保了大量编译时安全性,但编译器只能检查以确保Lambda表达式能转换成一个有效的表达式树,它不能保证表达式树最后的使用是否合适。
9.3.5 LINQ之外的表达式树
- 优化动态语言运行时
- 可以放心地对成员的引用进行重构
- 更简单的反射
9.4 类型推断和重载决策的改变
9.4.1 改变的起因
需要新的类型推断规则的例子:
class ConversionWithTypeInference { static void PrintConvertedValue(TInput input, Converter converter) { Console.WriteLine(converter(input)); } static void Main() { PrintConvertedValue("I'm a string", x => x.Length); } }
- 第一个实参明显是个字符串,但第二个呢?是个Lambda表达式,所以需要把它转换成一个Converter<TInput, TOutput>,而那意味着要知道TInput和TOutput的类型。所以如果还是沿用C#2.0的规则,代码清单9-11的代码就会编译失败。
9.4.2 推断匿名函数的返回类型
以下代码清单展示了貌似能编译,但不符合C#2类型推断规则的示例代码 (其实是可以编译的???)
class ReturnTypeInference { delegate T MyFunc(); static void WriteResult (MyFunc function) { Console.WriteLine(function()); } static void Main() { WriteResult(delegate { return 5; }); } }
代码清单9-13 根据一天当中的时间来选择返回int或object
class ReturnTypeInferenceWithMultipleReturns { delegate T MyFunc(); static void WriteResult (MyFunc function) { Console.WriteLine(function()); } static void Main() { WriteResult(delegate { if (DateTime.Now.Hour < 12) { return 10; } else { return new object(); } }); } }
- 在这种情况下,编译器采用和处理隐式类型的数组时相同的逻辑来确定返回类型。int到object存在一个隐式转换,但object到int就不存在了。所以object被推断为返回类型。
9.4.3 分两个阶段进行的类型推断
综合来自多个实参的信息,灵活地进行推断
class MultipleArgumentInference { static void PrintType(T first, T second) { Console.WriteLine(typeof(T)); } static void Main() { PrintType(1, new object()); } }
- 和上一个例子一样,这里T会被推断为Object
- 类型推断现在是分两个阶段进行的。
- 第一个阶段处理的是“普通“的实参,其类型是一开始便知道的。
- 第二个阶段是推断隐式类型的Lambda表达式和方法组的类型,其思想是根据我们迄今为止拼凑起来的信息,判断是否足够推断出Lambda表达式(或方法组)的参数类型。
图9-7用流程图展示了这一过程。(P.225)
class MultiStageInference { static void ConvertTwice(TInput input, Converter firstConversion, Converter secondConversion) { TMiddle middle = firstConversion(input); TOutput output = secondConversion(middle); Console.WriteLine(output); } static void Main() { ConvertTwice("Another string", text => text.Length, length => Math.Sqrt(length)); } }
9.4.4 选择正确的被重载的方法。
- void Write(int x) void Write(double y) 如果 Write(1) 会调用 Write(int x) 这个规则叫”更好的转换“规则
class OverloadingByDelegateReturnType { static void Execute(Func action) { Console.WriteLine("action returns an int: " + action()); } static void Execute(Funcaction) { Console.WriteLine("action returns a double: " + action()); } static void Main() { Execute(() => 1); //会调用第一个Execute函数 } }
9.4.5 类型推断和重载决策
9.5 小结
- 我们知道Lambda表达式并非仅仅是创建委托的一种更精简的语法。它们能转换成表达式树,然后可由其他代码处理,从而在不同的环境中执行等价的行为。如果没有这项功能,LINQ就仅限于进程内查询。