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

Ukończone

W poprzedniej lekcji przedstawiono sposób wyrażania intencji dopuszczalności 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ę dane wyjściowe podobne 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 na liście nie ma żadnej listy lub nie można odnaleźć 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 zgłoszenie 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 wystąpił wyjątek w wierszu 13 w pliku .\src\ContosoPizza.Service\Program.cs. W wierszu 13 Add metoda jest wywoływana we pizza.Cheeses właściwości . Ponieważ pizza.Cheeses element to null, NullReferenceException jest zgłaszany.

    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łączanie kontekstu dopuszczanego do wartości null

Teraz włączysz kontekst dopuszczany do wartości null 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ńczy się 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

    Ponowne kompilowanie wszystkich zestawów w projekcie można wymusić za pomocą polecenia wcześniejszego dotnet clean niż 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 błędami 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
    

    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) poinformuj o tym, że istnieją właściwości zadeklarowane jako niepuste, 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 niepustej kolekcji serów i dodatków jako parametrów konstruktora
  • Przechwytywanie właściwości get/set i dodawanie sprawdzania null
  • Wyrażanie intencji dla właściwości, które mają być dopuszczane do wartości null
  • Inicjowanie kolekcji przy użyciu domyślnych (pustych) wartości wbudowanych za pomocą inicjatorów właściwości
  • Przypisz właściwość domyślną (pustą) wartość w konstruktorze
  1. Aby naprawić błąd Pizza.Cheeses we właściwości, zmodyfikuj definicję właściwości w Pizza.cs , aby dodać null sprawdzenie. 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();
    }
    

    Powyższy kod:

    • Dodano nowe pole pomocnicze, które pomaga przechwycić get metody dostępu właściwości i set o nazwie _cheeses. Jest zadeklarowana jako dopuszczana do wartości 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, przy założeniu _cheeses , że nie nulljest to . Jeśli jest nullto wartość , zostanie ona przypisana _cheeses do new List<PizzaCheese>() przed zwróceniem _cheeseswartości .
    • 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 dopuszczaną do wartości 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 jest teraz wyrażana jako dopuszczana do wartości 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 Toppings elementu , new List<PizzaTopping>(); jeśli jest to 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 lekcji użyto kontekstu dopuszczanego do wartości null, aby zidentyfikować i zapobiec możliwym NullReferenceException wystąpieniom w kodzie.