C# 3.0新的特性

 

我眼中的C# 3.0

 

Written by Allen Lee

 

緣起

每次有新技術發佈時,我們總能感受到兩種截然不同的情緒:一種是恐懼和抵抗,伴隨着這種情緒的還有諸如"C# 2.0用的挺好的,爲什麼要在C# 3.0搞到那麼複雜?"或者"我還在使用C# 1.0呢?"等言辭;另一種則是興奮和擁抱,伴隨着這種情緒的還有諸如"原來這個問題在C# 3.0裏可以這麼簡單!"等言辭。

最近我在公司內部做一個LINQ的系列講座,在我爲其中C# 3.0新特性這一講準備演示文稿時,突然萌生了寫下這篇文章的念頭。語言的特性乃至其本身並沒有對錯之分,是否接受在很大程度上是一個感性問題,即你是否喜歡這樣的做事方式,我並沒有打算說服任何人接受C# 3.0和LINQ,寫這篇文章也只是想和大家分享一下我自己的感受。

有一次我觀看一個關於Expression Blend的培訓視頻,裏面說了一句讓我印象非常深刻的話:

I know how it works because I know why it works.

細細品味這句話,你會感受到它所要傳達的信息:理解爲何需要這個功能可以幫助你更好地理解如何使用這個功能,而這也正是我要在這篇文章裏採用的表達方式。

 

你是如何創建屬性的?

如果你長期使用C#,相信你不會對屬性這個東西感到陌生。一般地,屬性是對私有字段的一個簡單包裝,就像這樣:

代碼 1

使用屬性而不是直接公開私有字段的一個好處就是在屬性的獲取訪問器或設置訪問器里加入額外的邏輯並不會爲客戶端代碼帶來麻煩,例如你想在設置標題的時候做一些額外的檢查。但如果你只是簡單地包裝一下,像上面的代碼那樣,就會發現你其實多寫了不少可以省略的代碼。既然Title屬性和m_Title私有字段對應,獲取訪問器就肯定是返回m_Title的值,而設置訪問器也肯定是把值設到m_Title。再者,如果你只通過Title屬性來訪問這個數據,那麼m_Title私有字段就會變得無足輕重,這樣的話,爲什麼不交給編譯器代勞呢?這個時候,C# 3.0的自動屬性就可以派上用場了:

代碼 2

編譯器會爲你創建一個私有字段,並讓獲取訪問器和設置訪問器指向這個私有字段。當然,如果有需要,例如要在獲取訪問器或設置訪問器里加入額外的邏輯時,你隨時可以對獲取訪問器和設置訪問器進行展開。

 

你是如何初始化對象的?

現在,假設我們有這樣一個類:

代碼 3

你會怎樣初始化它?一種做法是用Book的默認構造函數創建對象實例,然後分別爲每個屬性賦值:

代碼 4

另一種做法是使用C# 3.0對象初始化器:

代碼 5

乍看一下,C# 3.0的做法似乎沒有讓人感到任何優越感,現在,請你仔細觀察一下,這兩份代碼分別包含多少個";"?代碼4有5個";",意味着它用了5個語句進行初始化;而代碼5只有1個";",意味着它只用了1個語句進行初始化。從詞法的角度來看,如果此刻我只能接受一個表達式,那麼代碼4的做法就幫不上忙了。一個變通的方法是爲Book類提供帶參的構造函數,但這種方法也有弊端,用戶可能只想在初始化時爲部分屬性提供數據,而我們又無法確切預知用戶會提供哪些屬性的組合,於是,我們可能要爲用戶提供足夠多的構造函數重載,嗯,有點無聊,也有點多餘。另一個變通的方法是提供接受最多參數的構造函數,如果用戶爲某個參數傳遞null,那麼就忽略與之對應的屬性,這個方法比較接近代碼5的做法,不同的是,如果你的屬性很多,而用戶關心的只是很少一部分,就可能不得不輸入很多null了。

現在,假設你要實例化一組Book對象,並把它們儲存在一個集合裏,你會怎麼做?下面是通常的做法:

代碼 6

如果結合使用C# 3.0的對象初始化器和集合初始化器,你就可以把代碼簡化爲:

代碼 7

集合裏的每個元素通過","分割,結合對象初始化器使用,整個集合的結構顯得比較明晰。字典的初始化也可以同樣簡單:

代碼 8

說到這裏,我相信你也能感覺到,C#似乎正在表達式化,以前需要很多條語句才能做到的事情,現在卻可以用單個表達式描述出來,而這種理念也滲透在整個C# 3.0的氛圍裏。

 

你是如何把運算邏輯外包出去的?

假設我現在得到了一組Book的實例對象,你要對它們進行排序,那麼你如何告訴它你要按價格來排序呢?

代碼 9

在C# 1.0裏,我們需要特意爲它提供一個獨立的方法:

代碼 10

然後向Sort()方法傳入所需委託的實例:

代碼 11

這在C# 2.0裏可以進一步簡化爲:

代碼 12

