練習 - 套用 null 安全性策略

已完成

在上一個單元中,您已了解如何在程式碼中表達可 NULL 性意圖。 在本單元中,您會將學到的內容套用至現有的 C# 專案。

注意

本課程模組使用 .NET CLI (命令列介面) 和 Visual Studio Code 進行本機開發。 完成本課程模組之後,您可以使用 Visual Studio (Windows)、Visual Studio for Mac (macOS) 來應用這些概念,或繼續使用 Visual Studio Code (Windows、Linux 和 macOS) 來進行開發。

本課程模組使用 .NET 6.0 SDK。 確認您已在慣用終端中執行下列命令來安裝 .NET 6.0:

dotnet --list-sdks

您會看到類似以下的輸出:

3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]

確定已列出開頭為 6 的版本。 如果未列出任何項目或找不到命令,則請安裝最新的 .NET 6.0 SDK

擷取並檢查範例程式碼

  1. 在命令終端機中,複製範例 GitHub 存放庫,並切換至複製的目錄。

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. 在 Visual Studio Code 中開啟專案目錄。

    code .
    
  3. 使用 dotnet run 命令執行範例專案。

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    這會導致擲回 NullReferenceException

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
       at Program.<Main>$(String[] args) in .\src\ContosoPizza.Service\Program.cs:line 13
    

    堆疊追蹤指出例外狀況發生在 .\src\ContosoPizza.Service\Program.cs 中的第 13 行。 在程式碼行 13 上,會在 pizza.Cheeses 屬性上呼叫 Add 方法。 因為 pizza.Cheesesnull,所以會擲回 NullReferenceException

    using ContosoPizza.Models;
    
    // Create a pizza
    Pizza pizza = new("Meat Lover's Special")
    {
        Size = PizzaSize.Medium,
        Crust = PizzaCrust.DeepDish,
        Sauce = PizzaSauce.Marinara,
        Price = 17.99m,
    };
    
    // Add cheeses
    pizza.Cheeses.Add(PizzaCheese.Mozzarella);
    pizza.Cheeses.Add(PizzaCheese.Parmesan);
    
    // Add toppings
    pizza.Toppings.Add(PizzaTopping.Sausage);
    pizza.Toppings.Add(PizzaTopping.Pepperoni);
    pizza.Toppings.Add(PizzaTopping.Bacon);
    pizza.Toppings.Add(PizzaTopping.Ham);
    pizza.Toppings.Add(PizzaTopping.Meatballs);
    
    Console.WriteLine(pizza);
    
    /*
        Expected output:
    
        The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
        It's covered with a blend of mozzarella and parmesan cheese.
        It's layered with sausage, pepperoni, bacon, ham and meatballs.
        This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    */
    

啟用可為 null 的內容

現在,您將啟用可為 null 的內容,並檢查其對組建的影響。

  1. src/ContosoPizza.Service/ContosoPizza.Service.csproj 中,新增醒目提示的程式碼行並儲存您的變更:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    

    上述變更會為整個 ContosoPizza.Service 專案啟用可為 null 的內容。

  2. src/ContosoPizza.Models/ContosoPizza.Models.csproj 中,新增醒目提示的程式碼行並儲存您的變更:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
    </Project>
    

    上述變更會為整個 ContosoPizza.Models 專案啟用可為 null 的內容。

  3. 使用 dotnet build 命令建置範例解決方案。

    dotnet build
    

    建置成功但出現 2 個警告。

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      Restored .\src\ContosoPizza.Service\ContosoPizza.Service.csproj (in 477 ms).
      Restored .\src\ContosoPizza.Models\ContosoPizza.Models.csproj (in 475 ms).
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
      ContosoPizza.Models -> .\src\ContosoPizza.Models\bin\Debug\net6.0\ContosoPizza.Models.dll
      ContosoPizza.Service -> .\src\ContosoPizza.Service\bin\Debug\net6.0\ContosoPizza.Service.dll
    
    Build succeeded.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        2 Warning(s)
        0 Error(s)
    
    Time Elapsed 00:00:07.48
    
  4. 使用 dotnet build 命令再次建置範例解決方案。

    dotnet build
    

    這次,建置會成功且不會出現任何錯誤或警告。 上一次建置成功完成,但出現警告。 由於來源未變更,因此建置程序不會再次執行編譯器。 因為建置不會執行編譯器,所以不會出現警告。

    提示

    您可以在 dotnet build 之前使用 dotnet clean 命令,強制重建專案中的所有組件。

  5. .csproj 檔案中,新增醒目提示的程式碼行並儲存您的變更。

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" />
      </ItemGroup>
    
    </Project>
    
    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
    </Project>
    

    先前的變更會指示編譯器在遇到警告時使建置失敗。

    提示

    您可以選擇使用或不使用 <TreatWarningsAsErrors>。 但我們建議使用,因為這能確保您不會忽略任何警告。

  6. 使用 dotnet build 命令建置範例解決方案。

    dotnet build
    

    建置失敗,出現 2 個錯誤。

    dotnet build
    Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
    Copyright (C) Microsoft Corporation. All rights reserved.
    
      Determining projects to restore...
      All projects are up-to-date for restore.
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    
    Build FAILED.
    
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
    .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj]
        0 Warning(s)
        2 Error(s)
    
    Time Elapsed 00:00:02.95
    

    當將警告視為錯誤時,應用程式就會停止建置。 這種情況實際上需要這樣做,因為錯誤數目很小,因此我們會快速解決這些問題。 這兩個錯誤 (CS8618) 讓您知道有屬性宣告為不可為 Null,且尚未經過初始化。

