Ćwiczenie — stosowanie strategii bezpieczeństwa wartości null
W poprzedniej lekcji przedstawiono sposób wyrażania intencji dopuszczalności null w kodzie. W tej lekcji zastosujesz zdobytą wiedzę do istniejącego projektu w języku C#.
Uwaga
W tym module używany jest interfejs wiersza polecenia platformy .NET (interfejs wiersza polecenia) i program Visual Studio Code na potrzeby programowania lokalnego. Po ukończeniu tego modułu można zastosować koncepcje przy użyciu programu Visual Studio (Windows), Visual Studio dla komputerów Mac (macOS) lub dalszego programowania przy użyciu programu Visual Studio Code (Windows, Linux i macOS).
W tym module jest używany zestaw .NET 6.0 SDK. Upewnij się, że masz zainstalowany program .NET 6.0, uruchamiając następujące polecenie w preferowanym terminalu:
dotnet --list-sdks
Pojawia się dane wyjściowe podobne do następującego:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
Upewnij się, że na liście znajduje się wersja rozpoczynająca się od 6
. Jeśli na liście nie ma żadnej listy lub nie można odnaleźć polecenia, zainstaluj najnowszy zestaw SDK platformy .NET 6.0.
Pobieranie i badanie przykładowego kodu
W terminalu poleceń sklonuj przykładowe repozytorium GitHub i przełącz się do sklonowanego katalogu.
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safety
Otwórz katalog projektu w programie Visual Studio Code.
code .
Uruchom przykładowy projekt przy użyciu
dotnet run
polecenia .dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Spowoduje to zgłoszenie 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
Ślad stosu wskazuje, że wystąpił wyjątek w wierszu 13 w pliku .\src\ContosoPizza.Service\Program.cs. W wierszu 13
Add
metoda jest wywoływana wepizza.Cheeses
właściwości . Ponieważpizza.Cheeses
element tonull
, NullReferenceException jest zgłaszany.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! */
Włączanie kontekstu dopuszczanego do wartości null
Teraz włączysz kontekst dopuszczany do wartości null i sprawdzisz jego wpływ na kompilację.
W pliku src/ContosoPizza.Service/ContosoPizza.Service.csproj dodaj wyróżniony wiersz i zapisz zmiany:
<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>
Poprzednia zmiana umożliwia kontekst dopuszczający wartość null dla całego
ContosoPizza.Service
projektu.W pliku src/ContosoPizza.Models/ContosoPizza.Models.csproj dodaj wyróżniony wiersz i zapisz zmiany:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Poprzednia zmiana umożliwia kontekst dopuszczający wartość null dla całego
ContosoPizza.Models
projektu.Skompiluj przykładowe rozwiązanie przy użyciu
dotnet build
polecenia .dotnet build
Kompilacja zakończy się pomyślnie z 2 ostrzeżeniami.
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
Ponownie skompiluj przykładowe rozwiązanie przy użyciu
dotnet build
polecenia .dotnet build
Tym razem kompilacja zakończy się powodzeniem bez błędów ani ostrzeżeń. Poprzednia kompilacja została ukończona pomyślnie z ostrzeżeniami. Ponieważ źródło nie uległo zmianie, proces kompilacji nie uruchamia ponownie kompilatora. Ponieważ kompilacja nie uruchamia kompilatora, nie ma żadnych ostrzeżeń.
Napiwek
Ponowne kompilowanie wszystkich zestawów w projekcie można wymusić za pomocą polecenia wcześniejszego
dotnet clean
niżdotnet build
.W plikach csproj dodaj wyróżnione wiersze i zapisz zmiany.
<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>
Poprzednie zmiany instruują kompilatorowi, aby kompilacja zakończyła się niepowodzeniem przy każdym napotkaniu ostrzeżenia.
Napiwek
Użycie elementu
<TreatWarningsAsErrors>
jest opcjonalne. Zalecamy to jednak, ponieważ gwarantuje, że nie przeoczysz żadnych ostrzeżeń.Skompiluj przykładowe rozwiązanie przy użyciu
dotnet build
polecenia .dotnet build
Kompilacja kończy się niepowodzeniem z błędami 2.
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
Podczas traktowania ostrzeżeń jako błędów aplikacja nie będzie już kompilować. Jest to w rzeczywistości pożądane w tej sytuacji, ponieważ liczba błędów jest niewielka i szybko je zajmiemy. Dwa błędy (CS8618) poinformuj o tym, że istnieją właściwości zadeklarowane jako niepuste, które nie zostały jeszcze zainicjowane.
Naprawianie błędów
Istnieje wiele taktyk rozwiązywania ostrzeżeń/błędów związanych z wartością null. Przykłady obejmują:
- Wymagaj niepustej kolekcji serów i dodatków jako parametrów konstruktora
- Przechwytywanie właściwości
get
/set
i dodawanie sprawdzanianull
- Wyrażanie intencji dla właściwości, które mają być dopuszczane do wartości null
- Inicjowanie kolekcji przy użyciu domyślnych (pustych) wartości wbudowanych za pomocą inicjatorów właściwości
- Przypisz właściwość domyślną (pustą) wartość w konstruktorze
Aby naprawić błąd
Pizza.Cheeses
we właściwości, zmodyfikuj definicję właściwości w Pizza.cs , aby dodaćnull
sprawdzenie. To nie jest naprawdę pizza bez sera, prawda?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(); }
Powyższy kod:
- Dodano nowe pole pomocnicze, które pomaga przechwycić
get
metody dostępu właściwości iset
o nazwie_cheeses
. Jest zadeklarowana jako dopuszczana do wartości null (?
) i pozostawiona niezainicjowana. - Akcesorium
get
jest mapowane na wyrażenie, które używa operatora łączenia wartości null (??
). To wyrażenie zwraca pole, przy założeniu_cheeses
, że nienull
jest to . Jeśli jestnull
to wartość , zostanie ona przypisana_cheeses
donew List<PizzaCheese>()
przed zwróceniem_cheeses
wartości . - Akcesorium
set
jest również mapowane na wyrażenie i korzysta z operatora łączenia wartości null. Gdy użytkownik przypiszenull
wartość, zostanie wyrzucona ArgumentNullException .
- Dodano nowe pole pomocnicze, które pomaga przechwycić
Ponieważ nie wszystkie pizze mają dodatki,
null
może być prawidłową wartością właściwościPizza.Toppings
. W takim przypadku warto wyrazić ją jako dopuszczaną do wartości null.Zmodyfikuj definicję właściwości w Pizza.cs , aby umożliwić używanie
Toppings
wartości 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(); }
Właściwość
Toppings
jest teraz wyrażana jako dopuszczana do wartości null.Dodaj wyróżniony wiersz do domeny 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! */
W poprzednim kodzie operator łączenia wartości null jest używany do przypisania
Toppings
elementu ,new List<PizzaTopping>();
jeśli jest tonull
.
Uruchamianie ukończonego rozwiązania
Zapisz wszystkie zmiany, a następnie skompiluj rozwiązanie.
dotnet build
Kompilacja kończy się bez ostrzeżeń ani błędów.
Uruchom aplikację.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Aplikacja jest uruchamiana do ukończenia (bez błędu) i wyświetla następujące dane wyjściowe:
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!
Podsumowanie
W tej lekcji użyto kontekstu dopuszczanego do wartości null, aby zidentyfikować i zapobiec możliwym NullReferenceException
wystąpieniom w kodzie.