在寫這篇文章的時候,C# 已經有了 17 年的歷史了,能夠確定地說它並無去任何地方。C# 語言團隊不斷致力於開發新特性,改善開發人員的體驗。程序員
在這篇文章中,我在介紹 C# 歷史版本的同時分享我最喜歡的特性,在強調實用性的同時展現其優勢。
數據庫
C#1.0 (ISO-1) 確實算是語言,卻沒有什麼使人興奮的,缺乏許多開發人員喜歡的特性。仔細一想,我能說得出喜歡的只有一個特別的特性 - 隱式和顯式接口實現 。編程
接口在現今開發 C# 的過程當中仍然流行使用,如下面的 IDateProvider 接口爲例。api
public interface IDateProvider { DateTime GetDate(); }
沒有什麼特別的,如今着手兩種實現方式 - 其中第一種是隱式實現,以下:數組
public class DefaultDateProvider : IDateProvider { public DateTime GetDate() { return DateTime.Now; } }
第二種實現是以下的顯式實現方式:安全
public class MinDateProvider : IDateProvider { DateTime IDateProvider.GetDate() { return DateTime.MinValue; } }
注意顯式實現如何省略訪問修飾符。此外,方法名稱被寫爲 IDateProvider.GetDate() ,它將接口名稱做爲限定符的前綴。
這兩件事情使得調用更明確的。網絡
顯式接口實現的一個很好的方面是它強制消費者依賴於接口。顯式實現接口的實例對象必須使用接口自己,而沒有其餘可用的接口成員!dom
可是,當您將其聲明爲接口或將此實現做爲指望接口的參數傳遞時,成員將如預期可用。異步
這是特別有用的方面,由於它強制使用接口。經過直接使用接口,不會將代碼耦合到底層實現。一樣,明確的接口實現避免命名或方法簽名歧義 - 並使單個類能夠實現具備相同成員的多個接口。async
Jeffery Richter 在他 CLR via C# 一書中提醒了咱們顯式的接口實現兩個主要問題是值類型實例在投射到一個接口和明確實現的方法時將被裝箱,同時不能被派生類調用。
請記住,裝箱和拆箱會影響性能。任何編程中,你應該評估用例來確保善用工具。
做爲參考,我將列出C# 2.0 (ISO-2) 的全部特性。
我最在最喜歡 泛型 仍是 迭代器 之間的搖擺,對我來講這是一個很是困難的選擇,最終仍是更喜歡泛型,順便說說其中原因。
由於相比於寫迭代器,我更頻繁地使用泛型。在 C# 中不少 SOLID 編程原則都是使用泛型來強化的,一樣它也有助於保持代碼的 乾爽。不要誤解個人意思,我同時也寫了一些迭代器,在 C# 一樣中值得采用!
讓咱們更詳細地看看泛型。
編者注:學習如何 在 C# 中 使用泛型來提升應用程序的可維護性
泛型向.NET Framework引入了類型參數的概念,這使得能夠設計類和方法來推遲一個或多個類型的規範,直到類或方法被客戶端代碼聲明和實例化爲止。
讓咱們想象一下,咱們有一個名爲 DataBag 的類,做爲一個數據包。它可能看起來像這樣:
public class DataBag { public void Add(object data) { // omitted for brevity... } }
起初看起來這彷佛是一個很棒的想法,由於你能夠在這個 DataBag 的實例中添加任何東西。可是當你真正想到這意味着什麼的時候,會以爲至關駭人。
全部添加的內容都隱式地包裝爲 System.Object 。此外,若是添加了值類型,則會發生裝箱。這些是您應該注意的性能考慮事項。
泛型解決了這一切,同時也增長了類型安全性。讓咱們修改前面的例子,在類中包含一個類型參數 T ,並注意方法簽名的變化。
public class DataBag { public void Add(T data) { // omitted for brevity... } }
例如如今一個 DataBag 實例將只容許調用者添加 DateTime 實例。要類型安全,沒有裝箱或拆箱 ... 讓更美好的事情發生。
泛型類型參數也能夠被約束。通用約束是強有力的,由於它們必須遵照相應的約束條件,只容許有限範圍的可用類型參數。
有幾種編寫泛型類型參數約束的方法,請考慮如下語法:
public class DataBag where T : struct { /* T 值類型 */ } public class DataBag where T : class { /* T 類、接口、委託、數組 */ } public class DataBag where T : new() { /* T 有無參構造函數 */ } public class DataBag where T : IPerson { /* T 繼承 IPerson */ } public class DataBag where T : BaseClass { /* T 派生自 BaseClass */ } public class DataBag where T : U { /* T 繼承 U, U 也是一個泛型參數 */ }
多個約束是容許的,用逗號分隔。類型參數約束當即生效,即編譯錯誤阻止程序員犯錯。考慮下面的DataBag約束。
public class DataBag where T : class { public void Add(T value) { // omitted for brevity... } }
如今,若是我試圖實例化DataBag,C#編譯器會讓我知道我作錯了什麼。更具體地說,它要求類型 'DateTime' 必須是一個引用類型,以便將其做爲 'T' 參數用於泛型類型或 'Program.DataBag' 方法中。
下面是C#3.0的主要特性列表。
我徘徊於選擇 Lambda表達式 仍是 擴展方法 。可是,聯繫我目前的 C# 編程,相對於任何其餘的 C# 運算符,我更多地使用lambda 操做符。我沒法表達對它的喜好。
在C#中有不少機會來利用 lambda 表達式和 lambda 運算符。=> lambda 運算符用於將左側的輸入與右側的 lambda 表達式體隔離開來。
一些開發人員喜歡將 lambda 表達式看做是表達委託調用的一種較爲冗長的方式。Action、Func 類型只是 System 名稱空間中的預約義的通常委託。
讓咱們從解決一個假設的問題開始,使用 lambda 表達式來幫助咱們編寫一些富有表現力和簡潔的 C# 代碼。
想象一下,咱們有大量表明趨勢天氣信息的記錄。咱們可能但願對這些數據執行一些操做,不是在一個典型的循環中遍歷它,而是在某個時候,咱們能夠採用不一樣的方式。
public class WeatherData { public DateTime TimeStampUtc { get; set; } public decimal Temperature { get; set; } } private IEnumerable GetWeatherByZipCode(string zipCode) { /* ... */ }
因爲 GetWeatherByZipCode 的調用返回一個 IEnumerable,它可能看起來想讓你循環迭代。假設咱們有一個方法來計算平均溫度。
private static decimal CalculateAverageTemperature( IEnumerable weather, DateTime startUtc, DateTime endUtc) { var sumTemp = 0m; var total = 0; foreach (var weatherData in weather) { if (weatherData.TimeStampUtc > startUtc && weatherData.TimeStampUtc < endUtc) { ++ total; sumTemp += weatherData.Temperature; } } return sumTemp / total; }
咱們聲明一些局部變量來存儲全部過濾日期範圍內的溫度總和及其總和,以便稍後計算平均值。在迭代內是一個 if 邏輯塊,用於檢查天氣數據是否在特定的日期範圍內。能夠重寫以下:
private static decimal CalculateAverageTempatureLambda( IEnumerable weather, DateTime startUtc, DateTime endUtc) { return weather.Where(w => w.TimeStampUtc > startUtc && w.TimeStampUtc w.Temperature) .Average(); }
正如你所看到的那樣,極大地簡化了代碼。if 邏輯塊實際上只是一個謂詞,若是天氣日期在範圍內,咱們將繼續進行一些額外的處理 - 就像一個過濾器。而後就像調用 Average 同樣,當咱們須要合計溫度時,咱們只須要投射 (或選擇) IEnumerable 的溫度過濾列表。
在 IEnumerable 接口上的 Where 和 Select 擴展方法中,使用 lambd a 表達式做爲參數。Where 方法須要一個 Func<T, bool> ,Select 方法 須要一個 Func 。
相比以前的版本,C# 4.0 新增的主要特性較少。
全部這些特性都是很是有用的。可是對於我來講,更傾向於命名可選參數,而不是泛型中的協變和逆變。這二者的取捨,取決於哪一個是我最經常使用的,以及近年來最令 C# 開發人員受益的那個特性。
命名可選參數實至名歸,儘管這是一個很是簡單的特性,其實用性卻很高。我就想問,誰沒有寫太重載或者帶有可選參數的方法?
當您編寫可選參數時,您必須爲其提供一個默認值。若是你的參數是一個值類型,那麼它必須是一個文字或者常數值,或者你可使用 default 關鍵字。一樣,您能夠將值類型聲明爲 Nullable ,並將其賦值爲 null 。假設咱們有一個帶有 GetData 方法的倉儲。
public class Repository { public DataTable GetData( string storedProcedure, DateTime start = default(DateTime), DateTime? end = null, int? rows = 50, int? offSet = null) { // omitted for brevity... } }
正如咱們所看到的,這個方法的參數列表至關長 - 好在有好幾個可選參數。所以,調用者能夠忽略它們,並使用默認值。正如你聲明的那樣,咱們能夠經過只傳遞 storedProcedure 參數來調用它。
var repo = new Repository(); var sales = repo.GetData("sp_GetHistoricalSales");
如今咱們已經熟悉了可選參數特性以及這些特性如何工做,順便使用一下命名參數。以上面的示例爲例,假設咱們只但願咱們的數據表返回 100 行而不是默認的 50 行。咱們能夠將咱們的調用改成包含一個命名參數,並傳遞所需的重寫值。
var repo = new Repository(); var sales = repo.GetData("sp_GetHistoricalSales", rows: 100);
像C#4.0版本同樣,C#5.0版本中沒有太多特性 - 可是其中有一個特性很是強大。
當 C# 5.0 發佈時,它實際上改變了 C# 開發人員編寫異步代碼的方式。今天仍然有不少困惑,我在這裏向您保證,這比大多數人想象的要簡單得多。這是 C# 的一個重大飛躍 - 它引入了一個語言級別的異步模型,它極大地賦予了開發人員編寫外觀和感受同步 (或者至少是連續的) 的「異步」代碼。
異步編程在處理 I/O 相關(如與數據庫、網絡、文件系統等進行交互)時很是強大。異步編程經過使用非阻塞方法幫助處理吞吐量。這種機制在透明的異步狀態機中代以使用暫停點和相應的延續的方式。
一樣,若是 CPU 負載計算的工做量很大,則可能須要考慮異步執行此項工做。這將有助於用戶體驗,由於UI線程不會被阻塞,而是能夠自由地響應其餘UI交互。
編者注:關於 C# 異步編程中使用異步等待的最佳實踐,http://www.dotnetcurry.com/csharp/1307/async-await-asynchronous-programming-examples。
在 C# 5.0 中,當語言添加了兩個新的關鍵字async和await時,異步編程被簡化了。這些關鍵字適用於 Task 和 Task 類型。下表將做爲參考:
Task 和 Task 類型表示異步操做。這些操做既能夠經過返回一個 Task ,也能夠返回void Task。當您使用 async 關鍵字修改返回方法時,它將使方法主體可以使用await 關鍵字。在評估 await 關鍵字時,控制流將返回給調用者,並在該方法中的那一點暫停執行。當等待的操做完成時,會同時恢復執行。
class IOBoundAsyncExample { // Yes, this is the internet Chuck Norris Database of jokes! private const string Url = "http://api.icndb.com/jokes/random?limitTo=[nerdy]"; internal async Task GetJokeAsync() { using (var client = new HttpClient()) { var response = await client.GetStringAsync(Url); var result = JsonConvert.DeserializeObject(response); return result.Value.Joke; } } } public class Result { [JsonProperty("type")] public string Type { get; set; } [JsonProperty("value")] public Value Value { get; set; } } public class Value { [JsonProperty("id")] public int Id { get; set; } [JsonProperty("joke")] public string Joke { get; set; } }
咱們用一個名爲 GetJokeAsync 的方法定義一個簡單的類,當咱們調用方法時,該方法返回一個 Task 。對於調用者,GetJokeAsync 方法最終會給你一個字符串 - 或可能出錯。
當響應返回時,從被暫停的地方恢復延續執行。而後,將結果 JSON 反序列化到 Result類的實例中,並返回 Joke 屬性。
C# 6.0 有不少很不錯的改進,很難選擇我最喜歡的特性。
我把範圍縮小到三個突出的特性:字符串插值,空合併運算符和 nameof 操做符。
儘管 nameof 操做符很棒,並且我常常用,可是顯然另外兩個特性更具影響力。又是一個兩難的選擇,最終仍是字符串插值獲勝出。
空合併運算符頗有用,它能讓我少寫代碼,但不必定防止個人代碼中的錯誤。而使用字符串插值時,能夠防止運行時出錯。
使用 $ 符號插入字符串文字時,將啓用 C# 中的字符串插值語法。至關於告訴 C# 編譯器,咱們要用到各類 C# 變量、邏輯或表達式來插入到此字符串。這對於手動拼接字符串、甚至是 string.Format 方法來講是一個重要的升級。先看一看以下代碼:
class Person { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() => string.Format("{0} {1}", FirstName); }
咱們有一個簡單的 Person 類,具備兩個屬性,表示名字和姓氏。咱們使用 string.Format 重寫 ToString 方法。問題是,編譯時,開發人員在但願將姓氏也做爲結果字符串的一部分時,使用 「{0} {1} 」參數很容易出錯。如上述代碼中,他們忘了加姓氏。一樣,開發人員能夠很容易地交換參數位置,在混亂的格式文字只傳遞了第一個索引,等等...如今考慮用字符串插值實現。
class Person { public string FirstName { get; set; } = "David"; public string LastName { get; set; } = "Pine"; public DateTime DateOfBirth { get; set; } = new DateTime(1984, 7, 7); public override string ToString() => $"{FirstName} {LastName} (Born {DateOfBirth:MMMM dd, yyyy})"; }
我冒昧添加 DateOfBirth 屬性和一些默認的屬性值。另外,咱們如今使用字符串插值重寫 ToString 方法。做爲一名開發人員,犯上述錯誤要困可貴多。最後,我也能夠在插值表達式中進行格式化。注意第三次插值,DateOfBirth 是 DateTime 類型 - 所以咱們可使用習慣的全部標準格式。只需使用 :運算符來分隔變量和格式化。
示例輸出:
編者注:有關C#6.0新特性的詳細內容,請閱讀 http://www.dotnetcurry.com/csharp/1042/csharp-6-new-features
模式匹配、元組和 Out 變量之間,我選擇了 Out 變量。
模式匹配是偉大的,但我真的不以爲本身常用它,至少如今尚未。也許我會在未來更多地使用它,可是到目前爲止我所寫的全部 C# 代碼中,沒有太多的地方能夠運用。再次,這是一個了不得的特性,只不過不是我最喜歡的 C# 7.0 特性。
元組也是一個很好的改進,是服務於語言的這一重要部分,能成爲一等公民真是值得慶祝。逃離了 .Item1,.Item2,.Item3等...的日子,但這麼說不夠準確,在反序列化中沒法還原元組名稱使這個公共 API 不太有用。
我同時不喜歡可變的 ValueTuple 類型。不明白這是誰設計的,但願有人能向我解釋,感受就像是一個疏忽。所以,只有 Out 變量合我心意。
從 C# 版本1.0以來,try-parse 模式已經在各類值類型中出現了。模式以下:
public boolean TryParse(string value, out DateTime date) { // omitted for brevity... }
該函數返回一個布爾值,指示給定的字符串值是否可以被解析。若是爲 true,則將解析後的值分配給 data參數。它使用方式以下:
if (DateTime.TryParse(someDateString, out var date)) { // date is now the parsed value } else { // date is DateTime.MinValue, the default value }
這種模式儘管有用的,卻有點麻煩。有時開發人員採起相同的模式,不管解析是否成功。有時可使用默認值。C# 7.0中的 out變量使得這個更加複雜,儘管我不以爲複雜。
if (DateTime.TryParse(someDateString, out var date)) { // date is now the parsed value } else { // date is DateTime.MinValue, the default value }
如今咱們移除了 if 語句塊的外部聲明,並把聲明做爲參數自己的一部分。使用 var 是合法的,由於類型是已知的。最後,date 變量的範圍沒有改變。它在聲明中內聯回 if 語句塊以前。
你可能會問:「爲何這是我最喜歡的功能之一?」......這種看起來真的沒有什麼變化。
不要懷疑,它使咱們的 C# 代碼更具備表現力。每一個人都喜歡擴展方法吧,那麼請思考如下代碼:
public static class StringExtensions { private delegate bool TryParseDelegate(string s, out T result); private static T To(string value, TryParseDelegate parse) => parse(value, out T result) ? result : default; public static int ToInt32(this string value) => To(value, int.TryParse); public static DateTime ToDateTime(this string value) => To(value, DateTime.TryParse); public static IPAddress ToIPAddress(this string value) => To(value, IPAddress.TryParse); public static TimeSpan ToTimeSpan(this string value) => To(value, TimeSpan.TryParse); }
這個擴展方法類看起來簡潔、明確、強有力。在定義了一個遵循 try-parse 模式的私有委託以後,咱們能夠編寫一個泛型複合方法,它能夠傳遞泛型類型參數、字符串和 tryparse 泛型委託。如今咱們能夠放心地使用這些擴展方法,用法以下:
ublic class Program { public static void Main(string[] args) { var str = string.Join( "", new[] { "James", "Bond", " +7 " }.Select(s => s.ToInt32())); Console.WriteLine(str); // prints "007" } }
編輯注意:要了解C#7的全部新功能,請查看教程 http://www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features
這篇文章對我我的而言頗具挑戰性。C# 的許多特性受我喜歡,所以在每一個版本選出一個最喜歡的特性是很是困難的。
每一個 C# 版本都包含了強大而有影響力的特性。C# 語言團隊以無數的方式進行創新 - 其中之一就是迭代發佈。在撰寫本文時,C#7.1 和 7.2已正式發佈。做爲 C# 開發人員,咱們正在生活在使人激動人心的語言進化時代!
排列出全部特性對我來講是很是有指示,有助於揭示哪些是實際有用的,哪些對我平常影響最大。我會一如既往的努力,成爲務實的開發者!並不是每一種特性對於手頭的工做來講都是必要的,但瞭解什麼是可用的是頗有必要的。
當咱們期待 C# 8 的提議和原型時,我對 C# 的將來感到興奮,它正滿懷信心、積極地試圖減輕 「十億美圓的錯誤」 (譯者注: 圖靈獎得主 Tony Hoare 曾指出空引用將形成十億美圓損失)。