修正錯誤

可解決與可 null 性相關的警告/錯誤的策略有很多。 這些範例包含:

  • 需要不可為 Null 的起司和配料集合作為建構函式參數
  • 攔截屬性 get/set 並新增 null 檢查
  • 表示屬性可為 Null 的意圖
  • 使用屬性初始設定式,以預設 (空白) 值內嵌初始化集合
  • 在建構函式中,將預設 (空白) 值指派給屬性
  1. 為了修正 Pizza.Cheeses 屬性上的錯誤,請修改 Pizza.cs 上的屬性定義以新增 null 檢查。 這真的不是沒有起司的披薩,對吧?

    namespace ContosoPizza.Models;
    
    public sealed record class Pizza([Required] string Name)
    {
        private ICollection<PizzaCheese>? _cheeses;
    
        public int Id { get; set; }
    
        [Range(0, 9999.99)]
        public decimal Price { get; set; }
    
        public PizzaSize Size { get; set; }
        public PizzaCrust Crust { get; set; }
        public PizzaSauce Sauce { get; set; }
    
        public ICollection<PizzaCheese> Cheeses
        {
            get => (_cheeses ??= new List<PizzaCheese>());
            set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
        }
    
        public ICollection<PizzaTopping>? Toppings { get; set; }
    
        public override string ToString() => this.ToDescriptiveString();
    }
    

    在上述程式碼中:

    • 已新增新的支援欄位來協助攔截 getset 和名為 _cheeses 的屬性存取子。 其會宣告為可為 null (?) 並保留未初始化。
    • get 存取子會對應至使用 null 聯合運算子 (??) 的運算式。 運算式會傳回 _cheeses 欄位,假設其不是 null。 如果其為 null,則其會在傳回 _cheeses 之前將 _cheeses 指派至 new List<PizzaCheese>()
    • set 存取子也會對應至運算式,並利用 null 聯合運算子。 當取用者指派 null 值時,會擲回 ArgumentNullException
  2. 因為不是所有披薩都有配料,nullPizza.Toppings 屬性而言可能是有效值。 在這種情況下,將其表示為可為 null 是合理的。

    1. 修改 Pizza.cs 上的屬性定義,以允許 Toppings 可為 null。

      namespace ContosoPizza.Models;
      
      public sealed record class Pizza([Required] string Name)
      {
          private ICollection<PizzaCheese>? _cheeses;
      
          public int Id { get; set; }
      
          [Range(0, 9999.99)]
          public decimal Price { get; set; }
      
          public PizzaSize Size { get; set; }
          public PizzaCrust Crust { get; set; }
          public PizzaSauce Sauce { get; set; }
      
          public ICollection<PizzaCheese> Cheeses
          {
              get => (_cheeses ??= new List<PizzaCheese>());
              set => _cheeses = value ?? throw new ArgumentNullException(nameof(value));
          }
      
          public ICollection<PizzaTopping>? Toppings { get; set; }
      
          public override string ToString() => this.ToDescriptiveString();
      }
      

      Toppings 屬性現在會表示為可為 null。

    2. 將醒目提示的程式碼行新增至 ContosoPizza.Service\Program.cs

      using ContosoPizza.Models;
      
      // Create a pizza
      Pizza pizza = new("Meat Lover's Special")
      {
          Size = PizzaSize.Medium,
          Crust = PizzaCrust.DeepDish,
          Sauce = PizzaSauce.Marinara,
          Price = 17.99m,
      };
      
      // Add cheeses
      pizza.Cheeses.Add(PizzaCheese.Mozzarella);
      pizza.Cheeses.Add(PizzaCheese.Parmesan);
      
      // Add toppings
      pizza.Toppings ??= new List<PizzaTopping>();
      pizza.Toppings.Add(PizzaTopping.Sausage);
      pizza.Toppings.Add(PizzaTopping.Pepperoni);
      pizza.Toppings.Add(PizzaTopping.Bacon);
      pizza.Toppings.Add(PizzaTopping.Ham);
      pizza.Toppings.Add(PizzaTopping.Meatballs);
      
      Console.WriteLine(pizza);
      
      /*
          Expected output:
      
          The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
          It's covered with a blend of mozzarella and parmesan cheese.
          It's layered with sausage, pepperoni, bacon, ham and meatballs.
          This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
      */
      

    在上述程式碼中,會使用 null 聯合運算子將 Toppings 指派至 new List<PizzaTopping>(); (如果其為 null)。

執行完成的解決方案

  1. 儲存您的所有變更,然後建置解決方案。

    dotnet build
    

    建置完成,且不出現任何警告或錯誤。

  2. 執行應用程式。

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    應用程式執行到完成 (不出現錯誤) 且顯示以下輸出:

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    The "Meat Lover's Special" is a deep dish pizza with marinara sauce.
    It's covered with a blend of mozzarella and parmesan cheese.
    It's layered with sausage, pepperoni, bacon, ham and meatballs.
    This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
    

摘要

在本單元中,您使用了可為 null 的內容來識別並防止程式碼中出現可能的 NullReferenceException