Ćwiczenie — stosowanie strategii bezpieczeństwa dla wartości null

Ukończone

W poprzedniej lekcji nauczyłeś się, jak wyrażać intencję stosowania null w kodzie. W tej lekcji zastosujesz zdobytą wiedzę do istniejącego projektu w języku C#.

Uwaga

W tym module używany jest interfejs wiersza polecenia platformy .NET (interfejs wiersza polecenia) i program Visual Studio Code na potrzeby programowania lokalnego. Po ukończeniu tego modułu można zastosować koncepcje przy użyciu programu Visual Studio (Windows), Visual Studio dla komputerów Mac (macOS) lub dalszego programowania przy użyciu programu Visual Studio Code (Windows, Linux i macOS).

W tym module jest używany zestaw .NET 6.0 SDK. Upewnij się, że masz zainstalowany program .NET 6.0, uruchamiając następujące polecenie w preferowanym terminalu:

dotnet --list-sdks

Pojawia się wynik podobny do następującego:

3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]

Upewnij się, że na liście znajduje się wersja rozpoczynająca się od 6 . Jeśli żadna nie jest wymieniona lub nie można znaleźć polecenia, zainstaluj najnowszy zestaw SDK platformy .NET 6.0.

Pobieranie i badanie przykładowego kodu

  1. W terminalu poleceń sklonuj przykładowe repozytorium GitHub i przełącz się do sklonowanego katalogu.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Otwórz katalog projektu w programie Visual Studio Code.

    code .
    
  3. Uruchom przykładowy projekt przy użyciu dotnet run polecenia .

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    Spowoduje to rzucenie 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
    

    Ślad stosu wskazuje, że wyjątek wystąpił w wierszu 13 w .\src\ContosoPizza.Service\Program.cs. W wierszu 13 metoda Add jest wywoływana na właściwości pizza.Cheeses. Ponieważ pizza.Cheeses jest null, NullReferenceException jest generowany.

    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!
    */
    

Włącz kontekst dopuszczający wartości null

Teraz włączysz kontekst nullowalny i sprawdzisz jego wpływ na kompilację.

  1. W pliku src/ContosoPizza.Service/ContosoPizza.Service.csproj dodaj wyróżniony wiersz i zapisz zmiany:

    <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>
    

    Poprzednia zmiana umożliwia kontekst dopuszczający wartość null dla całego ContosoPizza.Service projektu.

  2. W pliku src/ContosoPizza.Models/ContosoPizza.Models.csproj dodaj wyróżniony wiersz i zapisz zmiany:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
    </Project>
    

    Poprzednia zmiana umożliwia kontekst dopuszczający wartość null dla całego ContosoPizza.Models projektu.

  3. Skompiluj przykładowe rozwiązanie przy użyciu dotnet build polecenia .

    dotnet build
    

    Kompilacja zakończona pomyślnie z 2 ostrzeżeniami.

    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. Ponownie skompiluj przykładowe rozwiązanie przy użyciu dotnet build polecenia .

    dotnet build
    

    Tym razem kompilacja zakończy się powodzeniem bez błędów ani ostrzeżeń. Poprzednia kompilacja została ukończona pomyślnie z ostrzeżeniami. Ponieważ źródło nie uległo zmianie, proces kompilacji nie uruchamia ponownie kompilatora. Ponieważ kompilacja nie uruchamia kompilatora, nie ma żadnych ostrzeżeń.

    Napiwek

    Można wymusić ponowne kompilowanie wszystkich zestawów w projekcie, używając polecenia dotnet clean przed dotnet build.

  5. W plikach csproj dodaj wyróżnione wiersze i zapisz zmiany.

    <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>
    

    Poprzednie zmiany instruują kompilatorowi, aby kompilacja zakończyła się niepowodzeniem przy każdym napotkaniu ostrzeżenia.

    Napiwek

    Użycie elementu <TreatWarningsAsErrors> jest opcjonalne. Zalecamy to jednak, ponieważ gwarantuje, że nie przeoczysz żadnych ostrzeżeń.

  6. Skompiluj przykładowe rozwiązanie przy użyciu dotnet build polecenia .

    dotnet build
    

    Kompilacja kończy się niepowodzeniem z 2 błędami.

    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
    

    Podczas traktowania ostrzeżeń jako błędów aplikacja nie będzie już kompilować. Jest to w rzeczywistości pożądane w tej sytuacji, ponieważ liczba błędów jest niewielka i szybko je zajmiemy. Dwa błędy (CS8618) informują o tym, że istnieją właściwości zadeklarowane jako niemogące być null, które nie zostały jeszcze zainicjowane.

Naprawianie błędów

Istnieje wiele taktyk rozwiązywania ostrzeżeń/błędów związanych z wartością null. Przykłady obejmują:

  • Wymagaj kolekcji serów i dodatków, której nie można ustawić na wartość null, jako parametrów konstruktora
  • Przechwyć właściwość get/set i dodaj sprawdzenie null
  • Wyrażanie intencji, aby właściwości mogły przyjmować wartość null
  • Inicjowanie kolekcji przy użyciu domyślnych (pustych) wartości wbudowanych za pomocą inicjatorów właściwości
  • Przypisz właściwości domyślną wartość (pustą) w konstruktorze
  1. Aby naprawić błąd we właściwości Pizza.Cheeses, zmodyfikuj definicję właściwości w pliku Pizza.cs, aby dodać sprawdzenie null. To nie jest naprawdę pizza bez sera, prawda?

    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();
    }
    

    W poprzednim kodzie:

    • Dodano nowe pole pomocnicze, które pomaga przechwycić metody dostępu właściwości get i set, nazwane _cheeses. Jest deklarowana jako może przyjmować wartość null (?) i pozostawiona niezainicjowana.
    • Akcesorium get jest mapowane na wyrażenie, które używa operatora łączenia wartości null (??). To wyrażenie zwraca pole _cheeses, zakładając, że nie jest to null. Jeśli jest null, przypisuje _cheeses do new List<PizzaCheese>() przed zwróceniem _cheeses.
    • Akcesorium set jest również mapowane na wyrażenie i korzysta z operatora łączenia wartości null. Gdy użytkownik przypisze null wartość, zostanie wyrzucona ArgumentNullException .
  2. Ponieważ nie wszystkie pizze mają dodatki, null może być prawidłową wartością właściwości Pizza.Toppings . W takim przypadku warto wyrazić ją jako mogącą mieć wartość null.

    1. Zmodyfikuj definicję właściwości w Pizza.cs , aby umożliwić używanie Toppings wartości 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();
      }
      

      Właściwość Toppings może teraz przyjmować wartość null.

    2. Dodaj wyróżniony wiersz do domeny 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!
      */
      

    W poprzednim kodzie operator łączenia wartości null jest używany do przypisania wartości Toppings do new List<PizzaTopping>();, jeśli jest null.

Uruchamianie ukończonego rozwiązania

  1. Zapisz wszystkie zmiany, a następnie skompiluj rozwiązanie.

    dotnet build
    

    Kompilacja kończy się bez ostrzeżeń ani błędów.

  2. Uruchom aplikację.

    dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
    

    Aplikacja jest uruchamiana do ukończenia (bez błędu) i wyświetla następujące dane wyjściowe:

    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!
    

Podsumowanie

W tej jednostce użyto kontekstu z dopuszczalną wartością null, aby zidentyfikować i zapobiec możliwym NullReferenceException wystąpieniom w kodzie.