Övning – Tillämpa null-säkerhetsstrategier

Slutförd

I föregående lektion lärde du dig att uttrycka avsikten med nullabilitet i kod. I den här lektionen använder du det du har lärt dig för ett befintligt C#-projekt.

Kommentar

Den här modulen använder .NET CLI (Kommandoradsgränssnitt) och Visual Studio Code för lokal utveckling. När du har slutfört den här modulen kan du använda begreppen med Hjälp av Visual Studio (Windows), Visual Studio för Mac (macOS) eller fortsatt utveckling med Hjälp av Visual Studio Code (Windows, Linux och macOS).

Den här modulen använder .NET 6.0 SDK. Kontrollera att du har .NET 6.0 installerat genom att köra följande kommando i önskad terminal:

dotnet --list-sdks

Utdata som liknar följande visas:

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

Kontrollera att en version som börjar med 6 visas. Om inget visas eller om kommandot inte hittas installerar du den senaste .NET 6.0 SDK:t.

Hämta och granska exempelkoden

  1. I en kommandoterminal klonar du GitHub-exempellagringsplatsen och växlar till den klonade katalogen.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Öppna projektkatalogen i Visual Studio Code.

    code .
    
  3. Kör exempelprojektet med kommandot dotnet run .

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

    Detta resulterar i att en NullReferenceException utlöses.

    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
    

    Stackspårningen anger att undantaget inträffade på rad 13 i .\src\ContosoPizza.Service\Program.cs. På rad 13 Add anropas metoden för pizza.Cheeses egenskapen . Eftersom pizza.Cheeses är null, kastas en 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!
    */
    

Aktivera null-kontext

Nu ska du aktivera en nullbar kontext och undersöka dess effekt på bygget.

  1. I src/ContosoPizza.Service/ContosoPizza.Service.csproj lägger du till den markerade raden och sparar ändringarna:

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

    Föregående ändring aktiverar den nullbara kontexten för hela ContosoPizza.Service projektet.

  2. I src/ContosoPizza.Models/ContosoPizza.Models.csproj lägger du till den markerade raden och sparar ändringarna:

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

    Föregående ändring aktiverar den nullbara kontexten för hela ContosoPizza.Models projektet.

  3. Skapa exempellösningen dotnet build med kommandot .

    dotnet build
    

    Bygget lyckas med två varningar.

    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. Skapa exempellösningen igen med kommandot dotnet build .

    dotnet build
    

    Den här gången lyckas bygget utan fel eller varningar. Den tidigare versionen slutfördes med varningar. Eftersom källan inte ändrades kör byggprocessen inte kompilatorn igen. Eftersom kompilatorn inte körs finns det inga varningar.

    Dricks

    Du kan tvinga fram en ombyggnad av alla sammansättningar i ett projekt med hjälp dotnet clean av kommandot före dotnet build.

  5. I .csproj-filerna lägger du till de markerade raderna och sparar ändringarna.

    <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 tidigare ändringarna instruerar kompilatorn att misslyckas med bygget när en varning påträffas.

    Dricks

    Det är valfritt att använda <TreatWarningsAsErrors> . Vi rekommenderar det dock eftersom det säkerställer att du inte förbiser några varningar.

  6. Skapa exempellösningen dotnet build med kommandot .

    dotnet build
    

    Bygget misslyckas med 2 fel.

    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
    

    När du behandlar varningar som fel skapas inte längre appen. Detta är faktiskt önskvärt i den här situationen, eftersom antalet fel är litet och vi kommer snabbt att åtgärda dem. De två felen (CS8618) låter dig veta att det finns egenskaper som deklarerats som icke-nullbara som ännu inte har initierats.

Åtgärda felen

Det finns många metoder för att lösa varningar/fel som rör nullbarhet. Vissa exempel inkluderar:

  • Kräv en icke-nullbar samling ostar och pålägg som konstruktorparametrar
  • Fånga upp egenskapen get/set och lägg till en null kontroll
  • Uttrycka avsikten att egenskaperna ska vara nullbara
  • Initiera samlingen med ett standardvärde (tomt) infogat med egenskapsinitierare
  • Tilldela egenskapen ett standardvärde (tomt) i konstruktorn
  1. Åtgärda felet på Pizza.Cheeses egenskapen genom att ändra egenskapsdefinitionen på Pizza.cs för att lägga till en null kontroll. Det är inte riktigt en pizza utan ost, eller hur?

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

    I koden ovan:

    • Ett nytt bakgrundsfält läggs till för att hjälpa till att fånga upp egenskapsåtkomsterna get och set med namnet _cheeses. Den deklareras som nullbar (?) och lämnas oinitierad.
    • Accessorn get mappas till ett uttryck som använder null-coalescing-operatorn (??). Det här uttrycket returnerar fältet _cheeses , förutsatt att det inte nullär . Om den är nulltilldelar _cheeses den till new List<PizzaCheese>() innan den returnerar _cheeses.
    • Accessorn set mappas också till ett uttryck och använder operatorn null-coalescing. När en konsument tilldelar ett null värde ArgumentNullException genereras.
  2. Eftersom inte alla pizzor har toppings kan null vara ett giltigt värde för egenskapen Pizza.Toppings . I det här fallet är det klokt att uttrycka det som nullbart.

    1. Ändra egenskapsdefinitionen på Pizza.cs så att den kan Toppings vara 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();
      }
      

      Egenskapen Toppings uttrycks nu som nullbar.

    2. Lägg till den markerade raden i 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!
      */
      

    I föregående kod används operatorn null-coalescing för att tilldela Toppings till new List<PizzaTopping>(); om den är null.

Kör den slutförda lösningen

  1. Spara alla dina ändringar och skapa sedan lösningen.

    dotnet build
    

    Bygget slutförs utan varningar eller fel.

  2. Kör appen.

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

    Appen körs till slutförande (utan fel) och visar följande utdata:

    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!
    

Sammanfattning

I den här lektionen använde du en nullbar kontext för att identifiera och förhindra möjliga NullReferenceException förekomster i koden.