最新和改進(jìn)的 C# 6.0盡管 C# 6.0 尚未完成,但現(xiàn)在這些功能正處于接近完成的關(guān)鍵時刻。自 2014 年 5 月發(fā)布文章“C# 6.0 語言預(yù)覽版”(msdn.microsoft.com/magazine/dn683793.aspx) 以來,下一版本的 Visual Studio 的 CTP3 版本中對 C# 6.0 進(jìn)行了一些變更和改進(jìn)(代號為“14”)。 本文中,我將介紹各種新功能,并提供針對五月所討論的功能的更新。我還會保留對 C# 6. 每個功能的全面、最新的博客介紹更新。請訪問 /csharp6。其中的很多示例均來自我的“Essential C# 6.0”(Addison-Wesley Professional) 一書的下一版。 Null 條件運算符即使是 .NET 開發(fā)新手,也可能非常熟悉 NullReferenceException。有一個例外是幾乎總是會指出一個 Bug,因為開發(fā)人員在調(diào)用 (null) 對象的成員之前未進(jìn)行充分的 null 檢查。請看看以下示例: public static string Truncate(string value, int length){ string result = value; if (value != null) // Skip empty string check for elucidation { result = value.Substring(0, Math.Min(value.Length, length)); } return result;} 如果不進(jìn)行 null 檢查,此方法會引發(fā) NullReferenceException。盡管這很簡單,但檢查字符串參數(shù)是否為 null 的過程卻稍微有些繁瑣。通常,考慮到比較的頻率,該繁瑣的方法可能沒有必要。C# 6.0 包括一個新的 null 條件運算符,可幫助您更加簡便地編寫這些檢查: public static string Truncate(string value, int length){ return value?.Substring(0, Math.Min(value.Length, length));}[TestMethod]public void Truncate_WithNull_ReturnsNull(){ Assert.AreEqual<string>(null, Truncate(null, 42));}根據(jù) Truncate_WithNull_ReturnsNull 方法所演示的內(nèi)容,如果對象的值實際上為 null,則 null 條件運算符將返回 null。這帶來了一個問題,即 null 條件運算符在調(diào)用鏈中出現(xiàn)時會是什么情況?如以下示例中所示: public static string AdjustWidth(string value, int length){ return value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);}[TestMethod]public void AdjustWidth_GivenInigoMontoya42_ReturnsInigoMontoyaExtended(){ Assert.AreEqual<int>(42, AdjustWidth('Inigo Montoya', 42).Length);} 盡管 Substring 是通過 null 條件運算符進(jìn)行調(diào)用的,并且 null value?.Substring 似乎返回了 null,但語言行為按您的想法進(jìn)行。這簡化了對 PadRight 的調(diào)用過程,并立即返回 null,從而避免會導(dǎo)致出現(xiàn) NullReferenceException 的編程錯誤。這個概念稱為“null 傳播”。 Null 條件運算符會根據(jù)具體條件進(jìn)行 null 檢查,然后再調(diào)用目標(biāo)方法以及調(diào)用鏈中的所有其他方法。這將可能產(chǎn)生一個令人驚訝的結(jié)果,例如,text?.Length.GetType 語句中的結(jié)果。 如果 null 條件運算符在調(diào)用目標(biāo)為 null 時返回 null,那么調(diào)用會返回值類型的成員時最終會是什么數(shù)據(jù)類型(假定值類型不能為 null)?例如,從 value?.Length 返回的數(shù)據(jù)類型不能只是 int。答案當(dāng)然是:可以為 null 的類型(int?)。實際上,嘗試僅將結(jié)果分配給 int 將會出現(xiàn)編譯錯誤: int length = text?.Length; // Compile Error: Cannot implicitly convert type 'int?' to 'int' Null 條件具有兩種語法形式。首先,問號在點運算符前面 (?.)。其次,將問號和索引運算符結(jié)合使用。例如,給定一個集合(而非在索引到集合之前顯式進(jìn)行 null 檢查),您就可以使用 null 條件運算符執(zhí)行此操作: public static IEnumerable<T> GetValueTypeItems<T>( IList<T> collection, params int[] indexes) where T : struct{ foreach (int index in indexes) { T? item = collection?[index]; if (item != null) yield return (T)item; }} 此示例使用了運算符 ?[…] 的 null 條件索引形式,導(dǎo)致僅在集合不為 null 時才索引到集合。通過 null 條件運算符的此形式,T? item = collection?[index] 語句在行為上相當(dāng)于: 請注意,null 條件運算符僅可檢索項目,不會分配項目。如果給定 null 集合,那么這意味著什么? 請注意針對引用類型使用 ?[…] 時的隱式歧義。由于引用類型可以為 null,因此對于集合是否為 null,或者是否元素本身實際上就是 null 而言,來自 ?[…] 運算符的 null 結(jié)果不明確。 Null 條件運算符的一個非常有用的應(yīng)用程序解決了 C# 自 C# 1.0 以來一直存在的的一個特性,即在調(diào)用委托之前檢查是否為 null。我們來看一下圖 1 中顯示的 C# 2.0 代碼。 圖 1 在調(diào)用委托之前檢查是否為 Null class Theremostat{ event EventHandler<float> OnTemperatureChanged; private int _Temperature; public int Temperature { get { return _Temperature; } set { // If there are any subscribers, then // notify them of changes in temperature EventHandler<float> localOnChanged = OnTemperatureChanged; if (localOnChanged != null) { _Temperature = value; // Call subscribers localOnChanged(this, value); } } }} 通過使用 null 條件運算符,整個 set 實現(xiàn)過程就可簡化為: 現(xiàn)在,您只需對將 null 條件運算符作為前綴的 Invoke 進(jìn)行調(diào)用,不再需要將委托實例分配給本地變量,從而實現(xiàn)線程安全,甚至是在調(diào)用委托之前顯式檢查值是否為 null。 C# 開發(fā)人員都很想知道在最新的四個版本中是否對此內(nèi)容有所改進(jìn)。答案是最終進(jìn)行了改進(jìn)。僅此一項功能就可以改變調(diào)用委托的方式。 另一個 null 條件運算符普及的常見模式是與 coalesce 運算符結(jié)合使用。您無需在調(diào)用 Length 之前對 linesOfCode 進(jìn)行 null 檢查,而是可以編寫項目計數(shù)算法,如下所示: List<string> linesOfCode = ParseSourceCodeFile('Program.cs');return linesOfCode?.Count ?? 0; 在這種情況下,任何空集合(無項目)和 null 集合均標(biāo)準(zhǔn)化為返回相同數(shù)量??傊?,null 條件運算符將實現(xiàn)以下功能:
自動屬性初始化表達(dá)式有過正確實現(xiàn)結(jié)構(gòu)經(jīng)驗的所有 .NET 開發(fā)人員無疑都為一個問題所困擾:需要使用多少語法才能使類型固定不變(為 .NET 標(biāo)準(zhǔn)建議的類型)。此問題實際上是只讀屬性存在的問題:
所有這一切僅僅是為了“正確地”實現(xiàn)固定不變的屬性。之后,此情況還會針對類型的所有屬性重復(fù)發(fā)生。因此,正確操作需要比不堪一擊的方法付出明顯更多的努力。發(fā)布了自動屬性初始化表達(dá)式(CTP3 還包括對初始化表達(dá)式的支持)這個新功能后,C# 6.0 就可派上用場了。自動屬性初始化表達(dá)式允許直接在屬性的聲明內(nèi)分配屬性。對于只讀屬性,它負(fù)責(zé)確保屬性固定不變所需的所有繁瑣程序。例如,請看本示例中的 FingerPrint 類: public class FingerPrint{ public DateTime TimeStamp { get; } = DateTime.UtcNow; public string User { get; } = System.Security.Principal.WindowsPrincipal.Current.Identity.Name; public string Process { get; } = System.Diagnostics.Process.GetCurrentProcess().ProcessName;}如代碼所示,屬性初始化表達(dá)式允許向?qū)傩苑峙湟粋€初始值作為屬性聲明的一部分。屬性可以是只讀的(只包含 getter),也可以是讀/寫(包含 setter 和 getter)的。如果是只讀的,則基礎(chǔ)支持字段將通過只讀修飾符自動聲明。這就確保了在初始化之后會固定不變。 初始化表達(dá)式可以是任意表達(dá)式。例如,通過使用條件運算符,您可以設(shè)置默認(rèn)初始化值: public string Config { get; } = string.IsNullOrWhiteSpace( string connectionString = (string)Properties.Settings.Default.Context?['connectionString'])? connectionString : '<none>'; 本示例中,請注意之前的文章中所討論的如何使用聲明表達(dá)式(請參閱 /?p=4040)。如果您需要的不只是表達(dá)式,可以將初始化重構(gòu)到靜態(tài)方法中,然后對其進(jìn)行調(diào)用。 Nameof 表達(dá)式CTP3 版本中介紹的另一個新增功能是支持 nameof 表達(dá)式。您將多次需要在代碼中使用“魔幻字符串”。此類“魔幻字符串”是映射到您代碼中的程序元素的普通 C# 字符串。例如,引發(fā) ArgumentNullException 時,使用一個字符串表示無效對應(yīng)參數(shù)的名稱。遺憾的是,這些魔幻字符串未經(jīng)過編譯時驗證,任意程序元素更改(例如,重命名參數(shù))都不會自動更新魔幻字符串,從而導(dǎo)致不一致,而編譯器根本不會發(fā)現(xiàn)此問題。 在其他情況下,例如引發(fā) OnPropertyChanged 事件時,可以通過提取名稱的樹表達(dá)式技術(shù)避免出現(xiàn)魔幻字符串。鑒于操作簡單(只識別程序元素名稱),所以這或許有點讓人頭疼。無論哪種情況,解決方案都不太理想。 若要解決這一特性,C# 6.0 提供了對“程序元素”名稱的訪問權(quán)限,無論是類名稱、方法名稱、參數(shù)名稱還是特定屬性名稱(可能是對于使用反射的情況)。例如,圖 2 中的代碼使用 nameof 表達(dá)式提取參數(shù)的名稱。 圖 2 使用 Nameof 表達(dá)式提取參數(shù)名稱 void ThrowArgumentNullExceptionUsingNameOf(string param1){ throw new ArgumentNullException(nameof(param1));}[TestMethod]public void NameOf_UsingNameofExpressionInArgumentNullException(){ try { ThrowArgumentNullExceptionUsingNameOf('data'); Assert.Fail('This code should not be reached'); } catch (ArgumentNullException exception) { Assert.AreEqual<string>('param1', exception.ParamName);}正如測試方法所演示的,ArgumentNullException 的 ParamName 屬性具有 param1 值,這是使用方法中的 nameof(param1) 表達(dá)式的值集。Nameof 表達(dá)式不僅僅用于參數(shù),您還可以使用它來檢索所有編程元素,如圖 3 中所示。 圖 3 檢索其他編程元素 namespace CSharp6.Tests{ [TestClass] public class NameofTests { [TestMethod] public void Nameof_ExtractsName() { Assert.AreEqual<string>('NameofTests', nameof(NameofTests)); Assert.AreEqual<string>('TestMethodAttribute', nameof(TestMethodAttribute)); Assert.AreEqual<string>('TestMethodAttribute', nameof( Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute)); Assert.AreEqual<string>('Nameof_ExtractsName', string.Format('{0}', nameof(Nameof_ExtractsName))); Assert.AreEqual<string>('Nameof_ExtractsName', string.Format('{0}', nameof( CSharp6.Tests.NameofTests.Nameof_ExtractsName))); } }} Nameof 表達(dá)式僅檢索最終的標(biāo)識符,即使您使用更多的顯式包含點的名稱也是如此。此外,對于屬性而言,未隱含“Attribute”后綴。相反,編譯需要它。它非常適合用于清理混亂代碼。 主構(gòu)造函數(shù)自動屬性初始化表達(dá)式尤其適合與主構(gòu)造函數(shù)結(jié)合使用。主構(gòu)造函數(shù)為降低常見對象模式的繁瑣程度提供了一種方法。此功能自五月以來已顯著改進(jìn)。更新包括:
隨著 Web 服務(wù)、多層應(yīng)用程序、數(shù)據(jù)服務(wù)、Web API、JSON 及類似技術(shù)的普遍使用,類的一個普遍形式是數(shù)據(jù)傳輸對象 (DTO)。DTO 通常不會實現(xiàn)太多功能,而是專注于使數(shù)據(jù)存儲簡單化。它對于簡單性的關(guān)注使得主構(gòu)造函數(shù)極具新引力。例如,請看本示例中所示的固定 Pair 數(shù)據(jù)結(jié)構(gòu): struct Pair<T>(T first, T second){ public T First { get; } = first; public T Second { get; } = second; // Equality operator ...}構(gòu)造函數(shù)定義 Pair(string first, string second) 已合并到類聲明中。這會將構(gòu)造函數(shù)參數(shù)指定為 first 和 second(均為類型 T)。屬性初始化表達(dá)式中也引用了這些參數(shù),并將參數(shù)分配給其對應(yīng)的屬性。當(dāng)您發(fā)現(xiàn)此類定義的簡單性、對不變性的支持以及必不可少的構(gòu)造函數(shù)(所有屬性/字段的初始化表達(dá)式)時,您將會了解到它是如何幫助您正確編寫代碼的。這將導(dǎo)致先前需要不必要的詳細(xì)級別的常見模式得到顯著改進(jìn)。 主構(gòu)造函數(shù)主體指定對主構(gòu)造函數(shù)執(zhí)行的操作。這將有助于您在主構(gòu)造函數(shù)上實現(xiàn)通常在構(gòu)造函數(shù)上可以實現(xiàn)的等同功能。例如,改進(jìn) Pair<T> 數(shù)據(jù)結(jié)構(gòu)的可靠性的下一個步驟可能是提供屬性驗證。此類驗證可以確保 Pair.First 的 null 值將無效。現(xiàn)在,CTP3 包括一個主構(gòu)造函數(shù)主體(未聲明的構(gòu)造函數(shù)主體),如圖 4 中所示。 圖 4 實現(xiàn)主構(gòu)造函數(shù)主體 struct Pair<T>(T first, T second){ { if (first == null) throw new ArgumentNullException('first'); First = first; // NOTE: Not working in CTP3 } public T First { get; }; // NOTE: Results in compile error for CTP3 public T Second { get; } = second; public int CompareTo(T first, T second) { return first.CompareTo(First) + second.CompareTo(Second); }// Equality operator ...} 為清晰起見,我將主構(gòu)造函數(shù)主體置于類的第一個成員中。但這不是 C# 所要求的。主構(gòu)造函數(shù)主體可以按與其他類成員相關(guān)的任意順序顯示。 只讀屬性的另一個功能盡管在 CTP3 中沒有發(fā)揮作用,但您可以從構(gòu)造函數(shù)內(nèi)直接分配這些屬性(例如,F(xiàn)irst = first)。這不僅僅限于主構(gòu)造函數(shù),而且還適用于所有構(gòu)造函數(shù)成員。 支持自動屬性初始化表達(dá)式的一個有趣的結(jié)果是,它解決了早期版本中出現(xiàn)的需要顯式字段聲明的多種情況。它沒有解決一個最明顯的問題,即需要對 setter 進(jìn)行驗證的情況。另一方面,幾乎已不需要聲明只讀字段?,F(xiàn)在,無論何時聲明只讀字段,只要需要該封裝級別,您都可以將只讀自動屬性聲明為私有。 CompareTo 方法具有參數(shù) first 和 second,這好像與主構(gòu)造函數(shù)的參數(shù)名稱重復(fù)。由于主構(gòu)造函數(shù)名稱在自動屬性初始化表達(dá)式作用域內(nèi),因此,first 和 second 似乎并不明確。幸運的是,實際情況并非如此。作用域規(guī)則將依據(jù)不同維度而定,而您之前在 C# 中并未看到。 在 C# 6.0 之前,作用域始終由代碼內(nèi)的變量聲明放置來確定。參數(shù)在其幫助聲明的方法中綁定,字段在類中綁定,在 if 語句中聲明的變量由 if 語句條件主體綁定。 相比之下,主構(gòu)造函數(shù)參數(shù)則由時間來綁定。主構(gòu)造函數(shù)參數(shù)僅在執(zhí)行主構(gòu)造函數(shù)時為“活動”狀態(tài)。此時間范圍在主構(gòu)造函數(shù)主體的情況中很明顯??赡軐τ谧詣訉傩猿跏蓟磉_(dá)式的情況不太明顯。 但是,與作為 C# 1.0+ 中的類初始化表達(dá)式的一部分執(zhí)行的轉(zhuǎn)換為語句的字段初始化表達(dá)式類似,自動屬性初始化表達(dá)式也通過同樣的方式實現(xiàn)。換言之,主構(gòu)造函數(shù)參數(shù)的作用域與類初始化表達(dá)式和主構(gòu)造函數(shù)主體的生命周期綁定。在自動屬性初始化表達(dá)式或主構(gòu)造函數(shù)主體外部對主構(gòu)造函數(shù)參數(shù)進(jìn)行任何引用都將產(chǎn)生編譯錯誤。 還有其他幾個與主構(gòu)造函數(shù)相關(guān)的概念需要牢記。只有主構(gòu)造函數(shù)可以調(diào)用基構(gòu)造函數(shù)。您可以使用主構(gòu)造函數(shù)聲明后跟的基本(上下文)關(guān)鍵字執(zhí)行此操作: class UsbConnectionException( string message, Exception innerException, HidDeviceInfo hidDeviceInfo): Exception (message, innerException){ public HidDeviceInfo HidDeviceInfo { get; } = hidDeviceInfo;}如果指定其他構(gòu)造函數(shù),則構(gòu)造函數(shù)調(diào)用鏈必須最后調(diào)用主構(gòu)造函數(shù)。這意味著主構(gòu)造函數(shù)不能具有此初始化表達(dá)式。假定主構(gòu)造函數(shù)也不是默認(rèn)構(gòu)造函數(shù),所有其他構(gòu)造函數(shù)必須具有這些初始化表達(dá)式: public class Patent(string title, string yearOfPublication){ public Patent(string title, string yearOfPublication, IEnumerable<string> inventors) ...this(title, yearOfPublication) { Inventors.AddRange(inventors); }} 希望這些示例有助于展示主構(gòu)造函數(shù)簡化了 C#。通過主構(gòu)造函數(shù),還有機(jī)會以簡單的方式來執(zhí)行簡單的任務(wù),而不是以復(fù)雜的方式來執(zhí)行簡單的任務(wù)。它偶爾也會讓類包含多個主構(gòu)造函數(shù)和調(diào)用鏈,致使代碼不易于閱讀。如果您遇到主構(gòu)造函數(shù)語法使代碼看起來更為復(fù)雜而不是簡化代碼的情況,那么請不要使用主構(gòu)造函數(shù)。對于 C# 6.0 的所有增強(qiáng)功能,如果您有不喜歡的功能,或某個功能使您的代碼不易于閱讀,請不要使用該功能。 表達(dá)式主體函數(shù)和表達(dá)式屬性表達(dá)式主體函數(shù)是 C# 6.0 中的另一個語法精簡形式。有一些函數(shù)不包括語句體,而是以函數(shù)聲明后跟表達(dá)式的形式來實現(xiàn)。 例如,可以這樣向 Pair<T> 類添加 ToString 方法的重寫: 表達(dá)式主體函數(shù)沒有什么徹底更改。和 C# 6.0 中的大部分功能一樣,它們旨在提供簡化的語法,用于實現(xiàn)簡單的情況。當(dāng)然,表達(dá)式的返回類型必須與函數(shù)聲明中定義的返回類型相匹配。在這種情況下,ToString 將返回一個字符串,這同函數(shù)實現(xiàn)表達(dá)式返回的結(jié)果一樣。返回 void 或 Task 的方法應(yīng)通過同樣不會返回任何結(jié)果的表達(dá)式實現(xiàn)。 表達(dá)式主體簡化不僅僅限于函數(shù),您還可以使用表達(dá)式實現(xiàn)只讀(僅包含 getter)屬性——表達(dá)式屬性。例如,可以將 Text 成員添加到 FingerPrint 類: public string Text => string.Format('{0}: {1} - {2} ({3})', TimeStamp, Process, Config, User); 其他功能有一些功能不再計劃針對 C# 6.0 實現(xiàn):
var cppHelloWorldProgram = new Dictionary<int, string>{[10] = 'main() {',[20] = ' printf(\'hello, world\')',[30] = '}'};
有一些功能未在本文中進(jìn)行討論,因為這些功能已涵蓋在五月的文章中,但靜態(tài) using 語句(請參閱 /?p=4038)、聲明表達(dá)式(請參閱 /?p=4040,以及異常處理改進(jìn)(請參閱 /?p=4042)這些功能保持不變。 總結(jié)顯然,開發(fā)人員對 C# 興趣盎然,希望確保其一如既往地保持卓越的功能。語言團(tuán)隊會非常認(rèn)真地對待您的反饋,并根據(jù)用戶反饋對語言進(jìn)行適當(dāng)修改。歡迎訪問 roslyn.,并將您的想法告知團(tuán)隊。此外,請記得查看 /csharp6 來了解 C# 6.0 發(fā)布后的更新。 Mark Michaelis 是 IntelliTect 的創(chuàng)始人。他還擔(dān)任首席技術(shù)架構(gòu)師和培訓(xùn)師。自 1996 年以來,他就成為 Microsoft C#、Visual Studio Team System (VSTS) 和 Windows SDK 方面的 MVP。他在 2007 年就任 Microsoft 區(qū)域總監(jiān)。另外,他還是多個 Microsoft 軟件設(shè)計評審團(tuán)隊(包括 C#、連通系統(tǒng)部門和 VSTS)的成員。Michaelis 在開發(fā)人員大會上發(fā)表講話,還撰寫了許多文章和書籍,目前正在撰寫新一版的《Essential C#》(C# 本質(zhì)論)(Addison-Wesley Professional)。 衷心感謝以下 Microsoft 技術(shù)專家對本文的審閱:Mads Torgersen |
|
|