来源:匠心十年
链接:http://www.cnblogs.com/gaochundong/p/way_to_lambda.html
Lambda 表达式
早在 C# 1.0 时,C#中就引入了委托(delegate)类型的概念。通过使用这个类型,我们可以将函数作为参数进行传递。在某种意义上,委托可理解为一种托管的强类型的函数指针。
通常情况下,使用委托来传递函数需要一定的步骤:
定义一个委托,包含指定的参数类型和返回值类型。
在需要接收函数参数的方法中,使用该委托类型定义方法的参数签名。
为指定的被传递的函数创建一个委托实例。
可能这听起来有些复杂,不过本质上说确实是这样。上面的第 3 步通常不是必须的,C# 编译器能够完成这个步骤,但步骤 1 和 2 仍然是必须的。
幸运的是,在 C# 2.0 中引入了泛型。现在我们能够编写泛型类、泛型方法和最重要的:泛型委托。尽管如此,直到 .NET 3.5,微软才意识到实际上仅通过两种泛型委托就可以满足 99% 的需求:
Action 委托返回 void 类型,Func 委托返回指定类型的值。通过使用这两种委托,在绝大多数情况下,上述的步骤 1 可以省略了。但是步骤 2 仍然是必需的,但仅是需要使用 Action 和 Func。
那么,如果我只是想执行一些代码该怎么办?在 C# 2.0 中提供了一种方式,创建匿名函数。但可惜的是,这种语法并没有流行起来。下面是一个简单的匿名函数的示例:
Funcdouble, double> square = delegate(double x) { return x * x; };
为了改进这些语法,在 .NET 3.5 框架和 C# 3.0 中引入了Lambda 表达式。
首先我们先了解下 Lambda 表达式名字的由来。实际上这个名字来自微积分数学中的 λ,其涵义是声明为了表达一个函数具体需要什么。更确切的说,它描述了一个数学逻辑系统,通过变量结合和替换来表达计算。所以,基本上我们有 0-n 个输入参数和一个返回值。而在编程语言中,我们也提供了无返回值的 void 支持。
让我们来看一些 Lambda 表达式的示例:
// The compiler cannot resolve this, which makes the usage of var impossible! // Therefore we need to specify the type. Action dummyLambda = () => { Console.WriteLine('Hello World from a Lambda expression!'); }; // Can be used as with double y = square(25); Funcdouble, double> square = x => x * x;
// Can be used as with double z = product(9, 5); Funcdouble, double, double> product = (x, y) => x * y;
// Can be used as with printProduct(9, 5); Actiondouble, double> printProduct = (x, y) => { Console.WriteLine(x * y); };
// Can be used as with // var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 }); Funcdouble[], double[], double> dotProduct = (x, y) => { var dim = Math.Min(x.Length, y.Length); var sum = 0.0; for (var i = 0; i != dim; i++) sum += x[i] + y[i]; return sum; };
// Can be used as with var result = matrixVectorProductAsync(...); Funcdouble[,], double[], Taskdouble[]>> matrixVectorProductAsync = async (x, y) => { var sum = 0.0; /* do some stuff using await ... */ return sum; };
从这些语句中我们可以直接地了解到:
在使用 var 时,如果编译器通过参数类型和返回值类型推断无法得出委托类型,将会抛出 “Cannot assign lambda expression to an implicitly-typed local variable.” 的错误提示。来看下如下这些示例:

