Esercizio - Applicare strategie di sicurezza dei valori Null
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
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
Aprire la directory del progetto in Visual Studio Code.
code .
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.
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
.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
.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
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 didotnet build
.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.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 controllonull
- 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
Per correggere l'errore nella proprietà
Pizza.Cheeses
, modificare la definizione della proprietà in Pizza.cs per aggiungere un controllonull
. 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
eset
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 sianull
. Se ènull
, assegna_cheeses
anew 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 valorenull
, viene generata l'eccezione ArgumentNullException.
- Viene aggiunto un nuovo campo sottostante per consentire l'intercettazione delle funzioni di accesso alle proprietà
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.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.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
anew List<PizzaTopping>();
se ènull
.
Eseguire la soluzione completata
Salvare tutte le modifiche e quindi compilare la soluzione.
dotnet build
La compilazione viene completata senza avvisi o errori.
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.