2018 年 2 月

第 33 卷,第 2 期

本文章是由機器翻譯。

Essential .NET - C# 8.0 與可為 Null 的參考型別

標記 Michaelis |2018 年 2 月

可為 null 的參考類型 — 什麼?沒有可為 null 的所有參考型別嗎? 

我最喜歡的 C#,而小心語言設計棒。然而,因為目前狀態,且即使 7 的 C# 版本中,我們仍然沒有完美的語言。我是說來,雖然並不合理預期可能永遠會有新功能,可將加入至 C#,另外還有,不幸的是,某些問題。我不是指 bug 的問題,但是,而基本發出。可能是下列其中一個最大問題區域 —,另一個則自 C# 1.0 — 括住,參考型別可以是 null,事實上,參考型別都是 null 預設事實。以下是一些可為 null 參考型別為何不盡理想的原因:

  • 叫用 null 值上的成員將會發行 System.NullReferenceException 例外狀況和一個錯誤是 System.NullReferenceException 在實際執行程式碼會導致每次叫用。不幸的是,不過,可為 null 參考型別與我們"落在 」 動作的錯誤項目,而不是正確的事。「 落在 」 動作是叫用參考類型,而不檢查為 null。
  • 沒有參考類型和實值類型 (按照 Nullable < T > 簡介) 不一致,實值型別可為 null 時以裝飾"?"(例如 int? 數目);否則,它們預設為不可以是 null。相反地,參考型別會預設為 null。這是 「 標準 」 之具有已長時間,以 C# 撰寫的任何人,但如果我們無法執行過,我們會想要預設為非 null 的參考型別,加"?"是明確的方式,允許 null。
  • 您不可能執行靜態流程分析,以檢查是否值將會是 null 之前取值,或不相關的所有路徑。例如,考慮,如果沒有 unmanaged 程式碼引動過程,多執行緒處理,或 null 指派/取代根據執行階段的條件。(不以提及分析是否會包含所有程式庫會叫用的應用程式開發介面檢查。)
  • 沒有任何合理的語法表示 null 參考型別值無效的特定宣告。
  • 沒有任何方法裝飾參數,允許 null。

為已經前述,即便這,我喜歡在 C# idiosyncrasy 的 C# 為只接受 null 的行為的點。使用 C# 8.0,不過,C# 語言小組設定就區分,用意在於改善。具體而言,他們希望執行下列作業:

  • 提供 null 所預期的語法:讓開發人員明確地識別時參考型別必須包含 null,而且,因此,不是明確的旗標情況下會被指派 null。
  • 請預期不可為 null 的預設參考型別:變更預設期望的所有參考型別不可為 null,但這樣做的選擇加入編譯器參數而突然不勝負荷開發人員與現有的程式碼的警告。
  • 減少 NullReferenceExceptions 的相符項目:改善的靜態流程分析潛在的情況下其中尚未明確檢查值,null 叫用的其中一個成員的值之前加上旗標,來降低 NullReferenceException 例外狀況的可能性。
  • 啟用此功能的流程靜態分析警告:支援某種形式的 「 信任 me,我是程式設計人員 」,可讓開發人員覆寫編譯器的靜態流程分析並隱藏可能 NullReferenceException 的任何警告,因此,宣告。

發行項的其餘部分,讓我們看每個這些目標和 C# 8.0 如何實作基本支援為其 C# 語言內。

提供對預期會發生 Null 語法

若要開始,需要有區分時應該 null,而且當它不應該參考類型的語法。允許 null 的明顯語法使用嗎?可為 null 的宣告,實值類型和參考型別。藉由參考型別上的支援,開發人員提供的方法中,選擇 null,例如:

string? text = null;

這種語法新增說明為何可為 null 的重大改進摘要說明看似令人混淆的名稱,而 「 可為 null 的參考類型 」。 它不是因為某些新的資料類型可為 null 參考,但相反地,現在明確 — 選擇加入 — 支援該的資料類型。

什麼是不可為 null 的參考類型語法指定可為 null 的參考類型語法?  雖然這:

string! text = "Inigo Montoya"

可能看起來好的選擇,同時也會造成的問題所謂的只是:

string text = GetText();

我們還有有三個宣告,也就是: 可為 null 參考型別、 不可為 null 參考型別和--不知參考型別嗎?啊,No!!

相反地,我們事實上想是:

  • 可為 null 參考型別: 字串嗎?文字 = null。
  • 不可為 null 參考型別: 字串文字 ="Inigo Montoya"

這表示,當然中斷語言變更,這樣以任何修飾詞的參考型別是不可為 null 的預設值。

請預期不可為 Null 的預設值參考類型

