快樂的Lambda表達式(二) 自從Lambda隨.NET Framework3.5出現在.NET開發者眼前以來,它已經給我們帶來了太多的欣喜。它優雅,對開發者更友好,能提高開發效率,天啊!它還有可能降低發生一些潛在錯誤的可能。LINQ包括ASP.NET MVC中的很多功能都是用Lambda實現的。 ...
自從Lambda隨.NET Framework3.5出現在.NET開發者眼前以來,它已經給我們帶來了太多的欣喜。它優雅,對開發者更友好,能提高開發效率,天啊!它還有可能降低發生一些潛在錯誤的可能。LINQ包括ASP.NET MVC中的很多功能都是用Lambda實現的。我只能說自從用了Lambda,我腰也不酸了,腿也不疼了,手指也不抽筋了,就連寫代碼bug都少了。小伙伴們,你們今天用Lambda了麽?但是你真的瞭解它麽?今天我們就來好好的認識一下吧。
本文會介紹到一些Lambda的基礎知識,然後會有一個小小的性能測試對比Lambda表達式和普通方法的性能,接著我們會通過IL來深入瞭解Lambda到底是什麼,最後我們將用Lambda表達式來實現一些JavaScript裡面比較常見的模式。
瞭解Lambda
在.NET 1.0的時候,大家都知道我們經常用到的是委托。有了委托呢,我們就可以像傳遞變數一樣的傳遞方法。在一定程式上來講,委托是一種強類型的托管的方法指針,曾經也一時被我們用的那叫一個廣泛呀,但是總的來說委托使用起來還是有一些繁瑣。來看看使用一個委托一共要以下幾個步驟:
- 用delegate關鍵字創建一個委托,包括聲明返回值和參數類型
- 使用的地方接收這個委托
- 創建這個委托的實例並指定一個返回值和參數類型匹配的方法傳遞過去
複雜嗎?好吧,也許06年你說不複雜,但是現在,真的挺複雜的。
後來,幸運的是.NET 2.0為了們帶來了泛型。於是我們有了泛型類,泛型方法,更重要的是泛型委托。最終 在.NET3.5的時候,我們Microsoft的兄弟們終於意識到其實我們只需要2個泛型委托(使用了重載)就可以覆蓋99%的使用場景了。
- Action 沒有輸入參數和返回值的泛型委托
- Action<T1, …, T16> 可以接收1個到16個參數的無返回值泛型委托
- Func<T1, …, T16, Tout> 可以接收0到16個參數並且有返回值的泛型委托
這樣我們就可以跳過上面的第一步了,不過第2步還是必須的,只是用Action或者Func替換了。別忘了在.NET2.0的時候我們還有匿名方法,雖然它沒怎麼流行起來,但是我們也給它 一個露臉的機會。
Func<double, double> square = delegate (double x) { return x * x; }
最後,終於輪到我們的Lambda優雅的登場了。
// 編譯器不知道後面到底是什麼玩意,所以我們這裡不能用var關鍵字 Action dummyLambda = () => { Console.WriteLine("Hello World from a Lambda expression!"); }; // double y = square(25); Func<double, double> square = x => x * x; // double z = product(9, 5); Func<double, double, double> product = (x, y) => x * y; // printProduct(9, 5); Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); }; // 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; }; // var result = matrixVectorProductAsync(...); Func<double, double, Task<double>> matrixVectorProductAsync = async (x, y) => { var sum = 0.0; /* do some stuff using await ... */ return sum; };
從上面的代碼中我們可以看出:
- 如果只有一個參數,不需要寫()
- 如果只有一條執行語句,並且我們要返回它,就不需要{},並且不用寫return
- Lambda可以非同步執行,只要在前面加上async關鍵字即可
- Var關鍵字在大多數情況下都不能使用
當然,關於最後一條,以下這些情況下我們還是可以用var關鍵字的。原因很簡單,我們告訴編譯器,後面是個什麼類型就可以了。
Func<double,double> square = (double x) => x * x; Func<string,int> stringLengthSquare = (string s) => s.Length * s.Length; Action<decimal,string> squareAndOutput = (decimal x, string s) => { var sqz = x * x; Console.WriteLine("Information by {0}: the square of {1} is {2}.", s, x, sqz); };
現在,我們已經知道Lambda的一些基本用法了,如果僅僅就這些東西,那就不叫快樂的Lambda表達式了,讓我們看看下麵的代碼。
var a = 5; Func<int,int> multiplyWith = x => x * a; var result1 = multiplyWith(10); //50 a = 10; var result2 = multiplyWith(10); //100
是不是有一點感覺了?我們可以在Lambda表達式中用到外面的變數,沒錯,也就是傳說中的閉包啦。
void DoSomeStuff() { var coeff = 10; Func<int,int> compute = x => coeff * x; Action modifier = () => { coeff = 5; }; var result1 = DoMoreStuff(compute); ModifyStuff(modifier); var result2 = DoMoreStuff(compute); } int DoMoreStuff(Func<int,int> computer) { return computer(5); } void ModifyStuff(Action modifier) { modifier(); }
在上面的代碼中,DoSomeStuff方法裡面的變數coeff實際是由外部方法ModifyStuff修改的,也就是說ModifyStuff這個方法擁有了訪問DoSomeStuff裡面一個局部變數的能力。它是如何做到的?我們馬上會說的J。當然,這個變數作用域的問題也是在使用閉包時應該註意的地方,稍有不慎就有可能會引發你想不到的後果。看看下麵這個你就知道了。
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; }
猜猜你點擊這些按鈕的結果是什麼?是”1, 2, 3…”。但是,其實真正的結果是全部都顯示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;
其實做法很簡單,就是在for的迴圈裡面把當前的i保存下來,那麼每一個表達式裡面存儲的值就不一樣了。
接下來,我們整點高級的貨,和Lambda息息相關的表達式(Expression)。為什麼說什麼息息相關,因為我們可以用一個Expression將一個Lambda保存起來。並且允許我們在運行時去解釋這個Lambda表達式。來看一下下麵簡單的代碼:
Expression<Func<MyModel, int>> expr = model => model.MyProperty; var member = expr.Body as MemberExpression; var propertyName = member.Expression.Member.Name;
這個的確是Expression最簡單的用法之一,我們用expr存儲了後面的表達式。編譯器會為我們生成表達式樹,在表達式樹中包括了一個元數據像參數的類型,名稱還有方法體等等。在LINQ TO SQL中就是通過這種方法將我們設置的條件通過where擴展方法傳遞給後面的LINQ Provider進行解釋的,而LINQ Provider解釋的過程實際上就是將表達式樹轉換成SQL語句的過程。
Lambda表達式的性能
關於Lambda性能的問題,我們首先可能會問它是比普通的方法快呢?還是慢呢?接下來我們就來一探究竟。首先我們通過一段代碼來測試一下普通方法和Lambda表達 式之間的性能差異。
class StandardBenchmark : Benchmark { const int LENGTH = 100000; static double[] A; static double[] B; static void Init() { var r = new Random(); A = new double[LENGTH]; B = new double[LENGTH]; for (var i = 0; i < LENGTH; i++) { A[i] = r.NextDouble(); B[i] = r.NextDouble(); } } static long LambdaBenchmark() { Func<double> Perform = () => { var sum = 0.0; for (var i = 0; i < LENGTH; 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.Length; 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.Length; 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 < LENGTH; i++) sum += A[i] * B[i]; return sum; } } }
代碼很簡單,我們通過執行同樣的代碼來比較,一個放在Lambda表達式里,一個放在普通的方法裡面。通過4次測試得到如下結果:
Lambda Normal-Method
70ms 84ms
73ms 69ms
92ms 71ms
87ms 74ms
按理來說,Lambda應該是要比普通方法慢很小一點點的,但是不明白第一次的時候為什麼Lambda會比普通方法還快一點。- -!不過通過這樣的對比我想至少可以說明Lambda和普通方法之間的性能其實幾乎是沒有區別的。
那麼Lambda在經過編譯之後會變成什麼樣子呢?讓LINQPad告訴你。
上圖中的Lambda表達式是這樣的:
Action<string> DoSomethingLambda = (s) => { Console.WriteLine(s);// + local };
對應的普通方法的寫法是這樣的:
void DoSomethingNormal(string s) { Console.WriteLine(s); }
上面兩段代碼生成的IL代碼呢?是這樣地:
DoSomethingNormal: IL_0000: nop IL_0001: ldarg.1 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret <Main>b__0: IL_0000: nop IL_0001: ldarg.0 IL_0002: call System.Console.WriteLine IL_0007: nop IL_0008: ret
最大的不同就是方法的名稱以及方法的使用而不是聲明,聲明實際上是一樣的。通過上面的IL代碼我們可以看出,這個表達式實際被編譯器取了一個名稱,同樣被放在了當前的類裡面。所以實際上,和我們調類裡面的方法沒有什麼兩樣。下麵這張圖說明瞭這個編譯的過程:
上面的代碼中沒有用到外部變數,接下來我們來看另外一個例子。
void Main() { int local = 5; Action<string> DoSomethingLambda = (s) => { Console.WriteLine(s + local); }; global = local; DoSomethingLambda("Test 1"); DoSomethingNormal("Test 2"); } int global; void DoSomethingNormal(string s) { Console.WriteLine(s + global); }
這次的IL代碼會有什麼不同麽?
IL_0000: newobj UserQuery+<>c__DisplayClass1..ctor IL_0005: stloc.1 IL_0006: nop IL_0007: ldloc.1 IL_0008: ldc.i4.5 IL_0009: stfld UserQuery+<>c__DisplayClass1.local IL_000E: ldloc.1 IL_000F: ldftn UserQuery+<>c__DisplayClass1.<Main>b__0 IL_0015: newobj System.Action<System.String>..ctor IL_001A: stloc.0 IL_001B: ldarg.0 IL_001C: ldloc.1 IL_001D: ldfld UserQuery+<>c__DisplayClass1.local IL_0022: stfld UserQuery.global IL_0027: ldloc.0 IL_0028: ldstr "Test 1" IL_002D: callvirt System.Action<System.String>.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.<Main>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
你發現了嗎?兩個方法所編譯出來的內容是一樣的, DoSomtingNormal和<>c__DisplayClass1.<Main>b__0,它們裡面的內容是一樣的。但是最大的不一樣,請註意了。當我們的Lambda表達式裡面用到了外部變數的時候,編譯器會為這個Lambda生成一個類,在這個類中包含了我們表達式方法。在使用這個Lambda表達式的地方呢,實際上是new了這個類的一個實例進行調用。這樣的話,我們表達式裡面的外部變數,也就是上面代碼中用到的local實際上是以一個全局變數的身份存在於這個實例中的。
用Lambda表達式實現一些在JavaScript中流行的模式
說到JavaScript,最近幾年真是風聲水起。不光可以應用所有我們軟體工程現存的一些設計模式,並且由於它的靈活性,還有一些由於JavaScript特性而產生的模式。比如說模塊化,立即執行方法體等。.NET由於是強類型編譯型的語言,靈活性自然不如JavaScript,但是這並不意味著JavaScript能做的事情.NET就不能做,下麵我們就來實現一些JavaScript中好玩的寫法。
回調模式
回調模式也並非JavaScript特有,其實在.NET1.0的時候,我們就可以用委托來實現回調了。但是今天我們要實現的回調可就不一樣了。
void CreateTextBox() { var tb = new TextBox(); tb.IsReadOnly = true; tb.Text = "Please wait ..."; DoSomeStuff(() => { tb.Text = string.Empty; tb.IsReadOnly = false; }); } void DoSomeStuff(Action callback) { // Do some stuff - asynchronous would be helpful ... callback(); }
上面的代碼中,我們在DoSomeStuff完成之後,再做一些事情。這種寫法在JavaScript中是很常見的,jQuery中的Ajax的oncompleted, onsuccess不就是這樣實現的麽?又或者LINQ擴展方法中的foreach不也是這樣的麽?
返回方法
我們在JavaScript中可以直接return一個方法,在.net中雖然不能直接返回方法,但是我們可以返回一個表達式。
Func<string, string> SayMyName(string language) { switch(language.ToLower()) { case "fr": return name => { return "Je m'appelle " + name + "."; }; case "de": return name => { return "Mein Name ist " + name + "."; }; default: return name => { return "My name is " + name + "."; }; } } void Main() { var lang = "de"; //Get language - e.g. by current OS settings var smn = SayMyName(lang); var name = Console.ReadLine(); var sentence = smn(name); Console.WriteLine(sentence); }
是不是有一種策略模式的感覺?這還不夠完美,這一堆的switch case看著就心煩,讓我們用Dictionary<TKey,TValue>來簡化它。來看看來面這貨:
static class Translations { static readonly Dictionary<string, Func<string, string>> smnFunctions = new Dictionary<string, Func<string, string>>(); static Translations() { smnFunctions.Add("fr", name => "Je m'appelle " + name + "."); smnFunctions.Add("de", name => "Mein Name ist " + name + "."); smnFunctions.Add("en", name => "My name is " + name + "."); } public static Func<string, string> GetSayMyName(string language) { //Check if the language is available has been omitted on purpose return smnFunctions[language]; } }
自定義型方法
自定義型方法在JavaScript中比較常見,主要實現思路是這個方法被設置成一個屬性。在給這個屬性附值,甚至執行過程中我們可以隨時更改這個屬性的指向,從而達到改變這個方法的目地。
class SomeClass { public Func<int> NextPrime { get; private set; } int prime; public SomeClass { NextPrime = () => { prime = 2; NextPrime = () => {
// 這裡可以加上 第二次和第二次以後執行NextPrive()的邏輯代碼 return prime; }; return prime; } } }
上面的代碼中當NextPrime第一次被調用的時候是2,與此同時,我們更改了NextPrime,我們可以把它指向另外的方法,和JavaScrtip的靈活性比起來也不差吧?如果你還不滿意 ,那下麵的代碼應該能滿足你。
Action<int> loopBody = i => { if(i == 1000) loopBody = //把loopBody指向別的方法 /* 前10000次執行下麵的代碼 */ }; for(int j = 0; j < 10000000; j++) loopBody(j);
在調用的地方我們不用考慮太多,然後這個方法本身就具有調優性了。我們原來的做法可能是在判斷i==1000之後直接寫上相應的代碼,那麼和現在的把該方法指向另外一個方法有什麼區別呢?
自執行方法
JavaScript 中的自執行方法有以下幾個優勢:
- 不會污染全局環境
- 保證自執行裡面的方法只會被執行一次
- 解釋完立即執行
在C#中我們也可以有自執行的方法:
(() => { // Do Something here! })();
上面的是沒有參數的,如果你想要加入參數,也非常的簡單:
((string s, int no) => { // Do Something here! })("Example", 8);
.NET4.5最閃的新功能是什麼?async?這裡也可以
await (async (string s, int no) => { // 用Task非同步執行這裡的代碼 })("Example", 8); // 非同步Task執行完之後的代碼
對象即時初始化
大家知道.NET為我們提供了匿名對象,這使用我們可以像在JavaScript裡面一樣隨意的創建我們想要對象。但是別忘了,JavaScript裡面可以不僅可以放入數據,還可以放入方法,.NET可以麽?要相信,Microsoft不會讓我們失望的。
//Create anonymous object var person = new { Name = "Jesse", Age = 28, Ask = (string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42!"); } }; //Execute function person.Ask("Why are you doing this?");
但是如果你真的是運行這段代碼,是會拋出異常的。問題就在這裡,Lambda表達式是不允許賦值給匿名對象的。但是委托可以,所以在這裡我們只需要告訴編譯器,我是一個什麼類型的委托即可。
var person = new { Name = "Florian", Age = 28, Ask = (Action<string>)((string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42!"); }) };
但是這裡還有一個問題,如果我想在Ask方法裡面去訪問person的某一個屬性,可以麽?
var person = new { Name = "Jesse", Age = 18, Ask = ((Action<string>)((string question) => { Console.WriteLine("The answer to '" + question + "' is certainly 20. My age is " + person.Age ); })) };
結果是連編譯都通不過,因為person在我們的Lambda表達式這裡還是沒有定義的,當然不允許使用了,但是在JavaScript裡面是沒有問題的,怎麼辦呢?.NET能行麽?當然行,既然它要提前定義,我們就提前定義好了。
dynamic person = null; person = new { Name = "Jesse", Age = 28, Ask = (Action<string>)((string question) => { Console.WriteLine("The answer to `" + question + "` is certainly 42! My age is " + person.Age + "."); }) }; //Execute function person.Ask("Why are you doing this?");
運行時分支
這個模式和自定義型方法有點類似,唯一的不同是它不是在定義自己,而是在定義別的方法。當然,只有當這個方法基於屬性定義的時候才有這種實現的可能。
public Action AutoSave { get; private set; } public void ReadSettings(Settings settings) { /* Read some settings of the user */ if(settings.EnableAutoSave) AutoSave = () => { /* Perform Auto Save */ }; else AutoSave = () => { }; //Just do nothing! }
可能有人會覺得這個沒什麼,但是仔細想想,你在外面只需要調用AutoSave就可以了,其它的都不用管。而這個AutoSave,也不用每次執行的時候都需要去檢查配置文件了。
總結
Lambda表達式在最後編譯之後實質是一個方法,而我們聲明Lambda表達式呢實質上是以委托的形式傳遞的。當然我們還可以通過泛型表達式Expression來傳遞。通過Lambda表達式形成閉包,可以做很多事情,但是有一些用法現在還存在爭議,本文只是做一個概述 :),如果有不妥,還請拍磚。謝謝支持 :)
還有更多Lambda表達式的新鮮玩法,請移步: 背後的故事之 - 快樂的Lambda表達式(二)
原文鏈接: http://www.codeproject.com/Articles/507985/Way-to-Lambda