Esercizio - Applicare strategie di sicurezza dei valori Null

Completato

Nell'unità precedente si è appreso come esprimere la finalità di supporto dei valori Null nel codice. In questa unità i concetti appresi verranno applicati a un progetto C# esistente.

Nota

In questo modulo vengono usati l'interfaccia della riga di comando di.NET e Visual Studio Code per lo sviluppo locale. Una volta completato questo modulo, è possibile applicare i concetti usando Visual Studio (Windows), Visual Studio per Mac (macOS) o lo sviluppo continuo tramite Visual Studio Code (Windows, Linux e macOS).

Questo modulo usa .NET 6.0 SDK. Assicurarsi che .NET 6.0 sia installato eseguendo il comando successivo nel terminale preferito:

dotnet --list-sdks

Verrà visualizzato un output simile al seguente:

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

Assicurarsi che sia elencata una versione che inizia con 6. Se non viene elencata alcuna versione o se il comando non viene trovato, installare la versione più recente di .NET 6.0 SDK.

Recuperare ed esaminare il codice di esempio

  1. In un terminale di comando clonare il repository GitHub di esempio e passare alla directory clonata.

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Aprire la directory del progetto in Visual Studio Code.

    code .
    
  3. Eseguire il progetto di esempio usando il comando dotnet run.

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

    Verrà generata un'eccezione 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
    

    L'analisi dello stack indica che l'eccezione si è verificata alla riga 13 in .\src\ContosoPizza.Service\Program.cs. Alla riga 13 il metodo Add viene chiamato sulla proprietà pizza.Cheeses. Poiché pizza.Cheeses è null, viene generata un'eccezione 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!
    */
    

Abilitare un contesto che ammette i valori Null

A questo punto si abiliterà un contesto che ammette i valori Null e se ne esaminerà l'effetto sulla compilazione.

  1. In src/ContosoPizza.Service/ContosoPizza.Service.csproj aggiungere la riga evidenziata e salvare le modifiche:

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

    La modifica precedente abilita il contesto che ammette i valori Null per l'intero progetto ContosoPizza.Service.

  2. In src/ContosoPizza.Models/ContosoPizza.Models.csproj aggiungere la riga evidenziata e salvare le modifiche:

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

    La modifica precedente abilita il contesto che ammette i valori Null per l'intero progetto ContosoPizza.Models.

  3. Compilare la soluzione di esempio usando il comando dotnet build.

    dotnet build
    

    La compilazione ha esito positivo con 2 avvisi.

    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. Compilare di nuovo la soluzione di esempio usando il comando dotnet build.

    dotnet build
    

    Questa volta, la compilazione ha esito positivo senza errori o avvisi. La compilazione precedente è stata completata correttamente, con avvisi. Poiché l'origine non è stata modificata, il processo di compilazione non esegue di nuovo il compilatore. Poiché la compilazione non esegue il compilatore, non sono presenti avvisi.

    Suggerimento

    È possibile forzare la ricompilazione di tutti gli assembly di un progetto usando il comando dotnet clean prima di dotnet build.

  5. Nei file con estensione csproj aggiungere le righe evidenziate e salvare le modifiche.

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

    Le modifiche precedenti indicano al compilatore di non eseguire la compilazione ogni volta che viene rilevato un avviso.

    Suggerimento

    L'uso di <TreatWarningsAsErrors> è facoltativo. Tuttavia, è consigliabile perché garantisce che gli avvisi non vengano ignorati.

  6. Compilare la soluzione di esempio usando il comando dotnet build.

    dotnet build
    

    La compilazione ha esito negativo con 2 errori.

    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
    

    Quando si considerano gli avvisi come errori, l'app non viene più compilata. In effetti, questo è auspicabile in tale situazione, poiché il numero di errori è ridotto e li risolveremo rapidamente. I due errori (CS8618) segnalano la presenza di proprietà dichiarate come proprietà che non ammettono i valori Null che non sono ancora state inizializzate.

Correggere gli errori

Esistono molte tattiche per risolvere gli avvisi/errori correlati al supporto dei valori Null. Alcuni esempi includono:

  • Richiedere una raccolta che non ammette i valori Null di formaggi e condimenti come parametri del costruttore
  • Intercettare la proprietà get/set e aggiungere un controllo null
  • Esprimere la finalità per le proprietà di essere proprietà che ammettono i valori Null
  • Inizializzare la raccolta con un valore predefinito (vuoto) inline usando gli inizializzatori di proprietà
  • Assegnare alla proprietà un valore predefinito (vuoto) nel costruttore
  1. Per correggere l'errore nella proprietà Pizza.Cheeses, modificare la definizione della proprietà in Pizza.cs per aggiungere un controllo null. Non è una pizza senza formaggio, vero?

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

    Nel codice precedente:

    • Viene aggiunto un nuovo campo sottostante per consentire l'intercettazione delle funzioni di accesso alle proprietà get e set denominato _cheeses. Viene dichiarato come oggetto che ammette i valori Null (?) e lasciato non inizializzato.
    • La funzione di accesso get viene mappata a un'espressione che usa l'operatore di coalescenza di valori Null (??). Questa espressione restituisce il campo _cheeses, presupponendo che non sia null. Se è null, assegna _cheeses a new List<PizzaCheese>() prima di restituire _cheeses.
    • Anche la funzione di accesso set viene mappata a un'espressione che usa l'operatore di coalescenza di valori Null. Quando un consumer assegna un valore null, viene generata l'eccezione ArgumentNullException.
  2. Poiché non tutte le pizze hanno condimenti, null potrebbe essere un valore valido per la proprietà Pizza.Toppings. In questo caso, è opportuno esprimerla come proprietà che ammette i valori Null.

    1. Modificare la definizione della proprietà in Pizza.cs per consentire a Toppings di essere una proprietà che ammette i valori 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();
      }
      

      La proprietà Toppings è ora espressa come proprietà che ammette i valori Null.

    2. Aggiungere la riga evidenziata a 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!
      */
      

    Nel codice precedente l'operatore di coalescenza di valori Null viene usato per assegnare Toppings a new List<PizzaTopping>(); se è null.

Eseguire la soluzione completata

  1. Salvare tutte le modifiche e quindi compilare la soluzione.

    dotnet build
    

    La compilazione viene completata senza avvisi o errori.

  2. Eseguire l'app.

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

    L'app viene eseguita fino al completamento (senza errori) e visualizza l'output seguente:

    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!
    

Riepilogo

In questa unità è stato usato un contesto che ammette i valori Null per identificare ed evitare possibili occorrenze di NullReferenceException nel codice.