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

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