Упражнение: Применение стратегий безопасной работы с 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 в коде.