如果使用C# 2.0的匿名方法,我們可以省去很多不必要的代碼:

代碼 13

此外,使用匿名方法,Sort()方法和你希望它用來比較兩個Book實例對象的邏輯可以放在同一個地方;而使用獨立的命名方法,包含這個邏輯的方法可能會由於整理代碼而被挪到別的地方。這樣,當你看到代碼12時,爲了瞭解它內部的實現,就不得不花一些精力去尋找Compare()方法了。當然,你可以爭辯說,我們可以制定一個編碼規範,使得Compare()方法必須緊貼在Sort()方法的下方。是的,你可以,但如果這個邏輯並不需要重用,那麼使用匿名方法還是具有明顯的優勢的。如果這個邏輯需要重用,那麼匿名方法就無能爲力了。

現在,讓我們來考察一下代碼13,有沒有發現匿名方法的表達方式還不夠簡練?我們知道,books集合裏面只有Book的實例對象,所以Sort()方法傳給我們兩個參數的類型必定是Book,而Sort()方法期待的結果正是x.Price.CompareTo(y.Price)這個表達式的運算結果,至於delegate和return這樣的字眼可以說在這裏完全是多餘的,那麼爲什麼我們不直接這樣表達呢:

代碼 14

這就是C# 3.0引入的Lambda表達式語法。我見過一些人,他們通常強調儘可能簡單,但若事情突然變得比他們預期的還要簡單很多,他們就開始感到不適,甚至拒絕接受這種簡單,其實即使事物的發展方向和你的前進方向相一致,但如果發展速度大大超越了你,仍然有可能引發你內心對失控的恐懼。我希望Lambda表達式語法不會讓你感到太大的不適,當然我更希望你會喜歡上它。

Lambda表達式的理解其實可以很簡單,就是"=>"左邊的參數參與右邊的表達式運算,而運算結果將會返回,這有點像化合反應,即兩種或兩種以上的物質(左邊的參數)生成一種新物質(右邊的表達式的運算結果),不同的是,Lambda可以不接收任何參數,也可以不返回任何結果。

"=>"右邊除了可以放表達式之外,還可以放語句,像這樣:

代碼 15

我們把它稱爲Lambda語句(Lambda Statement),或許你已經發現,它和匿名方法相比只是不需要寫delegate關鍵字和參數類型。

 

你是如何爲對象擴展與之相關的功能的?

我一直在想,爲什麼String類沒有提供一個Reverse()方法,把字符串翻轉呢?我猜可能是因爲這種操作沒有什麼現實意思,除非你要做一個文字遊戲。實現Reverse()方法並不難,下面是其中一種做法:

代碼 16

使用方法也非常簡單:

代碼 17

你甚至可以把Reverse()方法放到某個靜態類裏,例如Utils,這樣,代碼17就可以變成:

代碼 18

在C# 3.0之前,你最多隻能走到這裏,而到了C# 3.0,你還可以使用擴展方法對它做進一步調整,使代碼18變成:

代碼 19

怎麼樣,看上去就像Reverse()方法是屬於String的,而你所需要做的僅僅是在Reverse()方法的target參數前面加上"this"關鍵字:

代碼 20

我們知道,計算機的底層世界並不知道什麼是面向對象,而我們在對象裏定義的實例方法都包含一個隱藏參數,這個參數就是指向當前對象實例的指針,C# 3.0的擴展方法在形式上模仿了這種做法,但由於擴展方法本質上並不屬於與之相關的類,所以你無法在擴展方法裏訪問類內部的私有成員。

就上面的討論來說,你可能認爲,和代碼18相比,代碼19並沒有太大的優勢,那麼爲什麼需要擴展方法呢?假設我們手頭上有一堆書,我想找到最便宜的LINQ的書,使用標準查詢運算符的話可以這樣寫:

代碼 21

我們知道,Where()、OrderBy()和First()等都是擴展方法,如果C# 3.0不支持擴展方法,那麼代碼21就不得不寫成這樣了:

代碼 22

代碼21的可讀性明顯比代碼22的高,也顯得更自然,而此時我們只是使用了3個標準查詢運算符,你可以想象一下,在沒有擴展方法的支持下要表達更復雜的查詢會是怎樣一番情景?

 

你是如何表達你想要的東西的?

現在,假設我想找到最便宜的LINQ的書,使用C# 2.0的語法,我可能需要這樣:

代碼 23

雖然我已經使用了Array.IndexOf()方法、List<T>.Sort()方法和匿名函數來簡化代碼,但仍然無法掩蓋一個事實,那就是我在講述如何獲取我想要的東西,而這也正是命令式編程(Imperative Programming)的核心思想。

如果使用C# 3.0的語法,情況將會大不一樣:

代碼 24

在這裏,你表達了你想要的東西,而不是獲取這些東西的具體步驟,這是聲明式編程(Declarative Programming)的核心思想,這樣做的好處是明顯的,你的需求可以被重新解析並執行,必要時還可以對底層的實現進行優化,但由於你並不關心和牽扯到具體的實現上,所以那些優化並不會導致你修改代碼。

