.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N220423602
出刊日期: 2022/4/27
C# 在第8版新增可為 Null 的參考型別(Nullable reference types,NRT),可以將參考型別明確標註為可以設定為「null」,以便於在開發階段讓工具協助檢查參考型別變數是否可以設定為「null」,如此可以減少應用程式擲出「System.NullReferenceException」例外錯誤的機率。
例如以下範例程式碼:
string s = null;
Console.WriteLine( s.Length );
若直接執行程式,則會直接擲出「System.NullReferenceException」例外錯誤,請參考下圖所示:

圖 1:「System.NullReferenceException」例外錯誤。
啟用與停用Nullable
預設使用Visual Studio 2022建立的.NET 6專案,已經自動在專案等級啟用「Nullable」,以檢查專案中所有檔案的程式碼。你可以使用屬性(Properties)視窗,或是編輯專案檔「*.csproj」來啟用或停用之。
以主控台應用程式為例,從Visual Studio 2022「Solution Explorer」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Properties」項目。在「Build」分頁中,從「Nullable」下拉式清單方塊中啟用或停用,請參考下圖所示:

圖 2:啟用與停用Nullable。
專案等級的設定會儲存在專案檔案(*.csproj)中,我們可以在Visual Studio 2022「Solution Explorer」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Edit Project File」項目,請參考下圖所示:

圖 3:編輯專案檔案。
專案檔案中包含<Nullable>enable</Nullable>的設定:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
若要停用可以直接將「Nullable」項目設定為「disable」,例如以下程式碼:
<Nullable>disable</Nullable>
在啟用「Nullable」的情況下,可為 Null 的參考型別(Nullable reference types)變數的值若為「null」,在開發階段Visual Studio 2022工具會顯示警告訊息,例如以下範例程式碼:
string s = null;
Console.WriteLine( s.Length );
第一行程式會顯示「CS8600」警告訊息,請參考下圖所示:

圖 4:「CS8600」警告訊息。
而第二行程式會顯示「CS8602」警告訊息,請參考下圖所示:

圖 5:「CS8602」警告訊息。
加上檢查程式碼
啟用Nullable功能之後,開發工具在編譯階段就可以幫你檢查程式碼,顯示警告訊息,提示程式執行時可能會出錯,要移除這些警告訊息的方式有很多種,其中一種變是加上檢查程式碼,避免在變數或屬性值為「null」時,直接進行操作。
例如修改程式碼如下,加上「?」號,讓「s」變數的型別為可為 Null 的參考型別(Nullable reference types,NRT),再加上「if」陳述式進行判斷,Visual Studio 2022將透過程式碼靜態分析功能為你檢視程式碼,並移除警告訊息:
string? s = null;
if ( s is not null ) {
Console.WriteLine( s.Length );
}
「?.」運算子
使用「?.」運算子,讓判斷程式碼更為簡短,將上述程式碼修改如下:
string? s = null;
Console.WriteLine( s?.Length );
「!」運算子
另一種做法是使用C# !(null-forgiving) 運算子,搭配不可為 Null 的參考型別(Non-nullable reference types),將程式改寫如下,將「s」設定為「null!」:
string s = null!;
Console.WriteLine( s.Length );
「??」運算子
有時可能需要將可能為Null的運算式指派給變數,這時可能會得到編譯器提示的警告訊息,例如以下程式碼:
string s = GetString();
Console.WriteLine( s?.Length );
static string? GetString( ) => "Hello";
第一行程式碼會出現「CS8600」警告訊息,請參考下圖所示:

圖 6:「CS8600」警告訊息。
我們同樣可以使用「??」運算子,在「GetString()」函式回傳「null」時,給予一個預設值,例如以下程式碼:
string s = GetString() ?? "";
Console.WriteLine( s?.Length ); //5
static string? GetString( ) => "Hello";
如此當「GetString」傳回「null」,程式執行時也不會發生例外,參考以下程式碼:
string s = GetString() ?? "";
Console.WriteLine( s?.Length ); //0
static string? GetString( ) => null;
也可以在方法呼叫語法之後使用C# !(null-forgiving) 運算子,參考以下程式碼:
string s = GetString()!;
Console.WriteLine( s?.Length ); //empty
static string? GetString( ) => null;
「#nullable」指示詞
如果你可以確保變數或屬性值不為「null」,那麼可以利用「#nullable」指示詞通知編譯器忽略這個警告,例如以下程式碼,第二行的警告會消失;而第四行的警告依然存在:
#nullable disable warnings
string s = null;
#nullable enable warnings
Console.WriteLine( s.Length );
請參考下圖所示:

