|
在上一篇文章中我們簡單探討了.NET 1.x和.NET 2.0中委托表現形式的變化,以及.NET 2.0中匿名方法的優勢、目的及注意事項。那么現在我們來談一下.NET 3.5(C# 3.0)中,委托的表現形式又演變成了什么樣子,還有什么特點和作用。
.NET 3.5中委托的寫法(Lambda表達式)
Lambda表達式在C#中的寫法是“arg-list => expr-body”,“=>”符號左邊為表達式的參數列表,右邊則是表達式體(body)。參數列表可以包含0到多個參數,參數之間使用逗號分割。例如,以下便是一個使用Lambda表達式定義了委托的示例1:
Func<int, int, int> max = (int a, int b) =>{ if (a > b) { return a; } else { return b; }};
與上文使用delegate定義匿名方法的作用相同,Lambda表達式的作用也是為了定義一個匿名方法。因此,下面使用delegate的代碼和上面是等價的:
Func<int, int, int> max = delegate(int a, int b){ if (a > b) { return a; } else { return b; }};
那么您可能就會問,這樣看來Lambda表達式又有什么意義呢?Lambda表達式的意義便是它可以寫的非常簡單,例如之前的Lambda表達式可以簡寫成這樣:
Func<int, int, int> max = (a, b) =>{ if (a > b) { return a; } else { return b; }};
由于我們已經注明max的類型是Func<int, int, int>,因此C#編譯器可以明確地知道a和b都是int類型,于是我們就可以省下參數之前的類型信息。這個特性叫做“類型推演”,也就是指編譯器可以自動知道某些成員的類型2。請不要輕易認為這個小小的改進意義不大,事實上,您會發現Lambda表達式的優勢都是由這一點一滴的細節構成的。那么我們再來一次改變:
Func<int, int, int> max = (a, b) => a > b ? a : b;
如果Lambda表達式的body是一個表達式(expression),而不是語句(statement)的話,那么它的body就可以省略大括號和return關鍵字。此外,如果Lambda表達式只包含一個參數的話,則參數列表的括號也可以省略,如下:
Func<int, bool> positive = a => a > 0;
如今的寫法是不是非常簡單?那么我們來看看,如果是使用delegate關鍵字來創建的話會成為什么樣子:
Func<int, bool> positive = delegate(int a){ return a > 0;};
您馬上就可以意識到,這一行和多行的區別,這幾個關鍵字和括號的省略,會使得編程世界一下子變得大為不同。
當然,Lambda表達式也并不是可以完全替代delegate寫法,例如帶ref和out關鍵字的匿名方法,就必須使用.NET 2.0中的delegate才能構造出來了。
使用示例一
Lambda表達式的增強在于“語義”二字。“語義”是指代碼所表現出來的含義,說的更通俗一些,便是指一段代碼給閱讀者的“感覺”如何。為了說明這個例子,我們還是使用示例來說明問題。
第一個例子是這樣的:“請寫一個方法,輸入一個表示整型的字符串列表,并返回一個列表,包含其中偶數的平方,并且需要按照平方后的結果排序”。很簡單,不是嗎?相信您一定可以一蹴而就:
static List<int> GetSquaresOfPositive(List<string> strList){ List<int> intList = new List<int>(); foreach (var s in strList) intList.Add(Int32.Parse(s)); List<int> evenList = new List<int>(); foreach (int i in intList) { if (i % 2 == 0) evenList.Add(i); } List<int> squareList = new List<int>(); foreach (int i in evenList) squareList.Add(i * i); squareList.Sort(); return squareList;}
我想問一下,這段代碼給您的感覺是什么?它給我的感覺是:做了很多事情。有哪些呢?
- 新建一個整數列表intList,把參數strList中所有元素轉化為整型保存起來。
- 新建一個整數列表evenList,把intList中的偶數保存起來。
- 新建一個整數列表squareList,把evenList中所有數字的平方保存起來。
- 將squareList排序。
- 返回squareList。
您可能會問:“當然如此,還能怎么樣?”。事實上,如果使用了Lambda表達式,代碼就簡單多了:
static List<int> GetSquaresOfPositiveByLambda(List<string> strList){ return strList .Select(s => Int32.Parse(s)) // 轉成整數 .Where(i => i % 2 == 0) // 找出所有偶數 .Select(i => i * i) // 算出每個數的平方 .OrderBy(i => i) // 按照元素自身排序 .ToList(); // 構造一個List}
配合.NET 3.5中定義的擴展方法,這段代碼可謂“一氣呵成”(在實際編碼過程中,老趙更傾向于把這種簡短的“遞進式”代碼寫作一行)。那么這行代碼的“語義”又有什么變化呢?在這里,“語義”的變化在于代碼的關注點從“怎么做”變成了“做什么”。這就是Lambda表達式的優勢。
在第一個方法中,我們構造了多個容器,然后做一些轉化,過濾,并且向容器填充內容。其實這些都是“怎么做”,也就是所謂的“how (to do)”。但是這些代碼并不能直接表示我們想要做的事情,我們想要做的事情其實是“得到XXX”,“篩選出YYY”,而不是“創建容器”,“添加元素”等操作。
在使用Lambda表達式的實現中,代碼變得“聲明式(declarative)”了許多。所謂“聲明式”,便是“聲稱代碼在做什么”,而不像“命令式(imperative)”的代碼在“操作代碼怎么做”。換句話說,“聲明式”關注的是“做什么”,是指“what (to do)”。上面這段聲明式的代碼,其語義則變成了:
- 把字符串轉化為整數
- 篩選出所有偶數
- 把每個偶數平方一下
- 按照平方結果自身排序
- 生成一個列表
至于其中具體是怎么實現的,有沒有構造新的容器,又是怎么向容器里添加元素的……這些細節,使用Lambda表達式的代碼一概不會關心——這又不是我們想要做的事情,為什么要關心它呢?
雖然擴展方法功不可沒,但我認為,Lambda表達式在這里的重要程度尤勝前者,因為它負責了最關鍵的“語義”。試想,“i => i * i”給您的感覺是什么呢?是構造了一個委托嗎(當然,您一定知道在這里其實構造了一個匿名方法)?至少對我來說,它的含義是“把i變成i * i”;同樣,“i => i % 2 == 0”給我的感覺是“(篩選標準為)i模2等于零”,而不是“構造一個委托,XXX時返回true,否則返回false”;更有趣的是,OrderBy(i => i)給我的感覺是“把i按照i自身排序”,而不是“一個返回i自身的委托”。這一切,都是在“聲明”這段代碼在“做什么”,而不是“怎么做”。
沒錯,“類型推演”,“省略括號”和“省略return關鍵字”可能的確都是些“細小”的功能,但也正是這些細微之處帶來了編碼方式上的關鍵性改變。
使用示例二
使用Lambda表達式還可以節省許多代碼(相信您從第一個示例中也可以看出來了)。不過我認為,最省代碼的部分更應該可能是其“分組”和“字典轉化”等功能。因此,我們來看第二個示例。
這個示例可能更加貼近現實。不知您是否關注過某些書籍后面的“索引”,它其實就是“列出所有的關鍵字,根據其首字母進行分組,并且要求對每組內部的關鍵字進行排序”。簡單說來,我們需要的其實是這么一個方法:
static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords) { ... }
想想看,您會怎么做?其實不難(作為示例,我們這里只關注小寫英文,也不關心重復關鍵字這種特殊情況):
static Dictionary<char, List<string>> GetIndex(IEnumerable<string> keywords){ // 定義字典 var result = new Dictionary<char, List<string>>(); // 填充字典 foreach (var kw in keywords) { var firstChar = kw[0]; List<string> groupKeywords; if (!result.TryGetValue(firstChar, out groupKeywords)) { groupKeywords = new List<string>(); result.Add(firstChar, groupKeywords); } groupKeywords.Add(kw); } // 為每個分組排序 foreach (var groupKeywords in result.Values) { groupKeywords.Sort(); } return result;}
那么如果利用Lambda表達式及.NET框架中定義的擴展方法,代碼又會變成什么樣呢?請看:
static Dictionary<char, List<string>> GetIndexByLambda(IEnumerable<string> keywords){ return keywords .GroupBy(k => k[0]) // 按照首字母分組 .ToDictionary( // 構造字典 g => g.Key, // 以每組的Key作為鍵 g => g.OrderBy(k => k).ToList()); // 對每組排序并生成列表}
光從代碼數量上來看,前者便是后者的好幾倍。而有關“聲明式”,“what”等可讀性方面的優勢就不再重復了,個人認為它比上一個例子給人的“震撼”有過之而無不及。
試想,如果我們把GetIndexByLambda方法中的Lambda表達式改成.NET 2.0中delegate形式的寫法:
static Dictionary<char, List<string>> GetIndexByDelegate(IEnumerable<string> keywords){ return keywords .GroupBy(delegate(string k) { return k[0]; }) .ToDictionary( delegate(IGrouping<char, string> g) { return g.Key; }, delegate(IGrouping<char, string> g) { return g.OrderBy(delegate(string s) { return s; }).ToList(); });}
您愿意編寫這樣的代碼嗎?
因此,Lambda表達式在這里還是起著決定性的作用。事實上正是因為有了Lambda表達式,.NET中的一些函數式編程特性才被真正推廣開來。“語言特性”決定“編程方式”的確非常有道理。這一點上Java是一個很好的反例:從理論上說,Java也有“內聯”的寫法,但是C#的使用快感在Java那邊還只能是個夢。試想GetIndexByLambda在Java中會是什么情況3:
public Dictionary<Char, List<String>> GetIndexInJava(Enumerable<String> keywords){ return keywords .GroupBy( new Func<String, Char> { public Char execute(String s) { return s.charAt(0); } }) .ToDictionary( new Func<Grouping<Char, String>, Char> { public Char execute(IGrouping<Char, String> g) { return g.getKey(); } }, new Func<Grouping<Char, String>, List<string>> { public List<String> execute(IGrouping<Char, String> g) { return g .OrderBy( new Func<String, String> { public String execute(String s) { return s; } }) .ToList(); } });}
一股語法噪音的氣息撲面而來,讓人無法抵擋。由于Java中的匿名類型語法(即上面這種內聯寫法)連類型信息(new Func<String, Char>{ ... }這樣的代碼)都無法省去,因此給人非常繁瑣的感覺。面對這樣的代碼,您可能會有和我一樣的想法:“還不如最普通的寫法啊”。沒錯,這種函數式編程的風格,由于缺乏語言特性支持,實在不適合在Java語言中使用。事實上,這種內聯寫法很早就出現了(至少在02、03年我還在使用Java的時候就已經有了),但是那么多年下來一點改進都沒有。而Lambda表達式出現之后,社區中立即跟進了大量項目,如Moq,Fluent NHibernate等等,充分運用了C# 3.0的這一新特性。難道這還不夠說明問題嗎?
對了,再次推薦一下Scala語言,它的代碼可以寫的和C#一樣漂亮。我不是Java平臺的粉絲,更是Java語言的忠實反對者,但是我對Java平臺上的Scala語言和開源項目都抱有強烈的好感。
既然談到了函數式編程,那么就順便再多說幾句。其實這兩個例子都有濃厚的函數式編程影子在里面,例如,對于函數試編程來說,Where常被叫做filter,Select常被叫做map。而.NET 3.5中定義的另一些方法在函數式編程里都有體現(如Aggregate相當于fold)。如果您對這方面感興趣,可以關注Matthew Poswysocki提出的Functional C#類庫。
總結
既可以提高可讀性,又能夠減少代碼數量,我實在找不出任何理由拒絕Lambda表達式。
哦,對了,您可能會提到“性能”,這的確也是一個重要的方面,不過關于這個話題我們下次再談。受篇幅限制,原本計劃的“上”“下”兩篇這次又不得不拆開了。至于其他的內容,也等討論完性能問題之后再說吧。
當然,世界上沒有東西是完美的,如果您覺得Lambda表達式在某些時候會給您帶來“危害”,那么也不妨使用delegate代替Lambda表達式。例如,為了代碼清晰,在某些時候還是顯式地指明參數類型比較好。不過對我而言,在任何情況下我都會使用Lambda表達式——最多使用“(int a, string b) =>”的形式咯,我想總比“delegate(int a, string b)”要統一、省事一些吧。
相關文章
注1:嚴格說來,這里的body是一個“語句(statement)”,而不是“表達式(expression)”。因為一個委托其實是一個方法,因此使用Lambda來表示一個委托,其中必然要包含“語句”。不過在目前的C#中,Lambda表達式還有一個作用是構造一顆“表達式樹”,而目前的C#編譯器只能構造“表達式樹”而不是“語句樹”。
注2:事實上,在.NET 2.0使用delegate關鍵字定義匿名方法時已經可以有些許“類型推演”的意味了——雖然還是必須寫明參數的類型,但是我們已經可以省略委托的類型了,不是嗎?
注3:除非我們補充Func、Enumerable,Dictionary,Grouping等類型及API,否則這段代碼在Java中是無法編譯通過的。事實上,這段Java代碼是我在記事本中寫出來的。不過這個形式完全正確。
NET技術:從.NET中委托寫法的演變談開去(中):Lambda表達式及其優勢,轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。