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