練習 - 套用 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。
擷取並檢查範例程式碼
在命令終端機中,複製範例 GitHub 存放庫,並切換至複製的目錄。
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safety
在 Visual Studio Code 中開啟專案目錄。
code .
使用
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.Cheeses
是null
,所以會擲回 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 的內容,並檢查其對組建的影響。
在 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 的內容。在 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 的內容。使用
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
使用
dotnet build
命令再次建置範例解決方案。dotnet build
這次,建置會成功且不會出現任何錯誤或警告。 上一次建置成功完成,但出現警告。 由於來源未變更,因此建置程序不會再次執行編譯器。 因為建置不會執行編譯器,所以不會出現警告。
提示
您可以在
dotnet build
之前使用dotnet clean
命令,強制重建專案中的所有組件。在 .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>
。 但我們建議使用,因為這能確保您不會忽略任何警告。使用
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 的意圖
- 使用屬性初始設定式,以預設 (空白) 值內嵌初始化集合
- 在建構函式中,將預設 (空白) 值指派給屬性
為了修正
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(); }
在上述程式碼中:
- 已新增新的支援欄位來協助攔截
get
、set
和名為_cheeses
的屬性存取子。 其會宣告為可為 null (?
) 並保留未初始化。 get
存取子會對應至使用 null 聯合運算子 (??
) 的運算式。 運算式會傳回_cheeses
欄位,假設其不是null
。 如果其為null
,則其會在傳回_cheeses
之前將_cheeses
指派至new List<PizzaCheese>()
。set
存取子也會對應至運算式,並利用 null 聯合運算子。 當取用者指派null
值時,會擲回 ArgumentNullException。
- 已新增新的支援欄位來協助攔截
因為不是所有披薩都有配料,
null
對Pizza.Toppings
屬性而言可能是有效值。 在這種情況下,將其表示為可為 null 是合理的。修改 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。將醒目提示的程式碼行新增至 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
)。
執行完成的解決方案
儲存您的所有變更,然後建置解決方案。
dotnet build
建置完成,且不出現任何警告或錯誤。
執行應用程式。
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
。