|
來(lái)源:匠心十年
鏈接:http://www.cnblogs.com/gaochundong/p/way_to_lambda.html
Lambda 表達(dá)式
早在 C# 1.0 時(shí),C#中就引入了委托(delegate)類型的概念。通過(guò)使用這個(gè)類型,我們可以將函數(shù)作為參數(shù)進(jìn)行傳遞。在某種意義上,委托可理解為一種托管的強(qiáng)類型的函數(shù)指針。
通常情況下,使用委托來(lái)傳遞函數(shù)需要一定的步驟:
定義一個(gè)委托,包含指定的參數(shù)類型和返回值類型。
在需要接收函數(shù)參數(shù)的方法中,使用該委托類型定義方法的參數(shù)簽名。
為指定的被傳遞的函數(shù)創(chuàng)建一個(gè)委托實(shí)例。
可能這聽(tīng)起來(lái)有些復(fù)雜,不過(guò)本質(zhì)上說(shuō)確實(shí)是這樣。上面的第 3 步通常不是必須的,C# 編譯器能夠完成這個(gè)步驟,但步驟 1 和 2 仍然是必須的。
幸運(yùn)的是,在 C# 2.0 中引入了泛型。現(xiàn)在我們能夠編寫泛型類、泛型方法和最重要的:泛型委托。盡管如此,直到 .NET 3.5,微軟才意識(shí)到實(shí)際上僅通過(guò)兩種泛型委托就可以滿足 99% 的需求:
Action 委托返回 void 類型,F(xiàn)unc 委托返回指定類型的值。通過(guò)使用這兩種委托,在絕大多數(shù)情況下,上述的步驟 1 可以省略了。但是步驟 2 仍然是必需的,但僅是需要使用 Action 和 Func。
那么,如果我只是想執(zhí)行一些代碼該怎么辦?在 C# 2.0 中提供了一種方式,創(chuàng)建匿名函數(shù)。但可惜的是,這種語(yǔ)法并沒(méi)有流行起來(lái)。下面是一個(gè)簡(jiǎn)單的匿名函數(shù)的示例:
Funcdouble, double> square = delegate(double x) { return x * x; };
為了改進(jìn)這些語(yǔ)法,在 .NET 3.5 框架和 C# 3.0 中引入了Lambda 表達(dá)式。
首先我們先了解下 Lambda 表達(dá)式名字的由來(lái)。實(shí)際上這個(gè)名字來(lái)自微積分?jǐn)?shù)學(xué)中的 λ,其涵義是聲明為了表達(dá)一個(gè)函數(shù)具體需要什么。更確切的說(shuō),它描述了一個(gè)數(shù)學(xué)邏輯系統(tǒng),通過(guò)變量結(jié)合和替換來(lái)表達(dá)計(jì)算。所以,基本上我們有 0-n 個(gè)輸入?yún)?shù)和一個(gè)返回值。而在編程語(yǔ)言中,我們也提供了無(wú)返回值的 void 支持。
讓我們來(lái)看一些 Lambda 表達(dá)式的示例:
// 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; };
從這些語(yǔ)句中我們可以直接地了解到:
在使用 var 時(shí),如果編譯器通過(guò)參數(shù)類型和返回值類型推斷無(wú)法得出委托類型,將會(huì)拋出 “Cannot assign lambda expression to an implicitly-typed local variable.” 的錯(cuò)誤提示。來(lái)看下如下這些示例:

