Cvičení – použití strategií zabezpečení s hodnotou null

Dokončeno

V předchozí lekci jste se dozvěděli o vyjádření záměru nullability v kódu. V této lekci použijete to, co jste se naučili v existujícím projektu C#.

Poznámka:

Tento modul používá rozhraní příkazového řádku .NET (rozhraní příkazového řádku) a Visual Studio Code pro místní vývoj. Po dokončení tohoto modulu můžete použít koncepty pomocí sady Visual Studio (Windows), Visual Studio pro Mac (macOS) nebo průběžného vývoje pomocí editoru Visual Studio Code (Windows, Linux a macOS).

Tento modul používá sadu .NET 6.0 SDK. Spuštěním následujícího příkazu v upřednostňovaném terminálu se ujistěte, že máte nainstalovaný .NET 6.0:

dotnet --list-sdks

Zobrazí se výstup podobný následujícímu:

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

Ujistěte se, že je uvedená verze, na 6 které začíná. Pokud žádný není uvedený nebo příkaz nebyl nalezen, nainstalujte nejnovější sadu .NET 6.0 SDK.

Načtení a prozkoumání vzorového kódu

  1. V příkazovém terminálu naklonujte ukázkové úložiště GitHub a přepněte do klonovaného adresáře.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Otevřete adresář projektu v editoru Visual Studio Code.

    code .
    
  3. Spusťte ukázkový projekt pomocí dotnet run příkazu.

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

    Výsledkem bude NullReferenceException vyvolání.

    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
    

    Trasování zásobníku označuje, že došlo k výjimce na řádku 13 v souboru .\src\ContosoPizza.Service\Program.cs. Na řádku 13 je Add metoda volána ve pizza.Cheeses vlastnosti. Protože pizza.Cheeses je , NullReferenceException je nullvyvolán.

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

Povolení kontextu s možnou hodnotou null

Teď povolíte kontext s možnou hodnotou null a prozkoumáte jeho účinek na sestavení.

  1. V souboru src/ContosoPizza.Service/ContosoPizza.Service.csproj přidejte zvýrazněný řádek a uložte změny:

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

    Předchozí změna umožňuje kontext s možnou hodnotou null pro celý ContosoPizza.Service projekt.

  2. V souboru src/ContosoPizza.Models/ContosoPizza.Models.csproj přidejte zvýrazněný řádek a uložte změny:

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

    Předchozí změna umožňuje kontext s možnou hodnotou null pro celý ContosoPizza.Models projekt.

  3. Pomocí příkazu sestavte ukázkové řešení dotnet build .

    dotnet build
    

    Sestavení proběhne úspěšně s 2 upozorněními.

    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. Znovu sestavte ukázkové řešení pomocí dotnet build příkazu.

    dotnet build
    

    Tentokrát bude sestavení úspěšné bez chyb nebo upozornění. Předchozí sestavení bylo úspěšně dokončeno s upozorněními. Vzhledem k tomu, že se zdroj nezměnil, proces sestavení znovu nespustí kompilátor. Vzhledem k tomu, že sestavení nespustí kompilátor, neexistují žádná upozornění.

    Tip

    Opětovné sestavení všech sestavení v projektu můžete vynutit pomocí dotnet clean příkazu před dotnet build.

  5. V souborech .csproj přidejte zvýrazněné řádky a uložte změny.

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

    Předchozí změny instruují kompilátoru, aby sestavení selhalo při každém zobrazení upozornění.

    Tip

    Použití <TreatWarningsAsErrors> je volitelné. Doporučujeme ho ale proto, abyste nepřehlédli žádná upozornění.

  6. Pomocí příkazu sestavte ukázkové řešení dotnet build .

    dotnet build
    

    Sestavení selže s chybami 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
    

    Při zpracování upozornění jako chyb už aplikace nevytvoří sestavení. To je ve skutečnosti žádoucí v této situaci, protože počet chyb je malý a my je rychle vyřešíme. Dvě chyby (CS8618) umožňují zjistit, že jsou vlastnosti deklarované jako nenulové, které ještě nebyly inicializovány.

Oprava chyb

Existuje mnoho taktik k vyřešení upozornění nebo chyb souvisejících s nulovostí. Mezi některé příklady patří:

  • Vyžadovat nenulovou kolekci sýrů a zastavování jako parametry konstruktoru
  • Zachycení vlastnosti get/set a přidání kontroly null
  • Vyjádření záměru pro vlastnosti, které mají být nullable
  • Inicializace kolekce s výchozí (prázdnou) hodnotou vloženou pomocí inicializátorů vlastností
  • Přiřaďte vlastnost výchozí (prázdnou) hodnotu v konstruktoru.
  1. Chcete-li opravit chybu vlastnosti Pizza.Cheeses , upravte definici vlastnosti v Pizza.cs přidat null kontrolu. Není to opravdu pizza bez sýra, že?

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

    V předchozím kódu:

    • Přidá se nové pomocné pole, které pomáhá zachytit get přístupové objekty a set přístupové objekty vlastností s názvem _cheeses. Deklaruje se jako nullable (?) a ponechá neinicializovaný.
    • Přístupový get objekt je namapován na výraz, který používá operátor nulového sjednocení (??). Tento výraz vrátí pole za předpokladu _cheeses , že není null. Pokud je null, přiřadí _cheeses se new List<PizzaCheese>() před vrácením _cheeses.
    • Přístupový set objekt je také mapován na výraz a používá operátor nulového sjednocení. Když příjemce přiřadí null hodnotu, je ArgumentNullException vyvolán.
  2. Vzhledem k tomu, že ne všechny pizzy mají zastavování, null může být platná hodnota pro Pizza.Toppings vlastnost. V tomto případě je vhodné ho vyjádřit jako možnou hodnotu null.

    1. Upravte definici vlastnosti v Pizza.cs tak, aby umožňovala Toppings hodnotu 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();
      }
      

      Vlastnost Toppings je nyní vyjádřena jako nullable.

    2. Přidejte zvýrazněný řádek do 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!
      */
      

    V předchozím kódu se operátor null-coalescing používá k přiřazení Toppings , new List<PizzaTopping>(); pokud je null.

Spuštění dokončeného řešení

  1. Uložte všechny provedené změny a pak sestavte řešení.

    dotnet build
    

    Sestavení se dokončí bez upozornění nebo chyb.

  2. Spustit aplikaci.

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

    Aplikace se spustí na dokončení (bez chyby) a zobrazí následující výstup:

    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!
    

Shrnutí

V této lekci jste použili kontext s možnou hodnotou null k identifikaci a zabránění možným NullReferenceException výskytům v kódu.