Lambda 表达式
早在 C# 1.0 时,C#中就引入了托付(delegate)范例的看法。经由过程运用这个范例,我们可以将函数作为参数举行通报。在某种意义上,托付可理解为一种托管的强范例的函数指针。
一般情况下,运用托付来通报函数须要肯定的步骤:
定义一个托付,包括指定的参数范例和返回值范例。
在须要吸收函数参数的要领中,运用该托付范例定义要领的参数署名。
为指定的被通报的函数建立一个托付实例。
可以这听起来有些庞杂,不过实质上说确实是如许。上面的第 3 步一般不是必须的,C# 编译器可以完成这个步骤,但步骤 1 和 2 依然是必须的。
荣幸的是,在 C# 2.0 中引入了泛型。如今我们可以编写泛型类、泛型要领和最主要的:泛型托付。尽管如此,直到 .NET 3.5,微软才意想到实际上仅经由过程两种泛型托付就可以满足 99% 的需求:
Action :无输入参数,无返回值
Action :支撑1-16个输入参数,无返回值
Func :支撑1-16个输入参数,有返回值
Action 托付返回 void 范例,Func 托付返回指定范例的值。经由过程运用这两种托付,在绝大多数情况下,上述的步骤 1 可以省略了。然则步骤 2 依然是必须的,但仅是须要运用 Action 和 Func。
那末,假如我只是想实行一些代码该怎样办?在 C# 2.0 中供应了一种体式格局,建立匿名函数。但惋惜的是,这类语法并没有流行起来。下面是一个简朴的匿名函数的示例:
Func<double, 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); Func<double, double> square = x => x * x; // Can be used as with double z = product(9, 5); Func<double, double, double> product = (x, y) => x * y; // Can be used as with printProduct(9, 5); Action<double, 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 }); Func<double[], 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(...); Func<double[,], double[], Task<double[]>> matrixVectorProductAsync = async (x, y) => { var sum = 0.0; /* do some stuff using await ... */ return sum; };
从这些语句中我们可以直接地相识到:
假如唯一一个入参,则可省略圆括号。
假如唯一一行语句,而且在该语句中返回,则可省略大括号,而且也可以省略 return 症结字。
经由过程运用 async 症结字,可以将 Lambda 表达式声明为异步实行。
大多数情况下,var 声明可以没法运用,仅在一些迥殊的情况下可以运用。
在运用 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 < buttons.Length; 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; } }
固然,应用 Lambda 表达式,我们可以把上面的代码写的更文雅一些,这么写的缘由是防备滋扰终究的效果。所以我们仅供应了 3 个必要的要领,个中一个担任实行 Lambda 测试,一个担任通例函数测试,第三个要领则是在通例函数。而缺乏的第四个要领就是我们的 Lambda 表达式,其已在第一个要领中内嵌了。运用的盘算要领并不主要,我们运用了随机数,进而避免了编译器的优化。末了,我们最感兴趣的就是通例函数与 Lambda 表达式的差别。
在运转这些测试后,我们会发明,在一般情况下 Lambda 表达式不会表现的比通例函数更差。而个中的一个很新鲜的效果就是,Lambda 表达式实际上在某些情况下表现的要比通例要领还要好些。固然,假如是在运用闭包的条件下,效果就不一样了。这个效果通知我们,运用 Lambda 表达式无需再犹疑。然则我们依然须要细致的斟酌当我们运用闭包时所丧失的机能。在这类情形下,我们一般会丧失一点机能,但也许依然还能接收。关于机能丧失的缘由将鄙人一节中揭开。
下面的表格中显现了基准测试的效果:
无入参无闭包比较
含入参比较
含闭包比较
含入参含闭包比较
Test | Lambda [ms] | Normal [ms] |
0 | 45+-1 | 46+-1 |
1 | 44+-1 | 46+-2 |
2 | 49+-3 | 45+-2 |
3 | 48+-2 | 45+-2 |
注:测试效果依据机械硬件设置有所差别
下面的图表中一样展示了测试效果。我们可以看到,通例函数与 Lambda 表达式会有雷同的限定。运用 Lambda 表达式并没有明显的机能丧失。
MSIL揭秘Lambda表达式
运用有名的东西 LINQPad 我们可以检察 MSIL。
我们来看下第一个示例:
void Main() { DoSomethingLambda("some example"); DoSomethingNormal("some example"); }
Lambda 表达式:
Actionstring> DoSomethingLambda = (s) => { Console.WriteLine(s);// + local };
响应的要领的代码:
void DoSomethingNormal(string s) { Console.WriteLine(s); }
两段代码的 MSIL 代码:
IL_0001: ldarg.0 IL_0002: ldfld UserQuery.DoSomethingLambda IL_0007: ldstr "some example" IL_000C: callvirt System.Action.Invoke IL_0011: nop IL_0012: ldarg.0 IL_0013: ldstr "some example" IL_0018: call UserQuery.DoSomethingNormal DoSomethingNormal: IL_0000: nop IL_0001: ldarg.1 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret b__0: IL_0000: nop IL_0001: ldarg.0 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret
此处最大的差别就是函数的定名和用法,而不是声明体式格局,实际上声明体式格局是雷同的。编译器会在当前类中建立一个新的要领,然后揣摸该要领的用法。这没什么迥殊的,只是运用 Lambda 表达式方便了很多。从 MSIL 的角度来看,我们做了雷同的事,也就是在当前的对象上挪用了一个要领。
我们可以将这些剖析放到一张图中,来展示编译器所做的变动。鄙人面这张图中我们可以看到编译器将 Lambda 表达式移到了一个零丁的要领中。
在第二个示例中,我们将展示 Lambda 表达式真正奇异的处所。在这个例子中,我们运用了一个通例的要领来接见全局变量,然后用一个 Lambda 表达式来捕捉部分变量。代码以下:
void Main() { int local = 5; Actionstring> DoSomethingLambda = (s) => { Console.WriteLine(s + local); }; global = local; DoSomethingLambda("Test 1"); DoSomethingNormal("Test 2"); } int global; void DoSomethingNormal(string s) { Console.WriteLine(s + global); }
现在看来没什么迥殊的。症结的题目是:编译器是怎样处置惩罚 Lambda 表达式的?
IL_0000: newobj UserQuery+c__DisplayClass1..ctor IL_0005: stloc.1 // CS$8__locals2 IL_0006: nop IL_0007: ldloc.1 // CS$8__locals2 IL_0008: ldc.i4.5 IL_0009: stfld UserQuery+c__DisplayClass1.local IL_000E: ldloc.1 // CS$8__locals2 IL_000F: ldftn UserQuery+c__DisplayClass1.b__0 IL_0015: newobj System.Action..ctor IL_001A: stloc.0 // DoSomethingLambda IL_001B: ldarg.0 IL_001C: ldloc.1 // CS$8__locals2 IL_001D: ldfld UserQuery+c__DisplayClass1.local IL_0022: stfld UserQuery.global IL_0027: ldloc.0 // DoSomethingLambda IL_0028: ldstr "Test 1" IL_002D: callvirt System.Action.Invoke IL_0032: nop IL_0033: ldarg.0 IL_0034: ldstr "Test 2" IL_0039: call UserQuery.DoSomethingNormal IL_003E: nop DoSomethingNormal: IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.0 IL_0003: ldfld UserQuery.global IL_0008: box System.Int32 IL_000D: call System.String.Concat IL_0012: call System.Console.WriteLine IL_0017: nop IL_0018: ret c__DisplayClass1.b__0: IL_0000: nop IL_0001: ldarg.1 IL_0002: ldarg.0 IL_0003: ldfld UserQuery+c__DisplayClass1.local IL_0008: box System.Int32 IL_000D: call System.String.Concat IL_0012: call System.Console.WriteLine IL_0017: nop IL_0018: ret c__DisplayClass1..ctor: IL_0000: ldarg.0 IL_0001: call System.Object..ctor IL_0006: ret
照样一样,两个函数从挪用语句上看是雷同的,照样应用了与之前雷同的机制。也就是说,编译器为该函数生成了一个名字,并把它替换到代码中。而此处最大的区分在于,编译器同时生成了一个类,而编译器生成的函数就被放到了这个类中。那末,建立这个类的目标是什么呢?它使变量具有了全局作用域局限,而此之前其已被用于捕捉变量。经由过程这类体式格局,Lambda 表达式有才能接见部分作用域的变量(由于从 MSIL 的看法来看,其仅是类实例中的一个全局变量罢了)。
然后,经由过程这个新生成的类的实例,一切的变量都从这个实例分派和读取。这处置惩罚了变量间存在援用的题目(会对类增加一个分外的援用 – 确实是如许)。编译器已充足的智慧,可以将那些被捕捉变量放到这个类中。所以,我们可以会期待运用 Lambda 表达式并不会存在机能题目。但是,这里我们必须提出一个正告,就是这类行动可以会引起内存走漏,由于对象依然被 Lambda 表达式援用着。只需这个函数还在,其作用局限依然有用(之前我们已相识了这些,但如今我们晓得了缘由)。
像之前一样,我们把这些剖析放入一张图中。从图中我们可以看到,闭包并非唯一的被挪动的要领,被捕捉变量也被挪动了。一切被挪动的对象都邑被放入一个编译器生成的类中。末了,我们从一个未知的类实例化了一个对象。
以上就是细致引见C# Lambda表达式的宿世此生(图)的细致内容,更多请关注ki4网别的相干文章!