命令式編程就像過程管理,你深入執行的細節,繼而對整個過程的執行實施控制;而聲明式編程則像目標管理(MBO),你制定目標,並把任務分配下去執行。代碼23給人的感覺就是整個執行過程都非常的清楚,你可以對任何一個步驟進行修改或者調優;而代碼24給人的感覺就是你除了說出你想要什麼,你什麼也不能做,這對於那些過程管理擁戴者來說可能是不可接受的,他們感到對事物失去了控制,無法建立安全感,因而產生了焦慮。曾經有人向我抱怨:如果你使用了LINQ,你就只能迫使自己相信它的實現是很好的。想想看,如果你的公司把飯堂業務承包給一個餐飲公司,你的公司可以插手別人如何招聘廚師、如何採購食物、如何燒菜燒飯嗎?選擇LINQ意味着你願意把執行細節交給別人去處理,從而脫離這些細節,如果你根本無法放下對這些細節的控制,那麼LINQ可能並不適合你。

很難說這兩種編程方式孰優孰劣,因爲在某些場合下,善於過程管理的管理者確實更能讓事態朝正確的方向發展;而在另一些場合下,目標管理爲實現者提供足夠的自由度,更能激勵他們積極地進行思考。管理界對於過程管理和目標管理孰優孰劣之爭論似乎從來沒有停過,更何況編程界對於命令式編程和聲明式編程孰優孰劣之爭論,我個人倒是更傾向於把這看成是找出更適合你自己的風格,而不是盲目聽信別人的說法。語言到底是發揮積極作用還是消極作用在很大程度上是取決於使用者的,我們應該使用語言有利的一面來協助我們的工作,而不是使用其有害的一面來傷害自己和別人。

回到代碼24,它把滿足條件的書的所有信息都返回給我,如果我只需要書名和作者名字呢?我們知道,在面向對象的世界裏,信息儲存在對象裏,於是我們不得不走到一個尷尬的境地,那就是我們要爲此創建一個臨時類:

代碼 25

噩夢正式開始了,如果我需要書名和價格呢?如果我需要書名、作者和價格呢?……(讀者可以自行補全這個列表)這個時候就輪到C# 3.0的匿名類型和隱式類型化變量出場了:

代碼 26

因爲匿名類型是由編譯器自動生成的,而在你寫代碼的時候它還沒有名字,所以你無法用這個類型來聲明這個變量,此時"var"關鍵字就派上用場了。這個是"var"關鍵字的最初目的,但得益於類型推斷系統,我們還可以使用"var"關鍵字聲明任何本地變量,只要我們在聲明的同時給予它初始化,否則編譯器無法進行推斷。曾經有人問我:如果我想返回代碼26裏的wanted7怎麼辦?我們知道,方法的返回值需要明確給出類型,而在我們寫下代碼26時,編譯器還沒有給查詢表達式裏的匿名類型取名。如果你真的要把它返回,你只能把方法的返回值類型定爲IEnumerable<object>,因爲我們只能確定匿名類型是object的後代,但這樣一來,客戶端代碼的日子就不太好過了,因爲除了通過反射來訪問你的對象,它別無他選。如果你真的要把它返回,那就意味着你和客戶端代碼有共享這個對象的需求,此時恰當的做法應該是使用命名類型。另外,代碼26裏構建匿名類型時的"book.Title"是"Title = book.Title"的簡寫,當你省略"Title ="時,編譯器會假定你希望匿名類型的這個屬性的名字和Book.Title的一樣。

匿名類型還有一個有趣的地方,它曾經是可變的(mutable),後來卻變成不可變的(immutable),Sree《Immutable is, the new Anonymous Type》一文中給出了這個轉變的解釋。我們知道,在面向對象的世界裏,對象封裝並維護自身的狀態,我們通過調用對象的方法所產生的副作用來影響對象的狀態,而不可變則是函數式編程(Functional Programming)的核心特徵,或許你已經感受到了,C# 3.0引入了大量函數式編程的東西,而函數式編程語言似乎也要風生水起,這究竟意味着什麼呢?

 

前路在何方?

無論你是否承認,C# 3.0在表達上比它之前的版本要來的簡單,但要獲得這種簡單,你必須先用很多東西武裝自己的腦袋,這使我想起曾經在一本書裏看到的一句話:

簡單是由複雜來支撐的。

不同語言之間的相互滲透已經不再是什麼新奇之事了,引入其它語言的功能有時候甚至可以看作是在戰略上入侵對手的市場,這在某種程度上有點像金融業的混業經營。下一個版本的C#將會是怎樣的呢?或許這個問題令你興奮不已,你甚至希望現在就讓C# Team看看你的創造力;或許這個問題令你痛心不已,你害怕自己無法適應下一波的變革,因爲變革可能導致動盪,動盪可能帶來失控,失控可能引發焦慮。不管怎樣,該來的是無法迴避的,或許現在先讓我們看看Matthew Podwysocki的《What Is the Future of C# Anyways?》是否有一些啓示……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章