圖 7:使用「#nullable」指示詞。
null狀態靜態分析Attribute
啟用Nullable之後,C#編譯器會進行程式碼靜態分析來檢查變數的null狀態來決定是否顯示警示訊息。不過有時後我們要適當給C#編譯器一些暗示,才能準確地辨識出變數的狀態。例如以下程式碼範例:
string? s = null;
if ( IsNotNull(s) ) {
Console.WriteLine( s.Length );
}
static bool IsNotNull( string? str ) => str is not null;
雖然在第二行程式碼中,我們已叫用自訂的「IsNotNull」函式檢查null狀態,在第三行程式中還會顯示警示訊息,請參考下圖所示:
![clip_image002[1] clip_image002[1]](http://blogs.uuu.com.tw/Articles/image.axd?picture=clip_image002%5B1%5D_thumb_2.jpg)
圖 8:自訂「IsNotNull」函式檢查。
參考以下程式碼,我們可以使用「NotNullWhen」Attribute來知會編譯器,當此函式回傳「true」時,便表示變數不為「null」,如此警示訊息便會自動消失:
using System.Diagnostics.CodeAnalysis;
string? s = null;
if ( IsNotNull( s ) ) {
Console.WriteLine( s.Length );
}
static bool IsNotNull( [NotNullWhen( true )] string? str ) => str is not null;
Entity Framework Core與可為 Null 的參考型別(NRT)
若在Entity Framework Core使用到可為 Null 的參考型別(NRT),需要特別的小心。Entity Framework Core Enitty類別的屬性(Property)值若可以包含「null」,便表示它是選擇性的(Optional),若Enitty類別的屬性(Property)值不可以包含「null」,則表示它應該是必要的(Required)。
停用可為 Null 的參考型別(NRT)時,所有.NET參考型別都視為選擇性的(可以包含「null」);啟用可為 Null 的參考型別(NRT)時,NRT被視為選擇性的(如string?,可以包含「null」),其它參考型別(如string)是必要的,不可以包含「null」。
Entity Framework Core建議應該使用可為 Null 的參考型別(Nullable reference types,NRT),這樣就可以不必使用Fluent API或Data Annotations來做重複的事情,例如設定[Required] Attribute來表示此屬性是否不可為「null」。
參考以下程式碼範例,在啟用「Nullable」功能時,根據慣例「Title」是必要的(Required);而「Description」是選擇性的:
public class Book {
public int Id { get; set; }
[StringLength( 50 )]
public string Title { get; set; }
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
public string? Description { get; set; }
}
當然我們也可以明確地使用[Required] Attribute 來指明「Description」是必要的,參考以下程式碼:
public class Book {
public int Id { get; set; }
[StringLength( 50 )]
public string Title { get; set; }
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
[Required]
public string? Description { get; set; }
}
設定預設值
上述程式碼會讓編譯器產生「CS8618」警告:
Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
請參考下圖所示:

圖 9:「CS8618」警告。
除去警告的最簡單做法就是賦予string型別的「Title」屬性一個初始值,例如以下程式碼:
public class Book {
public int Id { get; set; }
[StringLength( 50 )]
[Required]
public string Title { get; set; } = string.Empty;
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
public string? Description { get; set; }
}
不過這不是一個好做法,這樣C# 8發明的Nullable Reference Types就沒有意義了,Nullable Reference Types 用來確保屬性或變數要包含一個有用的值,並且在使用到屬性或變數時能偵測它是否為Null。
建構函式繫結(Constructor Binding)
為了避免編譯器會抱怨「CS8618」警告,可使用建構函式繫結(Constructor Binding)避免沒有初始化不可為 Null 的參考型別(Non-nullable reference types)的警告訊息,修改程式碼如下,加入建構函式:
public class Book {
public int Id { get; set; }
[StringLength( 50 )]
public string Title { get; set; }
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
public string? Description { get; set; }
public Book( string title, string? description = null ) {
Title = title;
Description = description;
}
}
建構函式繫結(Constructor Binding)有一個限制,不適用於導覽屬性(Navigation properties)的初始化。
!(null-forgiving) 運算子
上述問題的其中一種簡潔的解法是使用C# !(null-forgiving) 運算子,將程式改寫如下,將「Title」屬性值設定為「null!」:
public class Book {
public int Id { get; set; }
[StringLength( 50 )]
public string Title { get; set; } = null!;
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
public string? Description { get; set; }
}
DbContext與DbSet
當啟用可為 Null 的參考型別(Nullable reference types,NRT)時,在「DbContext」類別定義DbSet<T>屬性時也會遇到初始化的問題,例如以下程式碼:
public class BookContext : DbContext {
public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
}
public DbSet<Book> Books { get; set; }
}
編譯器會提示發生「CS8618」警告,請參考下圖所示:

圖 10:「CS8618」警告。
實際上「DbContext」基礎類別(Base Class)會負責初始化這個屬性,但編譯器目前無法偵測到這一點,因此我們可以將程式改寫如下,使用自動屬性語法設計DbSet<T>屬性,然後使用C# !(null-forgiving) 運算子知會編譯器這個DbSet<T>屬性會在其它程式碼中進行初始化,它會被設為非Null值:
public class BookContext : DbContext {
public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
}
public DbSet<Book> Books { get; set; } = null!;
}
另一種作法是設定屬性的初始值為Set<T>,例如以下程式碼:
public class BookContext : DbContext {
public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
}
public DbSet<Book> Books => Set<Book>( );
}
注意事項
逆向工程目前不支援可為 Null 的參考型別(NRT),EF Core在產生C#程式碼時預設會假設停用這個功能。