可為 Null 的參考型別

by vivid 27. 四月 2022 11:33

.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」例外錯誤,請參考下圖所示:

clip_image002

圖 1:「System.NullReferenceException」例外錯誤。

啟用與停用Nullable

預設使用Visual Studio 2022建立的.NET 6專案,已經自動在專案等級啟用「Nullable」,以檢查專案中所有檔案的程式碼。你可以使用屬性(Properties)視窗,或是編輯專案檔「*.csproj」來啟用或停用之。

以主控台應用程式為例,從Visual Studio 2022「Solution Explorer」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Properties」項目。在「Build」分頁中,從「Nullable」下拉式清單方塊中啟用或停用,請參考下圖所示:

clip_image004

圖 2:啟用與停用Nullable。

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

clip_image006

圖 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」警告訊息,請參考下圖所示:

clip_image008

圖 4:「CS8600」警告訊息。

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

clip_image010

圖 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」警告訊息,請參考下圖所示:

clip_image012

圖 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 );

請參考下圖所示:

clip_image014

圖 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]

圖 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.

請參考下圖所示:

clip_image016

圖 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」警告,請參考下圖所示:

clip_image018

圖 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#程式碼時預設會假設停用這個功能。

Tags:

.NET | .NET Magazine國際中文電子雜誌 | C# | 許薰尹Vivid Hsu

NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List