C#2.0之殤,以及函數(shù)式編程的未來
似乎還有不少項(xiàng)目在用C#2.0,但是C#2.0的生產(chǎn)力實(shí)在不如C#3.0——如果您不信,那么一會(huì)兒就會(huì)意識(shí)到這一點(diǎn)。有朋友認(rèn)為語言能力不重要,有了好用的框架/類庫也可以有很高的生產(chǎn)力。所以這篇文章,我們就設(shè)法使用“類庫”來彌補(bǔ)C#2.0的缺陷。
但是,我們真做的到嗎?
C#2.0之殤
C#2.0較C#1.0來說是一個(gè)突破,其中引入了泛型,以及匿名方法等新特性。如果前者還可以說是平臺(tái)的增強(qiáng),而語言只是個(gè)“輔助”的話,而后者則百分之一百是編譯器的魔法了。別小看這個(gè)特性,它為C#3.0的高生產(chǎn)力踏出了堅(jiān)實(shí)的一步——不過還是差了很多。例如,我們有一個(gè)要求:“把一個(gè)字符串?dāng)?shù)組中的元素轉(zhuǎn)化為整數(shù),再將其中的偶數(shù)放入一個(gè)List< int>容器中”。如果是C#3.0,這是再簡單不過的功能:
- string[]strArray={"1","2","3","4"};
- vareven=strArray.Select(s=>Int32.Parse(s)).Where(i=>i%2==0).ToList();
那么對(duì)于C#2.0(當(dāng)然對(duì)于C#1.0也一樣),代碼又該怎么寫呢?
- List< int>even=newList< int>();
- foreach(stringsinstrArray)
- {
- inti=Int32.Parse(s);
- if(i%2==0)
- {
- even.Add(i);
- }
- }
有人說函數(shù)式編程有什么用,C#3.0就是個(gè)很好的證明。C#3.0中引入了Lambda表達(dá)式,增強(qiáng)了在語言中構(gòu)造匿名方法的能力——這是一個(gè)語言中函數(shù)式編程特性的必備條件。C#3.0的實(shí)現(xiàn)與C#2.0相比,可讀性高,可以直接看出轉(zhuǎn)化、過濾,以及構(gòu)造容器的過程和標(biāo)準(zhǔn)。由于語言能力的增強(qiáng),程序的表現(xiàn)能力得到了很大的提高,在很多時(shí)候,我們可以省去將一些代碼提取為獨(dú)立方法的必要。當(dāng)然,即使您將其提取為額外的方法,C#3.0也可以讓您寫出更少的代碼。
如果您覺得以上代碼的差距還不是過于明顯的話——那么以下功能呢?
- int[]intArray={1,2,3,4,5,6,7,8,9,10};
- //所有偶數(shù)的平均數(shù)
- varevenAverage=intArray.Where(i=>i%2==0).Average();
- //都是偶數(shù)?
- varallEven=intArray.All(i=>i%2==0);
- //包含偶數(shù)?
- varcontainsEven=intArray.Any(i=>i%2==0);
- //第4到第8個(gè)數(shù)
- varfourthToEighth=intArray.Skip(3).Take(5);
如果您使用C#2.0來寫,您會(huì)怎么做?
拯救C#2.0
C#3.0通過引入了函數(shù)式編程特性大幅增強(qiáng)了語言的生產(chǎn)力。如果說C#2.0和Java還沒有太大差距的話,那么C#3.0已經(jīng)將Java甩開地太遠(yuǎn)太遠(yuǎn)。不過真要說起來,在Java中并非不可以加入函數(shù)式編程的理念。只不過,如果沒有足夠的語言特性進(jìn)行支持(如快速構(gòu)造匿名函數(shù)、閉包、一定程度的類型推演等等),函數(shù)式編程對(duì)于某些語言來說幾乎只能成為“理念”。不過現(xiàn)在,我們暫且先放下對(duì)“函數(shù)式編程”相關(guān)內(nèi)容的探索,設(shè)法拯救C#2.0所缺失的生產(chǎn)力吧。
C#3.0中可以使用Lambda表達(dá)式構(gòu)造一個(gè)匿名函數(shù),這個(gè)能力其實(shí)在C#2.0中也有。我們姑且認(rèn)為這點(diǎn)不是造成差距的主要原因,那么有一點(diǎn)是C#2.0絕對(duì)無法實(shí)現(xiàn)的,那就是“擴(kuò)展方法”。C#3.0中的擴(kuò)展方法,可以“零耦合”地為一個(gè),甚至一系列類型添加“實(shí)例方法”。當(dāng)然,這也是編譯器的功能,實(shí)際上我們只是定義了一些靜態(tài)方法而已。這一點(diǎn)在C#2.0中還是可以做到的:
- publicclassEnumerable
- {
- publicstaticIEnumerable< T>Where< T>(Func< T,bool>predicate,IEnumerable< T>source)
- {
- foreach(Titeminsource)
- {
- if(predicate(item))
- {
- yieldreturnitem;
- }
- }
- }
- publicstaticIEnumerable< TResult>Select< T,TResult>(Func< T,TResult>selector,IEnumerable< T>source)
- {
- foreach(Titeminsource)
- {
- yieldreturnselector(item);
- }
- }
- publicstaticList< T>ToList< T>(IEnumerable< T>source)
- {
- List< T>list=newList< T>();
- foreach(Titeminsource)
- {
- list.Add(item);
- }
- returnlist;
- }
- }
于是現(xiàn)在,我們便可以換種寫法來實(shí)現(xiàn)相同的功能了:
- string[]strArray={"1","2","3","4"};
- List< int>even=
- Enumerable.ToList(
- Enumerable.Where(
- delegate(inti){returni%2==0;},
- Enumerable.Select(
- delegate(strings){returnInt32.Parse(s);},
- strArray)));
即使您可以接受delegate關(guān)鍵字構(gòu)造匿名函數(shù)的能力,但是上面的做法還是有個(gè)天生的缺陷:邏輯與表現(xiàn)的次序想反。我們想表現(xiàn)的邏輯順序?yàn)椋恨D(zhuǎn)化(Select)、過濾(Where)、及容器構(gòu)造(ToList),C#3.0所表現(xiàn)出的順序和它相同,而C#2.0的順序則相反。由于語言能力的缺失,這個(gè)差距無法彌補(bǔ)。很多時(shí)候,語言的一些“小功能”并不能說是可有可無的特性,它很可能直接決定了是否可以用某種語言來構(gòu)造InternalDSL或進(jìn)行BDD。例如,由于F#的靈活語法,F(xiàn)sTest使得開發(fā)人員可以寫出"foobar"|>shouldcontains"foo"這樣的語句來避免機(jī)械的Assert語法。同樣,老趙也曾經(jīng)使用actor< =msg這樣的邏輯來替代actor.Post(msg)的顯式調(diào)用方式。
封裝邏輯
既然沒有“擴(kuò)展方法”,我們要避免靜態(tài)方法的調(diào)用形式,那么就只能在一個(gè)類中定義邏輯了。這點(diǎn)并不困難,畢竟在API的設(shè)計(jì)發(fā)展至今,已經(jīng)進(jìn)入了關(guān)注FluentInterface的階段,這方面已經(jīng)積累了大量的實(shí)踐。于是我們構(gòu)造一個(gè)Enumerable< T>類,封裝IEnumerable< T>對(duì)象,以此作為擴(kuò)展的入口:
- publicclassEnumerable< T>
- {
- privateIEnumerable< T>m_source;
- publicEnumerable(IEnumerable< T>source)
- {
- if(source==null)thrownewArgumentNullException("source");
- this.m_source=source;
- }
- ...
- }
- 并以此定義所需的Select和Where方法:
- publicEnumerable< T>Where(Func< T,bool>predicate)
- {
- if(predicate==null)thrownewArgumentNullException("predicate");
- returnnewEnumerable< T>(Where(this.m_source,predicate));
- }
- privatestaticIEnumerable< T>Where(IEnumerable< T>source,Func< T,bool>predicate)
- {
- foreach(Titeminsource)
- {
- if(predicate(item))
- {
- yieldreturnitem;
- }
- }
- }
- publicEnumerable< TResult>Select< TResult>(Func< T,TResult>selector)
- {
- if(selector==null)thrownewArgumentNullException("selector");
- returnnewEnumerable< TResult>(Select(this.m_source,selector));
- }
- privatestaticIEnumerable< TResult>Select< TResult>(IEnumerable< T>source,Func< T,TResult>selector)
- {
- foreach(Titeminsource)
- {
- yieldreturnselector(item);
- }
- }
這些擴(kuò)展都是些高階函數(shù),也都有延遲效果,相信很容易理解,在此就不多作解釋了。在這里我們直接觀察其使用方式:
- List< int>even=newEnumerable< string>(strArray)
- .Select(delegate(strings){returnInt32.Parse(s);})
- .Where(delegate(inti){returni%2==0;})
- .ToList();
不知道您對(duì)此有何感覺?
老趙對(duì)此并不滿意,尤其是和C#3.0相較之下。我們雖然定義了Enumerable封裝類,并提供了Select和Where等邏輯,但是由于匿名函數(shù)的構(gòu)造還是較為丑陋。使用delegate構(gòu)造匿名函數(shù)還是引起了不少噪音:
與JavaScript的function關(guān)鍵字,和VB.NET的Function關(guān)鍵字一樣,C#2.0在構(gòu)造匿名函數(shù)時(shí)無法省確delegate關(guān)鍵字。
與C#3.0中的Lambda表達(dá)式相比,使用delegate匿名函數(shù)缺少了必要的類型推演。
使用delegate構(gòu)造匿名函數(shù)時(shí)必須提供完整的方法體,也就是只能提供“語句”,而不能僅為一個(gè)“表達(dá)式”,因此return和最后的分號(hào)無法省確。
我們?cè)O(shè)法拯救C#2.0,但是我們真的做到了嗎?
框架/類庫真能彌補(bǔ)語言的生產(chǎn)力嗎?
【編輯推薦】