现在我们已经了解了大部分基础知识,但一些 Lambda 表达式特别酷的部分还没提及。
我们来看下这段代码:
var a = 5; Funcint, int> multiplyWith = x => x * a;
var result1 = multiplyWith(10); // 50 a = 10; var result2 = multiplyWith(10); // 100
可以看到,在 Lambda 表达式中可以使用外围的变量,也就是闭包。
static void DoSomeStuff() { var coeff = 10; Funcint, int> compute = x => coeff * x; Action modifier = () => { coeff = 5; }; var result1 = DoMoreStuff(compute); // 50
ModifyStuff(modifier);
var result2 = DoMoreStuff(compute); // 25 }
static int DoMoreStuff(Funcint, int> computer) { return computer(5); }
static void ModifyStuff(Action modifier) { modifier(); }
这里发生了什么呢?首先我们创建了一个局部变量和两个 Lambda 表达式。第一个 Lambda 表达式展示了其可以在其他作用域中访问该局部变量,实际上这已经展现了强大的能力了。这意味着我们可以保护一个变量,但仍然可以在其他方法中访问它,而不用关心那个方法是定义在当前类或者其他类中。
第二个 Lambda 表达式展示了在 Lambda 表达式中能够修改外围变量的能力。这就意味着通过在函数间传递 Lambda 表达式,我们能够在其他方法中修改其他作用域中的局部变量。因此,我认为闭包是一种特别强大的功能,但有时也可能引入一些非期望的结果。
var buttons = new Button[10]; for (var i = 0; i ) { var button = new Button(); button.Text = (i + 1) + '. Button - Click for Index!'; button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); }; buttons[i] = button; }
//What happens if we click ANY button?!
这个诡异的问题的结果是什么呢?是 Button 0 显示 0, Button 1 显示 1 吗?答案是:所有的 Button 都显示 10!
因为随着 for 循环的遍历,局部变量 i 的值已经被更改为 buttons 的长度 10。一个简单的解决办法类似于:
var button = new Button(); var index = i; button.Text = (i + 1) + '. Button - Click for Index!'; button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); }; buttons[i] = button;
通过定义变量 index 来拷贝变量 i 中的值。
注:如果你使用 Visual Studio 2012 以上的版本进行测试,因为使用的编译器与 Visual Studio 2010 的不同,此处测试的结果可能不同。可参考:Visual C# Breaking Changes in Visual Studio 2012
表达式树
在使用 Lambda 表达式时,一个重要的问题是目标方法是怎么知道如下这些信息的:
我们传递的变量的名字是什么?
我们使用的表达式体的结构是什么?
在表达式体内我们用了哪些类型?
现在,表达式树帮我们解决了问题。它允许我们深究具体编译器是如何生成的表达式。此外,我们也可以执行给定的函数,就像使用 Func 和 Action 委托一样。其也允许我们在运行时解析 Lambda 表达式。
我们来看一个示例,描述如何使用 Expression 类型:
Expressionint>> expr = model => model.MyProperty; var member = expr.Body as MemberExpression; var propertyName = memberExpression.Member.Name; //only execute if member != null
上面是关于 Expression 用法的一个最简单的示例。其中的原理非常直接:通过形成一个 Expression 类型的对象,编译器会根据表达式树的解析生成元数据信息。解析树中包含了所有相关的信息,例如参数和方法体等。
方法体包含了整个解析树。通过它我们可以访问操作符、操作对象以及完整的语句,最重要的是能访问返回值的名称和类型。当然,返回变量的名称可能为 null。尽管如此,大多数情况下我们仍然对表达式的内容很感兴趣。对于开发人员的益处在于,我们不再会拼错属性的名称,因为每个拼写错误都会导致编译错误。
如果程序员只是想知道调用属性的名称,有一个更简单优雅的办法。通过使用特殊的参数属性 CallerMemberName 可以获取到被调用方法或属性的名称。编译器会自动记录这些名称。所以,如果我们仅是需要获知这些名称,而无需更多的类型信息,则我们可以参考如下的代码写法:
string WhatsMyName([CallerMemberName] string callingName = null) { return callingName; }
Lambda 表达式的性能
有一个大问题是:Lambda 表达式到底有多快?当然,我们期待其应该与常规的函数一样快,因为 Lambda 表达式也同样是由编译器生成的。在下一节中,我们会看到为 Lambda 表达式生成的 MSIL 与常规的函数并没有太大的不同。
一个非常有趣的讨论是关于在 Lambda 表达式中的闭包是否要比使用全局变量更快,而其中最有趣的地方就是是否当可用的变量都在本地作用域时是否会有性能影响。
让我们来看一些代码,用于衡量各种性能基准。通过这 4 种不同的基准测试,我们应该有足够的证据来说明常规函数与 Lambda 表达式之间的不同了。
class StandardBenchmark : Benchmark { static double[] A; static double[] B; public static void Test() { var me = new StandardBenchmark(); Init();
for (var i = 0; i 10; i++) { var lambda = LambdaBenchmark(); var normal = NormalBenchmark(); me.lambdaResults.Add(lambda); me.normalResults.Add(normal); }
me.PrintTable(); }
static void Init() { var r = new Random(); A = new double[LENGTH]; B = new double[LENGTH];
for (var i = 0; i ) { A[i] = r.NextDouble(); B[i] = r.NextDouble(); } }
static long LambdaBenchmark() { Funcdouble> Perform = () => { var sum = 0.0;
for (var i = 0; i ) sum += A[i] * B[i];
return sum; }; var iterations = new double[100]; var timing = new Stopwatch(); timing.Start();
for (var j = 0; j ) iterations[j] = Perform();
timing.Stop(); Console.WriteLine('Time for Lambda-Benchmark: t {0}ms', timing.ElapsedMilliseconds); return timing.ElapsedMilliseconds; }
static long NormalBenchmark() { var iterations = new double[100]; var timing = new Stopwatch(); timing.Start();
for (var j = 0; j ) iterations[j] = NormalPerform();
timing.Stop(); Console.WriteLine('Time for Normal-Benchmark: t {0}ms', timing.ElapsedMilliseconds); return timing.ElapsedMilliseconds; }
static double NormalPerform() { var sum = 0.0;
for (var i = 0; i ) sum += A[i] * B[i];
return sum; } }
|