Oefening: strategieën voor null-veiligheid toepassen

Voltooid

In de vorige les hebt u geleerd over het uitdrukken van uw intentie voor null-baarheid in code. In deze les past u toe wat u hebt geleerd op een bestaand C#-project.

Notitie

Deze module maakt gebruik van de .NET CLI (Opdrachtregelinterface) en Visual Studio Code voor lokale ontwikkeling. Nadat u deze module hebt voltooid, kunt u de concepten toepassen met behulp van Visual Studio (Windows), Visual Studio voor Mac (macOS) of continue ontwikkeling met behulp van Visual Studio Code (Windows, Linux en macOS).

In deze module wordt de .NET 6.0 SDK gebruikt. Zorg ervoor dat .NET 6.0 is geïnstalleerd door de volgende opdracht uit te voeren in de terminal van uw voorkeur:

dotnet --list-sdks

Uitvoer van de volgende strekking weergegeven:

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

Zorg ervoor dat er een versie wordt vermeld die begint met 6 . Als er geen wordt vermeld of de opdracht niet wordt gevonden, installeert u de meest recente .NET 6.0 SDK.

De voorbeeldcode ophalen en onderzoeken

  1. Kloon in een opdrachtterminal de GitHub-voorbeeldopslagplaats en schakel over naar de gekloonde map.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Open de projectmap in Visual Studio Code.

    code .
    
  3. Voer het voorbeeldproject uit met behulp van de dotnet run opdracht.

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

    Dit leidt tot een NullReferenceException gegooid.

    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
    

    De stacktracering geeft aan dat de uitzondering is opgetreden op regel 13 in .\src\ContosoPizza.Service\Program.cs. Op regel 13 wordt de Add methode aangeroepen op de pizza.Cheeses eigenschap. Omdat pizza.Cheeses is null, wordt er een NullReferenceException gegooid.

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

Nu schakelt u een null-context in en bekijkt u het effect ervan op de build.

  1. Voeg in src/ContosoPizza.Service/ContosoPizza.Service.csproj de gemarkeerde regel toe en sla uw wijzigingen op:

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

    Met de voorgaande wijziging wordt de null-context voor het hele ContosoPizza.Service project ingeschakeld.

  2. Voeg in src/ContosoPizza.Models/ContosoPizza.Models.csproj de gemarkeerde regel toe en sla uw wijzigingen op:

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

    Met de voorgaande wijziging wordt de null-context voor het hele ContosoPizza.Models project ingeschakeld.

  3. Bouw de voorbeeldoplossing met behulp van de dotnet build opdracht.

    dotnet build
    

    De build slaagt met 2 waarschuwingen.

    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. Bouw de voorbeeldoplossing opnieuw met behulp van de dotnet build opdracht.

    dotnet build
    

    Deze keer slaagt de build zonder fouten of waarschuwingen. De vorige build is voltooid, met waarschuwingen. Omdat de bron niet is gewijzigd, voert het buildproces de compiler niet opnieuw uit. Omdat de build de compiler niet uitvoert, zijn er geen waarschuwingen.

    Tip

    U kunt een herbouw van alle assembly's in een project afdwingen met behulp van de dotnet clean opdracht voorafgaand aan dotnet build.

  5. Voeg in de .csproj-bestanden de gemarkeerde regels toe en sla uw wijzigingen op.

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

    De vorige wijzigingen geven aan dat de compiler de build mislukt wanneer er een waarschuwing wordt aangetroffen.

    Tip

    Het gebruik van <TreatWarningsAsErrors> is optioneel. Het wordt echter aangeraden om ervoor te zorgen dat u geen waarschuwingen over het hoofd ziet.

  6. Bouw de voorbeeldoplossing met behulp van de dotnet build opdracht.

    dotnet build
    

    De build mislukt met 2 fouten.

    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
    

    Bij het behandelen van waarschuwingen als fouten, wordt de app niet meer gebouwd. Dit is in feite gewenst in deze situatie, omdat het aantal fouten klein is en we zullen ze snel aanpakken. De twee fouten (CS8618) laten u weten dat er eigenschappen zijn gedeclareerd als niet-nullable die nog niet zijn geïnitialiseerd.

De fouten oplossen

Er zijn veel tactieken om de waarschuwingen/fouten met betrekking tot null-baarheid op te lossen. Enkele voorbeelden:

  • Een niet-nullable verzameling kaas en toppings vereisen als constructorparameters
  • De eigenschap get/set onderscheppen en een null controle toevoegen
  • De intentie uitdrukken voor de eigenschappen die null kunnen worden
  • Initialiseer de verzameling met een standaardwaarde (lege) inline met behulp van initialisatieprogramma's van eigenschappen
  • De eigenschap een standaardwaarde (leeg) toewijzen in de constructor
  1. Als u de fout in de Pizza.Cheeses eigenschap wilt oplossen, wijzigt u de eigenschapsdefinitie op Pizza.cs om een null controle toe te voegen. Het is niet echt een pizza zonder kaas, hè?

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

    In de voorgaande code:

    • Er wordt een nieuw backingveld toegevoegd om de toegangsrechten voor eigenschappen te onderscheppen met set de get naam _cheeses. Deze wordt gedeclareerd als nullable (?) en niet-geïnitialiseerd.
    • De get accessor wordt toegewezen aan een expressie die gebruikmaakt van de operator null-coalescing (??). Deze expressie retourneert het _cheeses veld, ervan uitgaande dat dit niet nullhet is. Als dit het isnull, wordt het toegewezen new List<PizzaCheese>() aan voordat u terugkeert _cheeses_cheeses.
    • De set accessor wordt ook toegewezen aan een expressie en maakt gebruik van de operator null-coalescing. Wanneer een consument een null waarde toewijst, wordt deze ArgumentNullException gegenereerd.
  2. Omdat niet alle pizza's toppings hebben, null kan dit een geldige waarde voor de Pizza.Toppings eigenschap zijn. In dit geval is het zinvol om het uit te drukken als nullable.

    1. Wijzig de eigenschapsdefinitie op Pizza.cs zodat deze Toppings nullable kan zijn.

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

      De Toppings eigenschap wordt nu uitgedrukt als nullable.

    2. Voeg de gemarkeerde regel toe aan 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!
      */
      

    In de voorgaande code wordt de operator null-coalescing gebruikt om toe te wijzen Toppingsnew List<PizzaTopping>(); als dit het geval is null.

De voltooide oplossing uitvoeren

  1. Sla al uw wijzigingen op en bouw vervolgens de oplossing.

    dotnet build
    

    De build wordt zonder waarschuwingen of fouten voltooid.

  2. De app uitvoeren.

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

    De app wordt uitgevoerd tot voltooiing (zonder fout) en geeft de volgende uitvoer weer:

    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!
    

Samenvatting

In deze les hebt u een null-context gebruikt om mogelijke NullReferenceException gebeurtenissen in uw code te identificeren en te voorkomen.