我眼中的C# 3.0

來源:互聯網
上載者:User

我眼中的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?》是否有一些啟示……

 

附:如果你有興趣看看我的簡報,可以點擊這裡下載。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.