Exercice : appliquer des stratégies de sécurité des valeurs Null
Dans l’unité précédente, vous avez appris à exprimer l’intention de possibilité de valeur Null dans le code. Dans cette leçon, vous allez appliquer ce que vous avez appris à un projet C# existant.
Notes
Ce module utilise l’interface CLI .NET et Visual Studio Code pour le développement local. À l’issue de ce module, vous pourrez appliquer les concepts avec Visual Studio (Windows) ou Visual Studio pour Mac (macOS), ou poursuivre le développement avec Visual Studio Code (Windows, Linux et macOS).
Ce module utilise le SDK .NET 6.0. Assurez-vous que vous avez installé .NET 6.0 en exécutant la commande suivante dans votre terminal préféré :
dotnet --list-sdks
Une sortie similaire à ce qui suit s’affiche :
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
Vérifiez que la liste comporte une version commençant par 6
. S’il n’y en a pas ou que la commande est introuvable, installez la dernière version du kit SDK .NET 6.0.
Récupérer et examiner l’exemple de code
Dans un terminal de commande, clonez l’exemple de dépôt GitHub et basculez vers le répertoire cloné.
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safety
Ouvrez le répertoire du projet dans Visual Studio Code.
code .
Exécutez le projet d’exemple à l’aide de la commande
dotnet run
.dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Cela entraînera la levée d’une exception 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
Le rapport des appels de procédure indique que l’exception s’est produite au niveau de la ligne 13 dans .\src\ContosoPizza.Service\Program.cs. Dans la ligne 13, la méthode
Add
est appelée sur la propriétépizza.Cheeses
. Étant donné quepizza.Cheeses
estnull
, une exception NullReferenceException est levée.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! */
Activer un contexte pouvant accepter la valeur Null
Vous allez maintenant activer un contexte pouvant accepter la valeur Null et examiner son effet sur la compilation.
Dans src/ContosoPizza. service/ContosoPizza.Service.csproj, ajoutez la ligne en surbrillance et enregistrez vos modifications :
<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 modification précédente active le contexte pouvant accepter la valeur Null pour l’ensemble du projet
ContosoPizza.Service
.Dans src/ContosoPizza.Models/ContosoPizza.Models.csproj, ajoutez la ligne en surbrillance et enregistrez vos modifications :
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
La modification précédente active le contexte pouvant accepter la valeur Null pour l’ensemble du projet
ContosoPizza.Models
.Compiler l’exemple de solution à l’aide de la commande
dotnet build
.dotnet build
La compilation réussit avec deux avertissements.
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
Compilez l’exemple de solution à l’aide de la commande une nouvelle fois à l’aide de la commande
dotnet build
.dotnet build
Cette fois-ci, la compilation réussit sans erreurs ni avertissements. La compilation précédente s’était terminée avec succès, avec des avertissements. Étant donné que la source n’a pas changé, le processus de compilation ne réexécute pas le compilateur. Étant donné que la compilation n’exécute pas le compilateur, les avertissements ont disparu.
Conseil
Vous pouvez forcer une recompilation de tous les assemblys d’un projet à l’aide de la commande
dotnet clean
avant d’exécuterdotnet build
.Dans les fichiers .csproj, ajoutez les lignes en surbrillance et enregistrez vos modifications.
<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>
Les modifications précédentes indiquent au compilateur de faire échouer la compilation chaque fois qu’un avertissement est rencontré.
Conseil
L’utilisation de
<TreatWarningsAsErrors>
est facultative. Toutefois, nous vous la recommandons, car cela vous permet de passer en revue tous les avertissements.Compiler l’exemple de solution à l’aide de la commande
dotnet build
.dotnet build
La compilation échoue avec 2 erreurs.
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
Lors du traitement des avertissements en tant qu’erreurs, l’application n’est plus compilée. Cela est en fait souhaité dans cette situation, car le nombre d’erreurs est faible et nous les traiterons rapidement. Les deux erreurs (CS8618) vous permettent de savoir qu’il existe des propriétés déclarées non-nullables qui n’ont pas encore été initialisées.
Corriger les erreurs
Il existe beaucoup de méthodes pour résoudre les avertissements/erreurs liés à la possibilité de valeur Null. Voici quelques exemples :
- Exiger une collection non-nullable de fromages et de garnitures comme paramètres du constructeur
- Intercepter la propriété
get
/set
et ajoutez une vérificationnull
- Exprimer l’intention de possibilité de valeur Null pour les propriétés
- Initialiser la collection avec une valeur inlined (vide) par défaut à l’aide d’initialiseurs de propriété
- Attribuer à la propriété une valeur (vide) par défaut dans le constructeur
Pour corriger l’erreur sur la propriété
Pizza.Cheeses
, modifiez la définition de la propriété sur Pizza.cs pour ajouter une vérificationnull
. Une pizza n’est pas vraiment une pizza sans fromage.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(); }
Dans le code précédent :
- Un nouveau champ de stockage a été ajouté pour faciliter l’interception des accesseurs de propriété
get
etset
nommés_cheeses
. Il est déclaré comme pouvant accepter la valeur Null (?
) et reste non initialisé. - L’accesseur
get
est mappé à une expression qui utilise l’opérateur de coalescence nulle (??
). Cette expression retourne le champ_cheeses
, en supposant qu’il ne s’agit pas denull
. S’il s’agit denull
, il affecte_cheeses
ànew List<PizzaCheese>()
avant de retourner_cheeses
. - L’accesseur
set
est également mappé à une expression et utilise l’opérateur de coalescence nulle. Lorsqu’un consommateur attribue une valeurnull
, l’exception ArgumentNullException est levée.
- Un nouveau champ de stockage a été ajouté pour faciliter l’interception des accesseurs de propriété
Dans la mesure où toutes les pizzas n’ont pas de garniture personnalisable,
null
peut constituer une valeur valide pour la propriétéPizza.Toppings
. Dans ce cas, il est logique que la valeur puisse accepter la valeur Null.Modifiez la définition de propriété dans Pizza.cs pour autoriser
Toppings
à avoir une valeur 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 propriété
Toppings
est désormais exprimée comme acceptant la valeur Null.Ajoutez la ligne en surbrillance à 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! */
Dans le code précédent, l’opérateur de coalescence nulle a été utilisé pour attribuer
Toppings
ànew List<PizzaTopping>();
s’il s’agit denull
.
Exécuter la solution finale
Enregistrez vos modifications, puis compilez la solution.
dotnet build
La compilation se termine sans erreurs ni avertissements.
Exécutez l’application.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
L’application s’exécute jusqu’à la fin (sans erreur) et affiche la sortie suivante :
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!
Résumé
Dans cette leçon, vous avez utilisé un contexte pouvant accepter la valeur Null pour identifier et empêcher les occurrences potentielles de NullReferenceException
dans votre code.