Упражнение. Применение стратегий безопасности использования значения 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.

Получение и изучение примера кода

  1. В окне командного терминала клонируйте пример репозитория GitHub и переключитесь в клонированный каталог.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Откройте каталог проекта в Visual Studio Code.

    code .
    
  3. Запустите пример проекта с помощью команды 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, и просматриваете его воздействие на сборку.

  1. В 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.

  2. В 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.

  3. Создайте пример решения с помощью команды 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
    
  4. Создайте пример решения снова с помощью команды dotnet build.

    dotnet build
    

    На этот раз сборка будет выполнена без ошибок и предупреждений. Предыдущая сборка была выполнена успешно с предупреждениями. Так как источник остался неизменным, процесс сборки не запускает компилятор снова. Так как при сборке компилятор не запускается, предупреждения не выводятся.

    Совет

    Перед выполнением команды dotnet build можно принудительно перестроить все сборки в проекте с помощью команды dotnet clean.

  5. В файлах 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> необязательно. Однако сделать это рекомендуется, чтобы не пропустить какие-либо предупреждения.

  6. Создайте пример решения с помощью команды 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
  • Инициализация коллекции со значением по умолчанию (пустой) с помощью инициализаторов свойств
  • Назначьте свойству значение по умолчанию (пустое) в конструкторе
  1. Чтобы исправить ошибку в свойстве 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.
  2. Так как не все пиццы имеют начинку, null может быть допустимым значением для свойства Pizza.Toppings. В этом случае имеет смысл выразить его как допускающее значение NULL.

    1. Измените определение свойства в файле 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.

    2. Добавьте выделенную строку в файл 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.

Запуск готового решения

  1. Сохраните все изменения, а затем выполните сборку решения.

    dotnet build
    

    Сборка завершится без ошибок и предупреждений.

  2. Выполнить приложение.

    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 в коде.