切換為非 null 的標準參考宣告 (沒有可為 null 的修飾詞) 可能是最困難的減少 idiosyncrasy 可為 null 的所有需求。事實是該今日,字串文字。在參考類型,稱為 「 允許為 null 的文字,預期為 null,文字的文字,而且實際上是預設文字結果 null 在許多情況下,這類欄位或陣列。不過,如同實值類型,允許 null 的參考型別應該是例外狀況,沒有預設值。如果我們將 null 指派給文字或無法初始化 null 以外的文字,編譯器會加上旗標 (編譯器已加上旗標取值的本機變數,在初始化之前) 的文字任何的變數取值時,它可能會偏好。

不幸的是,這表示變更語言,並發出警告,當您指派 null (字串文字 = null,例如),或指派可為 null 的參考類型 (例如字串? 文字 = null,則字串 moreText = 文字;)。第一個 (字串文字 = null) 是一項重大變更。(如先前所造成的任何警告的項目發出警告是中斷變更)。  若要可避免讓開發人員,但出現警告,只要他們開始使用 C# 8.0 編譯器,改為 null 屬性將會關閉支援根據預設,因此沒有重大變更。若要利用.net framework,因此,您將需要選擇中啟用此功能。(不過請注意,在撰寫本文時,當時可用的預覽itl.tc/csnrtp,null 屬性預設為開啟。)

當然,一旦啟用此功能,將會出現警告,選擇呈現給您。選擇明確參考型別要或不允許空值。如果不存在,然後移除 null 指派,因此移除此警告。不過,這可能會導致警告稍後在因為未指派給變數,您必須將它指派非 null 值。或者,如果 null 明確 (設定代表 「 不明 」,例如),然後變更為可為 null,在宣告的型別:

string? text = null;

減少 NullReferenceExceptions 的相符項目