現(xiàn)在我們已經(jīng)了解了大部分基礎(chǔ)知識(shí),但一些 Lambda 表達(dá)式特別酷的部分還沒(méi)提及。
我們來(lái)看下這段代碼:
var a = 5; Funcint, int> multiplyWith = x => x * a;
var result1 = multiplyWith(10); // 50 a = 10; var result2 = multiplyWith(10); // 100
可以看到,在 Lambda 表達(dá)式中可以使用外圍的變量,也就是閉包。
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(); }
這里發(fā)生了什么呢?首先我們創(chuàng)建了一個(gè)局部變量和兩個(gè) Lambda 表達(dá)式。第一個(gè) Lambda 表達(dá)式展示了其可以在其他作用域中訪問(wèn)該局部變量,實(shí)際上這已經(jīng)展現(xiàn)了強(qiáng)大的能力了。這意味著我們可以保護(hù)一個(gè)變量,但仍然可以在其他方法中訪問(wèn)它,而不用關(guān)心那個(gè)方法是定義在當(dāng)前類或者其他類中。
第二個(gè) Lambda 表達(dá)式展示了在 Lambda 表達(dá)式中能夠修改外圍變量的能力。這就意味著通過(guò)在函數(shù)間傳遞 Lambda 表達(dá)式,我們能夠在其他方法中修改其他作用域中的局部變量。因此,我認(rèn)為閉包是一種特別強(qiáng)大的功能,但有時(shí)也可能引入一些非期望的結(jié)果。
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?!
這個(gè)詭異的問(wèn)題的結(jié)果是什么呢?是 Button 0 顯示 0, Button 1 顯示 1 嗎?答案是:所有的 Button 都顯示 10!
因?yàn)殡S著 for 循環(huán)的遍歷,局部變量 i 的值已經(jīng)被更改為 buttons 的長(zhǎng)度 10。一個(gè)簡(jiǎn)單的解決辦法類似于:
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;
通過(guò)定義變量 index 來(lái)拷貝變量 i 中的值。
注:如果你使用 Visual Studio 2012 以上的版本進(jìn)行測(cè)試,因?yàn)槭褂玫木幾g器與 Visual Studio 2010 的不同,此處測(cè)試的結(jié)果可能不同??蓞⒖迹篤isual C# Breaking Changes in Visual Studio 2012
表達(dá)式樹(shù)
在使用 Lambda 表達(dá)式時(shí),一個(gè)重要的問(wèn)題是目標(biāo)方法是怎么知道如下這些信息的:
我們傳遞的變量的名字是什么?
我們使用的表達(dá)式體的結(jié)構(gòu)是什么?
在表達(dá)式體內(nèi)我們用了哪些類型?
現(xiàn)在,表達(dá)式樹(shù)幫我們解決了問(wèn)題。它允許我們深究具體編譯器是如何生成的表達(dá)式。此外,我們也可以執(zhí)行給定的函數(shù),就像使用 Func 和 Action 委托一樣。其也允許我們?cè)谶\(yùn)行時(shí)解析 Lambda 表達(dá)式。
我們來(lái)看一個(gè)示例,描述如何使用 Expression 類型:
Expressionint>> expr = model => model.MyProperty; var member = expr.Body as MemberExpression; var propertyName = memberExpression.Member.Name; //only execute if member != null
上面是關(guān)于 Expression 用法的一個(gè)最簡(jiǎn)單的示例。其中的原理非常直接:通過(guò)形成一個(gè) Expression 類型的對(duì)象,編譯器會(huì)根據(jù)表達(dá)式樹(shù)的解析生成元數(shù)據(jù)信息。解析樹(shù)中包含了所有相關(guān)的信息,例如參數(shù)和方法體等。
方法體包含了整個(gè)解析樹(shù)。通過(guò)它我們可以訪問(wèn)操作符、操作對(duì)象以及完整的語(yǔ)句,最重要的是能訪問(wèn)返回值的名稱和類型。當(dāng)然,返回變量的名稱可能為 null。盡管如此,大多數(shù)情況下我們?nèi)匀粚?duì)表達(dá)式的內(nèi)容很感興趣。對(duì)于開(kāi)發(fā)人員的益處在于,我們不再會(huì)拼錯(cuò)屬性的名稱,因?yàn)槊總€(gè)拼寫錯(cuò)誤都會(huì)導(dǎo)致編譯錯(cuò)誤。
如果程序員只是想知道調(diào)用屬性的名稱,有一個(gè)更簡(jiǎn)單優(yōu)雅的辦法。通過(guò)使用特殊的參數(shù)屬性 CallerMemberName 可以獲取到被調(diào)用方法或?qū)傩缘拿Q。編譯器會(huì)自動(dòng)記錄這些名稱。所以,如果我們僅是需要獲知這些名稱,而無(wú)需更多的類型信息,則我們可以參考如下的代碼寫法:
string WhatsMyName([CallerMemberName] string callingName = null) { return callingName; }
Lambda 表達(dá)式的性能
有一個(gè)大問(wèn)題是:Lambda 表達(dá)式到底有多快?當(dāng)然,我們期待其應(yīng)該與常規(guī)的函數(shù)一樣快,因?yàn)?Lambda 表達(dá)式也同樣是由編譯器生成的。在下一節(jié)中,我們會(huì)看到為 Lambda 表達(dá)式生成的 MSIL 與常規(guī)的函數(shù)并沒(méi)有太大的不同。
一個(gè)非常有趣的討論是關(guān)于在 Lambda 表達(dá)式中的閉包是否要比使用全局變量更快,而其中最有趣的地方就是是否當(dāng)可用的變量都在本地作用域時(shí)是否會(huì)有性能影響。
讓我們來(lái)看一些代碼,用于衡量各種性能基準(zhǔn)。通過(guò)這 4 種不同的基準(zhǔn)測(cè)試,我們應(yīng)該有足夠的證據(jù)來(lái)說(shuō)明常規(guī)函數(shù)與 Lambda 表達(dá)式之間的不同了。
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; } }
|