Esercizio - Applicare strategie di sicurezza dei valori Null
Nell'unità precedente, hai imparato a esprimere l'intento di nullabilità 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-safetyAprire 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.csprojVerrà 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 13L'analisi dello stack indica che l'eccezione si è verificata alla riga 13 in .\src\ContosoPizza.Service\Program.cs. Alla riga 13 il metodo
Addviene 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 buildLa 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.48Compilare di nuovo la soluzione di esempio usando il comando
dotnet build.dotnet buildQuesta 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 cleanprima 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 buildLa 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.95Quando 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 non nullabili che non sono ancora state inizializzate.
Correggere gli errori
Esistono molte tattiche per risolvere gli avvisi/errori relativi alla nullabilità. Alcuni esempi includono:
- Richiedere una raccolta che non ammette i valori Null di formaggi e condimenti come parametri del costruttore
- Intercettare la proprietà
get/sete 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à
getesetdenominato_cheeses. Viene dichiarato come oggetto che ammette i valori Null (?) e lasciato non inizializzato. - L'accessor
getviene mappato a un'espressione che usa l'operatore null-coalescing (??). Questa espressione restituisce il campo_cheeses, presupponendo che non sianull. Se ènull, assegna_cheesesanew List<PizzaCheese>()prima di restituire_cheeses. - Anche la funzione di accesso
setviene 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,
nullpotrebbe essere un valore valido per la proprietàPizza.Toppings. In questo caso, è opportuno esprimerla come annullabile.Modificare la definizione della proprietà in Pizza.cs per consentire a
Toppingsdi 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 nullable.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
Toppingsanew List<PizzaTopping>();se ènull.
Eseguire la soluzione completata
Salvare tutte le modifiche e quindi compilare la soluzione.
dotnet buildLa compilazione viene completata senza avvisi o errori.
Avvia l'app.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csprojL'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à, hai utilizzato un contesto nullable per identificare ed evitare possibili occorrenze di NullReferenceException nel codice.