指定的方法宣告為可為 null 或不可為 null 的型別,這是現在由編譯器的靜態流程分析,以判斷可能違反宣告時。宣告為可為 null 參考型別或避免 null 指派至非 null 的型別也能運作,而新的警告或錯誤可能會出現在稍後在程式碼中。為先前所提到的、 不可為 null 參考型別會造成錯誤稍後在程式碼如果從未指派本機變數 (這是 C# 8.0 之前的本機變數,則為 true)。靜態流程分析相較之下,標示任何取值 (dereference) 的 null,它無法偵測到先前的檢查是否有 null 的型別引動過程和/或為 null 以外的值可為 null 值的任何指派。圖 1 顯示一些範例。

圖 1 範例靜態的資料流程分析結果

string text1 = null;
// Warning: Cannot convert null to non-nullable reference
string? text2 = null;
string text3 = text2;
// Warning: Possible null reference assignment
Console.WriteLine( text2.Length ); 
// Warning: Possible dereference of a null reference
if(text2 != null) { Console.WriteLine( text2.Length); }
// Allowed given check for null

無論如何,最終結果是減少潛在 NullReferenceExceptions 使用靜態流程分析以確認可為 null 的意圖。

如同先前討論,靜態流程分析應該加上旗標不可為 null 的型別可能會指派 null 時,請直接或當指派 null 的型別。不幸的是,這並不容易。例如,如果方法宣告,它會傳回非可為 null 的參考類型 (可能是程式庫尚未尚未更新具有 null 屬性修飾詞),或會錯誤地傳回 null (可能是已忽略警告) 或非嚴重例外狀況發生和預期的指派不會執行,還是有可能,不可為 null 參考型別無法以結束的 null 值。不幸的但支援針對可為 null 參考型別應該降低可能性,擲回 NullReferenceException,但不將它消除。(這是類似於與錯誤的編譯器的核取時將變數指派)。 同樣地,靜態流程分析永遠不會辨識的程式碼,事實上,並檢查有 null 之前取值的值。事實上,流程分析只會檢查區域變數和參數,方法主體內的 null 屬性,並可運用方法和運算子的簽章,以判斷有效性。它不是,比方說,深入稱為 IsNullOrEmpty 執行分析上是否該方法成功會檢查有 null 的任何其他的 null 檢查是必要的方法主體。

啟用此功能的流程靜態分析警告

指定可能與錯誤的靜態流程分析中,如果您的簽入 null (可能與例如物件的呼叫。ReferenceEquals (s,null) 或字串。編譯器無法辨識 IsNullOrEmpty()) 嗎?當程式設計人員知道更好,我值不可以 null,它們可以在取值 (dereference) 下列 !運算子 (例如,文字 !) 中:

string? text;...
if(object.ReferenceEquals(text, null))
{  var type = text!.GetType()
}

不含驚嘆號,編譯器會警告可能的 null 引動過程。同樣地,將可為 null 的值指派給不可為 null 值時可以裝飾帶有驚嘆號通知程式設計人員,您知道編譯器指派的值:

string moreText = text!;

如此一來,您可以覆寫靜態流程分析方式就如同您可以使用明確的轉換。當然,在執行階段仍會發生適當的驗證。

總結

引進的參考類型的 null 屬性修飾詞不會導入新的類型。參考類型則仍可為 null,且編譯字串嗎?產生的 IL,仍然只 System.String。在 IL 層級的差異是可為 null 的修改類型的裝飾,使用的屬性:

System.Runtime.CompilerServices.NullableAttribute

如此一來,下游編譯可以繼續利用宣告的意圖。此外,假設該屬性可用的先前版本的 C#,仍然可以參考 C# 8.0 編譯的程式庫 — 雖然沒有任何 null 屬性增強功能。最重要的是,這表示現有應用程式開發介面 (例如.NET API) 可以更新使用可為 null 的中繼資料,而不會中斷應用程式開發介面。此外,這表示沒有任何多載的 null 屬性修飾詞為基礎的支援。

沒有為增強 null 處理 C# 8.0 中的一種不幸結果。不可為 null 之傳統上為 null 宣告轉換,將一開始導入大量的警告。雖然這很不幸,我認為已受到維護合理的平衡之間不滿,以及改善的其中一個是程式碼:

  • 因為值不再 null 時,它不應該會警告您可能會移除 null 指派的非 null 的型別排除 bug。
  • 或者,加入可為 null 的修飾詞會提升您的程式碼透過更明確了解您的意圖。
  • 經過一段時間會溶解可為 null 的已更新程式碼和較舊的程式碼之間的阻抗不相符,減少 NullReferenceException bug,用來進行。
  • Null 屬性功能預設為關閉上現有的專案,因此您可能會延遲處理方式,直到您選擇的時間。最後,您會有更穩固程式碼。您知道更好的情況下於編譯器中,您可以使用 !運算子 (宣告,"信任,我是程式設計人員。 」) 要轉型。

在 C# 8.0 進一步增強功能

有三個主要的其他部分的列入考量適用於 C# 8.0 的增強功能:

非同步資料流:支援非同步資料流可讓等候語法來反覆查看集合的工作 (工作 < bool >)。例如,您可以叫用

foreach await (var data in asyncStream)

並不會封鎖 await,任何陳述式的執行緒,但會改為 「 繼續 」 它們反覆運算完成之後。和迭代器,將會產生下一個項目,後面接著呼叫 T 目前 {get;} (要求的可列舉資料流迭代器是 < bool > MoveNextAsync 工作的引動過程) 要求。

預設介面實作:使用 C# 中,您可以實作多個介面的每個介面的簽章會繼承。此外,它是使所有的衍生的類別都有成員的預設實作,提供基底類別中的成員實作。不幸的是,不可能是實作多個介面並且也會提供介面的預設實作,也就是多重繼承。引入的預設介面實作,與我們克服這項限制。假設可能合理的預設實作,使用 C# 8.0 您可以包含預設成員實作 (屬性和方法只) 和實作介面的所有類別都有預設實作。雖然多重繼承可能會有好處,這會提供實際改善的幅度延伸與其他成員的介面不會引入重大 API 變更的能力。您可以比方說,計數將方法加入至 IEnumerator < T > (雖然實作它需要逐一查看集合中的所有項目) 而不會中斷所有實作介面的類別。請注意,此功能需要對應的 framework 版本 (項目尚未自 C# 2.0 和泛型後需要)。

延伸模組的所有項目:使用 LINQ 提供擴充方法的簡介。I 請注意需要當時 Anders Hejlsberg 與 dinner 詢問有關其他擴充功能類型,例如其內容。Mr.Hejlsberg 通知我的小組已只考慮情況所需的實作 LINQ。現在,10 年之後,假設正在重新評估,而且它們會考量擴充方法不只有屬性,但也事件、 運算子和建構函式 (後者開啟一些有趣的處理站模式甚至可能會的增加實作)。請注意一個很重要的一點,尤其是屬性,是靜態類別中實作的擴充方法,因此,沒有任何額外的執行個體的狀態所引入的延伸類型。如果您需要這種狀態時,您必須將它儲存在集合,以做為索引的延伸的類型執行個體,以便擷取相關聯的狀態。


標記 Michaelis是創辦 IntelliTect,他作為其主要技術架構設計人員和訓練。幾乎二十他經過 Microsoft MVP,以及 Microsoft 地區導向器自 2007年。Michaelis 做數個 Microsoft 軟體設計檢閱小組成員,包括 C#、 Microsoft Azure、 SharePoint 和 Visual Studio ALM。他在開發人員所做的心得,而且已經寫許多書籍,包括其最新,「 基本 C# 7.0 (第 6 版) 」 (itl.tc/EssentialCSharp)。在 Facebook 上連絡他facebook.com/Mark.Michaelis,在他的部落格上IntelliTect.com/Mark,Twitter 上: @markmichaelis或透過電子郵件在mark@IntelliTect.com

非常感謝下列 Microsoft 技術專家檢閱這篇文章:Kevin Bost、 Grant Ericson、 Tom Faust Mads Torgersen


MSDN Magazine 論壇中的這篇文章的討論