练习 - 应用 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! */
启用可为空上下文
现在,你将启用可为空上下文并检查其对生成的影响。
在 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
项目启用可为空上下文。在 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
项目启用可为空上下文。使用
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
检查 - 表达属性可为空的意图
- 使用属性初始化表达式通过默认(空)值内联初始化集合
- 在构造函数中为属性分配一个默认(空)值
若要修复
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(); }
在上述代码中:
- 添加了一个新的支持字段以帮助截获名为
_cheeses
的get
和set
属性访问器。 它被声明为可为空 (?
) 并且未初始化。 get
访问器映射到使用 null 合并运算符 (??
) 的表达式。 此表达式返回_cheeses
字段,假设它不是null
。 如果是null
,则在返回_cheeses
之前将_cheeses
赋予new List<PizzaCheese>()
。set
访问器也映射到表达式并使用 null 合并运算符。 当使用者赋予null
值时,会引发 ArgumentNullException。
- 添加了一个新的支持字段以帮助截获名为
由于并非所有披萨都有配料,因此
null
可能是Pizza.Toppings
属性的有效值。 在这种情况下,将其表达为可为空是有意义的。修改 Pizza.cs 上的属性定义以允许
Toppings
可为空。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
属性表示为可为空。将突出显示的行添加到 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! */
在前面的代码中,如果
Toppings
是null
,则使用 null 合并运算符将其赋予new List<PizzaTopping>();
。
运行已完成的解决方案
保存所有更改,然后生成解决方案。
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!
总结
在此单元中,你使用可为空上下文来识别和阻止代码中可能出现的 NullReferenceException
。