在寫這篇文章的時候,C# 已經有了 17 年的歷史了,可以肯定地說它并沒有去任何地方。C# 語言團隊不斷致力于開發新特性,改善開發人員的體驗。 在這篇文章中,我在介紹 C# 歷史版本的同時分享我最喜歡的特性,在強調實用性的同時展示其優點。 C# 1.0 C#1.0 (ISO-1) 確實算是語言,卻沒有什么令人興奮的,缺少許多開發人員喜歡的特性。仔細一想,我能說得出喜歡的只有一個特別的特性 - 隱式和顯式接口實現 。 接口在現今開發 C# 的過程中仍然流行使用,以下面的 IDateProvider 接口為例。
沒有什么特別的,現在著手兩種實現方式 - 其中第一種是隱式實現,如下:
第二種實現是如下的顯式實現方式:
注意顯式實現如何省略訪問修飾符。 此外,方法名稱被寫為 IDateProvider.GetDate() ,它將接口名稱作為限定符的前綴。 這兩件事情使得調用更明確的。 顯式接口實現的一個很好的方面是它強制消費者依賴于接口。顯式實現接口的實例對象必須使用接口本身,而沒有其他可用的接口成員! 但是,當您將其聲明為接口或將此實現作為期望接口的參數傳遞時,成員將如預期可用。 這是特別有用的方面,因為它強制使用接口。通過直接使用接口,不會將代碼耦合到底層實現。同樣,明確的接口實現避免命名或方法簽名歧義 - 并使單個類可以實現具有相同成員的多個接口。 Jeffery Richter 在他 CLR via C# 一書中提醒了我們顯式的接口實現兩個主要問題是值類型實例在投射到一個接口和明確實現的方法時將被裝箱,同時不能被派生類調用。 請記住,裝箱和拆箱會影響性能。任何編程中,你應該評估用例來確保善用工具。 C# 2.0 作為參考,我將列出C# 2.0 (ISO-2) 的所有特性。
我最在最喜歡 泛型 還是 迭代器 之間的搖擺,對我來說這是一個非常困難的選擇,最終還是更喜歡泛型,順便說說其中緣由。 因為相比于寫迭代器,我更頻繁地使用泛型。在 C# 中很多 SOLID 編程(http://www./software-gardening/1365/solid-principles)原則都是使用泛型來強化的,同樣它也有助于保持代碼的 干爽。不要誤解我的意思,我同時也寫了一些迭代器,在 C# 同樣中值得采用! 讓我們更詳細地看看泛型。 注:學習如何 在 C# 中 使用泛型來提高應用程序的可維護性(http://www./patterns-practices/1381/using-generics-csharp-maintainability) 泛型向.NET Framework引入了類型參數的概念,這使得可以設計類和方法來推遲一個或多個類型的規范,直到類或方法被客戶端代碼聲明和實例化為止。 讓我們想象一下,我們有一個名為 DataBag 的類,作為一個數據包。它可能看起來像這樣:
起初看起來這似乎是一個很棒的想法,因為你可以在這個 DataBag 的實例中添加任何東西。但是當你真正想到這意味著什么的時候,會覺得相當駭人。 所有添加的內容都隱式地包裝為 System.Object 。此外,如果添加了值類型,則會發生裝箱。這些是您應該注意的性能考慮事項。 泛型解決了這一切,同時也增加了類型安全性。讓我們修改前面的例子,在類中包含一個類型參數 T ,并注意方法簽名的變化。
例如現在一個 DataBag 實例將只允許調用者添加 DateTime 實例。要類型安全,沒有裝箱或拆箱 ... 讓更美好的事情發生。 泛型類型參數也可以被約束。通用約束是強有力的,因為它們必須遵守相應的約束條件,只允許有限范圍的可用類型參數。 有幾種編寫泛型類型參數約束的方法,請考慮以下語法:
多個約束是允許的,用逗號分隔。類型參數約束立即生效,即編譯錯誤阻止程序員犯錯。考慮下面的DataBag約束。
現在,如果我試圖實例化DataBag,C#編譯器會讓我知道我做錯了什么。更具體地說,它要求類型 ''DateTime'' 必須是一個引用類型,以便將其作為 ''T'' 參數用于泛型類型或 ''Program.DataBag'' 方法中。 C# 3.0 下面是C#3.0的主要特性列表。
我徘徊于選擇 Lambda表達式 還是 擴展方法 。但是,聯系我目前的 C# 編程,相對于任何其他的 C# 運算符(https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/) 我更多地使用Lambda操作符(https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-operator) 。 我無法表達對它的喜愛。在C#中有很多機會來利用 lambda 表達式和 lambda 運算符。=> lambda 運算符用于將左側的輸入與右側的 lambda 表達式體隔離開來。 一些開發人員喜歡將 lambda 表達式看作是表達委托調用的一種較為冗長的方式。Action、Func 類型只是 System 名稱空間中的預定義的一般委托。 讓我們從解決一個假設的問題開始,使用 lambda 表達式來幫助我們編寫一些富有表現力和簡潔的 C# 代碼。 想象一下,我們有大量代表趨勢天氣信息的記錄。我們可能希望對這些數據執行一些操作,不是在一個典型的循環中遍歷它,而是在某個時候,我們可以采用不同的方式。
由于 GetWeatherByZipCode 的調用返回一個 IEnumerable,它可能看起來想讓你循環迭代。假設我們有一個方法來計算平均溫度。
我們聲明一些局部變量來存儲所有過濾日期范圍內的溫度總和及其總和,以便稍后計算平均值。在迭代內是一個 if 邏輯塊,用于檢查天氣數據是否在特定的日期范圍內??梢灾貙懭缦拢?/p>
正如你所看到的那樣,極大地簡化了代碼。if 邏輯塊實際上只是一個謂詞,如果天氣日期在范圍內,我們將繼續進行一些額外的處理 - 就像一個過濾器。然后就像調用 Average 一樣,當我們需要合計溫度時,我們只需要投射 (或選擇) IEnumerable 的溫度過濾列表。 在 IEnumerable 接口上的 Where 和 Select 擴展方法中,使用 lambd a 表達式作為參數。Where 方法需要一個 Func C# 4.0 相比之前的版本,C# 4.0 新增的主要特性較少。
所有這些特性都是非常有用的。但是對于我來說,更傾向于命名可選參數,而不是泛型中的協變和逆變。這兩者的取舍,取決于哪個是我最常用的,以及近年來最令 C# 開發人員受益的那個特性。 命名可選參數實至名歸,盡管這是一個非常簡單的特性,其實用性卻很高。我就想問,誰沒有寫過重載或者帶有可選參數的方法? 當您編寫可選參數時,您必須為其提供一個默認值。如果你的參數是一個值類型,那么它必須是一個文字或者常數值,或者你可以使用 default 關鍵字。同樣,您可以將值類型聲明為 Nullable ,并將其賦值為 null 。假設我們有一個帶有 GetData 方法的倉儲。
正如我們所看到的,這個方法的參數列表相當長 - 好在有好幾個可選參數。因此,調用者可以忽略它們,并使用默認值。正如你聲明的那樣,我們可以通過只傳遞 storedProcedure 參數來調用它。
現在我們已經熟悉了可選參數特性以及這些特性如何工作,順便使用一下命名參數。以上面的示例為例,假設我們只希望我們的數據表返回 100 行而不是默認的 50 行。我們可以將我們的調用改為包含一個命名參數,并傳遞所需的重寫值。
C# 5.0 像C#4.0版本一樣,C#5.0版本中沒有太多特性 - 但是其中有一個特性非常強大。
當 C# 5.0 發布時,它實際上改變了 C# 開發人員編寫異步代碼的方式。今天仍然有很多困惑,我在這里向您保證,這比大多數人想象的要簡單得多。 這是 C# 的一個重大飛躍 - 它引入了一個語言級別的異步模型,它極大地賦予了開發人員編寫外觀和感覺同步 (或者至少是連續的) 的“異步”代碼。 異步編程在處理 I/O 相關(如與數據庫、網絡、文件系統等進行交互)時非常強大。異步編程通過使用非阻塞方法幫助處理吞吐量。這種機制在透明的異步狀態機中代以使用暫停點和相應的延續的方式。 同樣,如果 CPU 負載計算的工作量很大,則可能需要考慮異步執行此項工作。這將有助于用戶體驗,因為UI線程不會被阻塞,而是可以自由地響應其他UI交互。 注:關于 C# 異步編程中使用異步等待的最佳實踐,http://www./csharp/1307/async-await-asynchronous-programming-examples。 在 C# 5.0 中,當語言添加了兩個新的關鍵字async和await時,異步編程被簡化了。這些關鍵字適用于 Task 和 Task 類型。下表將作為參考: Task 和 Task 類型表示異步操作。這些操作既可以通過返回一個 Task ,也可以返回void Task。當您使用 async 關鍵字修改返回方法時,它將使方法主體能夠使用await 關鍵字。 在評估 await 關鍵字時,控制流將返回給調用者,并在該方法中的那一點暫停執行。當等待的操作完成時,會同時恢復執行。
我們用一個名為 GetJokeAsync 的方法定義一個簡單的類,當我們調用方法時,該方法返回一個 Task 。對于調用者,GetJokeAsync 方法最終會給你一個字符串 - 或可能出錯。 當響應返回時,從被暫停的地方恢復延續執行。然后,將結果 JSON 反序列化到 Result類的實例中,并返回 Joke 屬性。 C# 6.0 C# 6.0 有很多很不錯的改進,很難選擇我最喜歡的特性。
我把范圍縮小到三個突出的特性:字符串插值,空合并運算符和 nameof 操作符。 盡管 nameof 操作符很棒,而且我經常用,但是顯然另外兩個特性更具影響力。又是一個兩難的選擇,最終還是字符串插值獲勝出。 空合并運算符很有用,它能讓我少寫代碼,但不一定防止我的代碼中的錯誤。而使用字符串插值時,可以防止運行時出錯。 使用 $ 符號插入字符串文字時,將啟用 C# 中的字符串插值語法。相當于告訴 C# 編譯器,我們要用到各種 C# 變量、邏輯或表達式來插入到此字符串。這對于手動拼接字符串、甚至是 string.Format 方法來說是一個重要的升級。先看一看如下代碼:
我們有一個簡單的 Person 類,具有兩個屬性,表示名字和姓氏。我們使用 string.Format 重寫 ToString 方法。 問題是,編譯時,開發人員在希望將姓氏也作為結果字符串的一部分時,使用 “{0} {1} ”參數很容易出錯。 如上述代碼中,他們忘了加姓氏。同樣,開發人員可以很容易地交換參數位置,在混亂的格式文字只傳遞了第一個索引,等等...現在考慮用字符串插值實現。
我冒昧添加 DateOfBirth 屬性和一些默認的屬性值。另外,我們現在使用字符串插值重寫 ToString 方法。作為一名開發人員,犯上述錯誤要困難得多。 最后,我也可以在插值表達式中進行格式化。注意第三次插值,DateOfBirth 是 DateTime 類型 - 因此我們可以使用習慣的所有標準格式。只需使用 :運算符來分隔變量和格式化。 示例輸出:David Pine (Born July 7, 1984)有關C#6.0新特性的詳細內容,請閱讀 http://www./csharp/1042/csharp-6-new-features C# 7.0
模式匹配、元組和 Out 變量之間,我選擇了 Out 變量。 模式匹配是偉大的,但我真的不覺得自己經常使用它,至少現在還沒有。也許我會在將來更多地使用它,但是到目前為止我所寫的所有 C# 代碼中,沒有太多的地方可以運用。再次,這是一個了不起的特性,只不過不是我最喜歡的 C# 7.0 特性。 元組也是一個很好的改進,是服務于語言的這一重要部分,能成為一等公民真是值得慶祝。逃離了 .Item1,.Item2,.Item3等...的日子,但這么說不夠準確,在反序列化中無法還原元組名稱使這個公共 API 不太有用。 我同時不喜歡可變的 ValueTuple 類型。不明白這是誰設計的,希望有人能向我解釋,感覺就像是一個疏忽。因此,只有 Out 變量合我心意。 從 C# 版本1.0以來,try-parse 模式已經在各種值類型中出現了。模式如下:
該函數返回一個布爾值,指示給定的字符串值是否能夠被解析。如果為 true,則將解析后的值分配給 data參數。它使用方式如下:
這種模式盡管有用的,卻有點麻煩。有時開發人員采取相同的模式,無論解析是否成功。有時可以使用默認值。C# 7.0中的 out變量使得這個更加復雜,盡管我不覺得復雜。
現在我們移除了 if 語句塊的外部聲明,并把聲明作為參數本身的一部分。使用 var 是合法的,因為類型是已知的。最后,date 變量的范圍沒有改變。它在聲明中內聯回 if 語句塊之前。 你可能會問:“為什么這是我最喜歡的功能之一?”......這種看起來真的沒有什么變化。 不要懷疑,它使我們的 C# 代碼更具有表現力。每個人都喜歡擴展方法吧,那么請思考以下代碼:
這個擴展方法類看起來簡潔、明確、強有力。在定義了一個遵循 try-parse 模式的私有委托之后,我們可以編寫一個泛型復合方法,它可以傳遞泛型類型參數、字符串和 tryparse 泛型委托?,F在我們可以放心地使用這些擴展方法,用法如下:
注意:要了解C#7的所有新功能,請查看教程 http://www./csharp/1286/csharp-7-new-expected-features 結論 這篇文章對我個人而言頗具挑戰性。C# 的許多特性受我喜歡,因此在每個版本選出一個最喜歡的特性是非常困難的。 每個 C# 版本都包含了強大而有影響力的特性。C# 語言團隊以無數的方式進行創新 - 其中之一就是迭代發布。在撰寫本文時,C#7.1 和 7.2已正式發布。作為 C# 開發人員,我們正在生活在令人激動人心的語言進化時代! 排列出所有特性對我來說是非常有指示,有助于揭示哪些是實際有用的,哪些對我日常影響最大。我會一如既往的努力,成為務實的開發者!并非每一種特性對于手頭的工作來說都是必要的,但了解什么是可用的是很有必要的。 當我們期待 C# 8 的提議和原型時,我對 C# 的未來感到興奮,它正滿懷信心、積極地試圖減輕 “十億美元的錯誤” (譯者注: 圖靈獎得主 Tony Hoare 曾指出空引用將造成十億美元損失)。 |
|