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

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

  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 для свойства 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, и просматриваете его воздействие на сборку.

  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 clean можно принудительно перестроить все сборки в проекте перед выполнением команды dotnet build.

  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. Некоторыми примерами могут служить:

  • Требуется коллекция сыров и начинок, которая не может быть 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 поле new List<PizzaCheese>() присваивается _cheeses.
    • Метод доступа 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!
    

Итоги

В этом уроке вы использовали нулевой контекст, чтобы обнаружить и предотвратить возможные случаи NullReferenceException в коде.