非空引用類型——C 8.0 原文地址:https://devblogs.microsoft.com/dotnet/try out nullable reference types/?utm_source=vs_developer_news&utm_medium=referral 該新增的特性最關鍵的 ...
非空引用類型——C#8.0
該新增的特性最關鍵的作用是處理泛型和更高級 API 的使用場景。這些都是我們從 .NETCore 上註解衍生過來的經驗。
通用不為 NULL 約束
通常的做法是不允許泛型類型為 NULL。我們給出下麵代碼:
interface IDoStuff<Tin, Tout>
{
Tout DoStuff(Tin input);
}
這種做法對為空引用和值類型也許令人滿意的。也就是說對 string
或者 or
來說很好,但是對 string?
或 or
卻不是。
這樣可以通過 notnull
約束來實現。
interface IDoStuff<Tin, Tout>
where Tin: notnull
where Tout: notnull
{
Tout DoStuff(Tin input);
}
像下麵這樣的實現類如果沒有應用相同的泛型約束還是會生成一個警告。
//警告:CS8714 - 可空類型參數 TIn 無法匹配 notnull 約束
//警告:CS8714 - 可空類型參數 TOut 無法匹配 notnull 約束
public class DoStuffer<TIn, TOut>: IDoStuff<TIn, TOut>
{
public TOut DoStuff(TIn input)
{
...
}
}
修改成下麵版本則無此問題
// No warnings!
public class DoStuffer<TIn, TOut>: IDoStuff<TIn, TOut>
where TIn : notnull
where TOut : notnull
{
public TOut DoStuff(TIn input)
{
...
}
}
當創建這個類的實例時,如果你用可空引用類型時,也會生成一個警告
//警告:CS8714 - 可空類型參數 string? 無法匹配 notnull 約束
var doStuffer = new DoStuff<string?,string?>();
// no wanings
var doStufferRight = new DoStuff<string,string>();
針對於值類型同樣如此:
// Warning: CS8714 - 類型參數 'int?' 是可為 null 的 無法匹配 'notnull' 約束
var doStuffer = new DoStuff<int?, int?>();
// No warnings!
var doStufferRight = new DoStuff<int, int>();
註意:如果有人編寫如上代碼沒有發生警告,是因為在解決方案下沒有開啟可空驗證,具體在
.csproj
配置如下:<Project> <PropertyGroup> <CheckForOverflowUnderflow>false</CheckForOverflowUnderflow> <LangVersion>8.0</LangVersion> <!-- New name --> <Nullable>enable</Nullable> <!-- Old name while we wait for new name to be everywhere --> <NullableContextOptions>enable</NullableContextOptions> <Features>strict</Features> </PropertyGroup> </Project>
這個約束條件對於泛型代碼來說是很有用的,當你想要確保使用一個不為 null 的引用類型。一個顯著的例子是 Dictionary<TKey,TValue>
其中的 TKey
現在被約束為 notnull
它不允許使用為 null 的類型作為 key:
// Warning: CS8714 警告內容同上
var d1 = new Dictionary<string?, string>(10);
// 正因如此,不為空的鍵類型使用 null 作為 key 是會有一個警告
var d2 = new Dictionary<string, string>(10);
// Warning: CS8625 - 無法將 null 文本轉換為不為 null 引用類型
var nothing = d2[null];
然而,不是所有的為 null 泛型問題都能通過這種方式解決。這裡我們添加一些屬性標簽,來允許你們在編譯器中影響可 null 分析。
T? 的問題
你可能想知道:當指定一個可 null 引用或值類型的泛型類的時候,為什麼不僅僅只用 T?
不久好了麽?這個答案非常複雜。
從 T?
的定義上來看,意思就是說它是 “任何可為 null 類型”。但是,這就潛意識在暗示意味著 T 將是 “任何不可為 null 的類型”,這是錯誤的。這在今天用可 null 值類型的 T 是可能的(例如 bool?
)。這是因為 T 早就是一個沒有限制的反省類型。使用 T 作為一個無限制的泛型類,這個變化在語義上是不是期望的,並且在已經存在大量的代碼中無疑是災難。
在一個,值得註意的是可 null 引用類型與為 null 的值的類型是不等價的。Nullable 值類型具體映射到會生成一個類,在 .NET 中 int?
實際上 Nullable<int>
。但是對於 string?
,它實際上還是 string
,但是編譯器會生成滑鼠標簽來標記它。這樣做是為了向後相容。換句話說,string?
是一個“冒牌類型”(fake type),int?
卻不是。
為了區分為 null 值類型預計為 null 引用類型,我們用如下模式:
void M<T>(T? t) where T: notnull
這段代碼是說這個參數 T
為 null,並且 T
被約束為 notnull
。如果 T 是一個 string
,那麼實際上 M 的簽名方法將會成為 M<string>([NullableAttribute] T t)
。但是如果 T 是個 int
,M 則會變成 M<int>(Nullable<int> t)
。這兩個簽名方法本質上就是不通過的,並且這種差異是互相矛盾的。
因為這個問題,null 引用類型以及 null 值類型這兩者具體表現的差異,任何使用 T?
都必須要求你所引用的這個 T 的 class
和 struct
都要有這個約束。
最後,T? 的存在,都能在 null 值類型和 null 引用類型之間工作,但是不能解決泛型所有的問題。你也許想在單一方向用可 null 類型(比如只在輸入或只在輸出)並且它既不能用 notnull
也不能用 T 和 T? 來表示分開,除非你認為對於輸入輸出分離泛型類。
可 null 先決條件:AllowNull
和 DisallowNull
看下麵這段代碼
public class MyClass
{
public string MyValue { get; set; }
}
這個 API 在 C#8.0 之前可能也是支持的。但是 string
的意思現在是不為 null 的 string
!我們也許希望實際上任然會允許 null 值,但是在 get
總是返回一些 string
的值。這個地方你就可以使用 AllowNull
:
public class MyClass
{
private string _innerValue = string.Empty;
[AllowNull]
public string MyValue
{
get
{
return _innerValue;
}
set
{
_innerValue = value ?? string.Empty;
}
}
}
這樣我就能確保我們得到的值總是不為 null 的,並且我類型還是為 string
類型。但是為了向後相容,我們還是想接受 null 值。AllowNull
標簽就能讓你特指這個 setter 接受 null 值。那樣調用就能得到預期結果:
void M1(MyClass mc)
{
mc.MyValue = null; //沒有 AllowNull 標簽會有警告
}
void M2(MyClass mc)
{
Console.WriteLine(mc.MyValue.Length); // ok, 註意這裡沒有警告
}
註意:這裡存在一個 bug ,null 值的分配與 可為 null 的分析衝突了。這在編譯器後續的更新中解決。
考慮下麵代碼:
public static class HandleMethods
{
public static void DisposeAndClear(ref MyHandle handle)
{
...
}
}
在這個例子中,MyHandle
引用某個資源的句柄。這個 API 的典型用法是我們有一個引用傳遞的不為 null 的實例,但是當它被清除時,這個引用就為 null 了。我們用 DisallowNull
來很好的表示:
public static class HandleMethods
{
public static void DisposeAndClear([DisallowNull] ref MyHandle? handle)
{
...
}
}
這個效果就是任何調用者通過傳遞 null 值,那麼就會引發一個警告,但是你企圖用 . 來處理這個被調用的方法也將會發出警告:
void M(MyHandle handle)
{
MyHandle? local = null; // Create a null value here
HandleMethods.DisposeAndClear(ref local); // Warning: CS8601 - Possible null reference assignment
// Now pass the non-null handle
HandleMethods.DisposeAndClear(ref handle); // No warning! ... But the value could be null now
Console.WriteLine(handle.SomeProperty); // Warning: CS8602 - Dereference of a possibly null reference
}
對於那些案例在我們需要的地方,這兩個特性允許我們單方向可為 null 或不為 null。
更正式的講:
AllowNull
標簽允許調用者傳遞 null 值,甚至是如果這個類型不允許為 null。
DisallowNull
標簽不允許調用者傳遞 null 值,甚至是如果這個類型允許為 null。
他們都能任何輸入上指定:
- 值類型參數
in
參數ref
參數- 欄位
- 屬性
- 索引
重要:這些標簽隻影響那些註解了的方法調用的 null 分析。這些被註解的方法以及介面實現類的主體不遵循這些特性。
Nullable 後置條件:MaybeNull
和 NotNull
請看下麵代碼:
public class MyArray
{
// 返回結果如果不匹配則預設值
public static T Find<T>(T[] array, Func<T, bool> match)
{
...
}
// 調用的時候不會返回 null
public static void Resize<T>(ref T[] array, int newSize)
{
...
}
}
這裡會有另一個問題。我們希望在 Find
方法找到匹配返回 default
,它是可 null 的引用類型。我們希望 Resize
方法接受一個可能為 null 的輸入,但是在調用 Resize
之後確保我們想要的 array
值不是 null。同樣,我們用 notnull
約束不能解決。
這個時候輸入 [MaybeNull]
以及 [NotNull]
現在就能想象輸出的可為 null 了。我們只要做需要做些小修改:
public class MyArray
{
[return: MaybeNull]
public static T Find<T>(T[] array, Func<T, bool> match)
{
...
}
public static void Resize<T>([NotNull] ref T[]? array, int newSize)
{
...
}
}
然後我們就調用就會有這樣的效果:
void M(string[] testArray)
{
var value = MyArray.Find<string>(testArray, s => s == "Hello!");
Console.WriteLine(value.Length); // Warning: 取消引用可能出現的空引用
MyArray.Resize<string>(ref testArray, 200);
Console.WriteLine(testArray.Length); // Safe!
}
註意:在 .netcore 3.0 preview 7 下,我的VS2019 16.2.3 版本中以上代碼不會爆出警告,只有在當前域引用可能為 null 的引用才會暴警告:
string value = defalt; Console.WriteLine(value.Length);//這裡會爆出警告提示
也說明瞭目前的版本還不完善。
第一個方法指定 T 返回的能是 null 值。也就是說當使用調用這個方法返回的結果時,必須要檢查值是否為 null。
第二個方法有一個嚴格的簽名:[NotNull] ref T[]? array
。意思是 array
作為輸入能為 null,但是當 Resize
被調用時,array
不能為 null。也就是如果你調用 Resize
之後你在 array
有引用("."),你將不會得到一個警告。因為在 調用Resize
時,array
值永遠不為 null 了。
正式說:
MaybeNull
特性允許你返回可為 null 的類型,甚至這個類型不允許為 null。NotNull
特性不允許返回的結果的為 null,甚至是本身這個類允許為 null。他們都能指定以下的任何輸出上:
- 方法返回
out
標記參數(在方法調用後)ref
標記參數(在方法調用後)- 欄位
- 屬性
- 索引
重要提示:這些特性僅僅只是影響對那些被註解的調用方法的調用者可為 null 性的分析。那些被註解的方法主體和類似介面實現的東西一樣不遵循這個這些標簽。我們也許會在下一個特性中加入。
後置條件:MaybeNullWhen(bool)
和 NotNullWhen(bool)
考慮如下代碼片段:
public class MyString
{
// value 為 null 時為 true
public static bool IsNullOrEmpty(string? value)
{
...
}
}
public class MyVersion
{
// 如果轉換成功,Version 將不會為 null
public static bool TryParse(string? input, out Version? version)
{
...
}
}
public class MyQueue<T>
{
// 如果我們不能將它出隊列,那麼 result 能為 null
public bool TryDequeue(out T result)
{
...
}
}
這些方法在 .NET 隨處可見,當返回值是 true
或 false
對應於參數的可為 null 性(或者可能位 null)。MyQueue
這個例子有點特殊,因為他是泛型的。TryDequeque
方法如果在它返回 false
時應該給 result
賦值為 null
,但是這種情況只有 T 是引用類型下才可以。如果 T 是值類型 struct
結構體,那麼它不會是 null。
所以針對這種情況,我們想做以下三件事:
- 如果
IsNullOrEmpty
返回false
,那麼value
不會為 null - 如果
TryParse
返回 true,那麼 version 不為 null - 如果
TryDequeue
返回false
,那麼result
能為 null,如果被提供的參數類型是引用類型的話
很遺憾,C# 編譯器並不會將這些方法返回的結果對於參數的可空性關聯起來。
現在有了 NotNullWhen(bool)
和 MaybeNullWhen(bool)
就能對參數進行更細緻的處理:
public class MyString
{
public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
{
...
}
}
public class MyVersion
{
public static bool TryParse(string? input, [NotNullWhen(true)]out Version? version)
{
...
}
}
public class MyQueue<T>
{
public bool TryDequeue([MaybeNullWhen(false)] out T result)
{
...
}
}
然後我們就可以這樣調用了:
void StringTest(string? s)
{
if(MyString.IsNullOrEmpty(s))
{
//這裡會有警告
//Console.WriteLine(s.Length);
return;
}
Console.WriteLine(s.Length); //安全
}
void VersionTest(string? s)
{
if (!MyVersion.TryParse(s, out var version))
{
// 警告
// Console.WriteLine(version.Major);
return;
}
Console.WriteLine(version.Major); // Safe!
}
//註意 在我的實驗下,以下代碼並不會像文中註釋中一樣會產生警告
void QueueTest(MyQueue<string> q)
{
if (!q.TryDequeue(out var s))
{
// This would generate a warning:
// Console.WriteLine(s.Length); //實際上在我VS中是沒有警告的
return;
}
Console.WriteLine(s.Length); // Safe!
}
調用者可以使用與往常一樣的模式處理 api,沒有來自編譯器的任何警告:
- 如果
IsNullOrEmpty
返回true
,那麼可以安全的在value
使用 "." - 如果
TryParse
返回true
,version
成功轉換並可以安全使用 “.” - 如果
TryDequeue
返回false
,result
可能為 null,所以要根據實際需要檢查值(例如:當一個類型是值類型結構體時,返回false
不為 null,但如果為引用類型,那麼它就為 null)
正式的講:
NotNullWhen(bool)
簽名的參數是不為 null 的,甚至這個類型本身不允許為 null,條件依賴於 bool
方法返回的值。MaybeNullWhen(bool)
簽名的參數能為 null,甚至是參數類型本身不允許,條件依賴於方法返回 bool
值。他們能指定任何參數類型。
輸入輸出之間的無 Null 依賴:NotNullIfNotNull(string)
考慮下麵代碼片段:
class MyPath
{
public static string? GetFileName(string? path)
{
...
}
}
在這個例子,我們希望能夠返回 null 字元串,並且我們也應該接受一個 null 值作輸入。所以這個方法能完成我想要的效果。
但是,如果 path
不為 null,我們希望確保返回總是能返回一個字元串。即我想要 GetFileName
返回一個不為 null 的值,條件是 path 不為 null。這裡是沒有方法去表達這個意思的。
而 [NotNullIfNotNull]
就登場了。這個特性能使你的代碼變得更花哨,所以小心的使用它!
這裡我將展示我使用這個 API 的代碼:
class MyPath
{
[return: NotNullIfNotNull("path")]
public static string? GetFileName(string? path)
{
...
}
}
那麼我們調用這個方法就有這樣的效果:
void PathTest(string? path)
{
var possiblyNullPath = MyPath.GetFileName(path);
Console.WriteLine(possiblyNullPath);// Warning: 取消引用可能出現空引用
if(!string.IsNullOrEmpty(path))
{
var goodPath = MyPath.GetFileName(path);
Console.WriteLine(goodPath);// safe 註意:在我的驗證下仍然有警告
}
}
正式的:
NotNullIfNotNull(string)
特性簽名錶示任何輸出值是不為 null 的,條件依賴於給出的特性指定的名稱參數的可 null 性。它們可以在以下具體結構指定:
- 方法返回
ref
標明的參數
流特性:DoesNotReturn
和 DoesNotReturnIf(bool)
你可以使用多個方法影響程式的控制流。例如,一個異常幫助類方法,調用它將拋出一個異常,或者一個斷言方法,它根據你的輸入是 true
還是 false
來拋出異常。
你也許希望做一些能像 assert
這樣,這個值不為 null,並且我們認為你們會喜歡它,如果編譯器能理解它。
輸入 DoseNotReturn
和 DoseNotReturnIf(bool)
。這裡有一些例子來告訴你怎樣使用:
internal static class ThrowHelper
{
[DoseNotReturn]
public static void ThrowArgumentNullException(ExceptionArgument arg)
{
...
}
}
public static class MyAssertionLibrary
{
public static void MyAssert([DoesNotReturnIf(false)] bool condition)
{
...
}
}
當 ThrowArgumentNullException
在方法中被調用時,它會拋出異常。註解在簽名上的 DoesNotReturn
將發出信號給編譯器表示在之後不要進行非 null 分析,因為代碼將會不可達。
當MyAssert
被調用時,並且條件傳遞為 false
,它會拋出異常。註解的 DoesNotReturnIf(false)
以及裡面的條件參數能夠讓編譯器知道程式流不會繼續往下走,如果條件為 false
。當你想要斷言一個值的可空性的時候,這是非常有用的。在代碼 MyAssert(value != null)
路徑之後,那麼編譯器會假使 value
不是 null。
DoesNotReturn
用在方法上。DoesNotReturnIf(bool)
用在輸入參數上。
進化(Evolving)你的註解
一旦你在公開的 API 使用了註解,那你就要考慮一個事實,那就是更新 API 會影響下游:
- 增加可空性的註解地方,它們可能會給用戶的代碼帶來警告。
- 移除可空性註解能如此(比如介面實現)
可空註解在你的公有 API 一部分。增加或移除都會帶來警告。我們建議在預覽版本開始這個,並且征求你們的反饋,目標是在一個完整的版本之後不改變任何註解。儘管這不總是可能的,但是我們還是推薦。
Microsoft framwork 和 庫目前的狀態
因為可空型引用類型是新的,主要的 C# Microsoft framwork 和 lib 的作者也還沒有進行合適的註解。
就是說,.NET Core 中的 “Core Lib”,它代表了 .NET Core 共用的 framwork 的 25% 都完整的做了更新。包括了System
,System.IO
以及 System.Collections.Generic
。我們正在關註對我們的決定的反饋,以便我們做合適的做出調整,在他們的使用變得更加廣泛之前。
儘管任然還有大約 80% 的 CoreFX 需要註解,但是最常用的 APIs 都有了註解。
可空引用類型的線路
目前,我們把整個可空引用類型視為預覽版。它是穩定,但是這個特性涉及到我們自己技術和更好的 .NET 生態。還需要一些時間來完成。
也就是我們鼓勵庫作者使用在他們的庫中開始註解。這個特性只會讓庫變得更好的使用可空能力,幫忙 .NET 更加安全。
在未來的一年左右,我們將繼續提高這個特性以及在 Microsoft frameworks 和 libs 中傳播。
針對語言,特別是編譯器分析,我們將使它增強以至於我們能使你需要做的事最小化,就像使用非空操作符(!
)。這些增強都在這裡追蹤。
針對 CoreFX,我們將會維護註解到 80% 的 api,同樣也會根據反饋做出適當的調整。
對於 ASP.NET Core 和 Entity Framework,我們一旦添加到 CoreFX 和 編譯器我們都會添加註解。
我們沒有計划去給 WinForms 和 WPFs 的 APIs 註解,但是我們樂意聽到你們的反饋在不同的事情上。
最後,我們將繼續提高 C# Visual Studio 工具。我們對於這些特性有很多建議去幫助使用這些特性,我們也很喜歡你們的建議。
下一步
如果你仍然在讀以及在你的代碼中沒有嘗試這個特性,特別是你的庫代碼,還請嘗試以及給我們反饋在你感覺有任何異樣。在 .NET 中消除令人意外的 NullReferenceException
還需要漫長的過程,但是我們希望在長時間運行的之後,開發者不用擔心被隱式的空值影響。你們能幫助導我們。試試這個特性,在你的類庫中使用註解。反饋你的經驗
同步至:https://github.com/MarsonShine/Books/blob/master/CSharpGuide/docs/Write_Safe_And_Efficient_Code.md