Упражнение: Применение стратегий безопасной работы с NULL
В предыдущем уроке вы узнали о том, как выражать намерение использования null в коде. На этом уроке вы примените полученные знания при работе с имеющимся проектом 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 для свойства
Add
вызывается методpizza.Cheeses
. Поскольку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 clean
можно принудительно перестроить все сборки в проекте перед выполнением командыdotnet build
.В файлах 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
полеnew List<PizzaCheese>()
присваивается_cheeses
. - Метод доступа
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!
Итоги
В этом уроке вы использовали нулевой контекст, чтобы обнаружить и предотвратить возможные случаи NullReferenceException
в коде.