Упражнение. Применение стратегий безопасности использования значения NULL
В предыдущем уроке вы узнали о выражении намерения nullability в коде. На этом уроке вы примените полученные знания при работе с имеющимся проектом C#.
Примечание.
В этом модуле используются .NET CLI (интерфейс командной строки) и Visual Studio Code для локальной разработки. По завершении этого модуля вы сможете применять его основные понятия, используя Visual Studio (Windows), Visual Studio для Mac (macOS), или продолжить разработку с помощью Visual Studio Code (Windows, Linux и macOS).
Этот модуль использует пакет SDK для .NET 6.0. Убедитесь, что у вас установлена платформа .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
. Если таких версий нет или эта команда не найдена, установите новейшую версию пакета SDK для .NET 6.0.
Получение и изучение примера кода
В окне командного терминала клонируйте пример репозитория 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
Трассировка стека указывает, что исключение произошло в строке 13 в .\src\ContosoPizza.Service\Program.cs. В строке 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>
Предыдущее изменение включает контекст, допускающий значение NULL, для всего проекта
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>
Предыдущее изменение включает контекст, допускающий значение NULL, для всего проекта
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. Некоторыми примерами могут служить:
- Требовать непустую коллекцию сыров и начинок в качестве параметров конструктора
- Перехват свойства
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
в коде.