演習 - Null Safety 戦略を適用する
前のユニットでは、コードで Null 許容の意図を表現する方法を学習しました。 次のユニットでは、これまでに学習した内容を既存の C# プロジェクトに適用します。
注
このモジュールでは、ローカル開発に .NET CLI (コマンド ライン インターフェイス) と Visual Studio Code を使用します。 このモジュールを終了すると、Visual Studio (Windows)、Visual Studio for Mac (macOS)、または Visual Studio Code (Windows、Linux、macOS) を使った継続的開発を使用して、その概念を適用できます。
このモジュールでは、.NET 6.0 SDK を使用します。 適切なターミナルで次のコマンドを実行して、.NET 6.0 がインストールされていることを確実にします。
dotnet --list-sdks
次のような出力が表示されます。
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
6 で始まるバージョンが一覧に表示されていることを確実にします。 何も表示されていない場合、またはコマンドが見つからない場合は、 最新の .NET 6.0 SDK をインストールします。
サンプル コードを取得して調べる
コマンド ターミナルで、サンプル GitHub リポジトリをクローンし、クローンしたディレクトリに切り替えます。
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safetyVisual Studio Code でプロジェクト ディレクトリを開きます。
code .dotnet runコマンドを使用してサンプル プロジェクトを実行します。dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csprojこれにより、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スタック トレースは、. \src\ContosoPizza.Service\Program.cs の 13 行目で例外が発生したことを示します。 13 行目では、
Addプロパティに対してpizza.Cheesesメソッドが呼び出されています。pizza.Cheesesはnullのため、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! */
Null 許容コンテキストを有効にする
ここでは、Null 許容コンテキストを有効にし、ビルドに対する影響を調べます。
src/ContosoPizza.Service/ContosoPizza.Service.csproj で、強調表示された行を追加し、変更を保存します。
<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>上記の変更により、
ContosoPizza.Serviceプロジェクト全体に対して Null 許容コンテキストが有効になります。src/ContosoPizza.Models/ContosoPizza.Models.csproj で、強調表示された行を追加し、変更を保存します。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>上記の変更により、
ContosoPizza.Modelsプロジェクト全体に対して Null 許容コンテキストが有効になります。dotnet buildコマンドを使用してサンプル ソリューションをビルドします。dotnet build2 つの警告が出されて、ビルドは成功します。
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コマンドを使用して、
dotnet buildビルドします。dotnet build今度はエラーや警告なしで、ビルドは成功します。 前のビルドは正常に完了しましたが、警告がありました。 ソースは変更されていなかったため、ビルド プロセスでコンパイラは再度実行されません。 ビルドでコンパイラが実行されないため、警告はありません。
ヒント
dotnet cleanの前にdotnet buildコマンドを使用することにより、プロジェクト内のすべてのアセンブリを強制的にリビルドできます。.csproj ファイルで、強調表示された行を追加し、変更を保存します。
<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>前の変更では、警告が発生したら必ずビルドを失敗させるようにコンパイラに指示しています。
ヒント
<TreatWarningsAsErrors>の使用は省略可能です。 ただし、これによって警告を見落さなくなるので、使用することをお勧めします。dotnet buildコマンドを使用してサンプル ソリューションをビルドします。dotnet buildビルドは失敗し、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警告をエラーとして扱う場合、アプリはビルドされなくなります。 エラーの数は少なくすぐに対処できるので、これはこの状況で実際には望ましい状態です。 この 2 つのエラー (CS8618) により、まだ初期化されていない、null 非許容として宣言されたプロパティがあることを認識できます。
エラーを修正する
Null 値の許容に関連する警告やエラーを解決する方法は多数あります。 次に例をいくつか示します。
- コンストラクターのパラメーターとして、null 非許容のチーズとトッピングのコレクションが必要です
- プロパティ
get/setをインターセプトし、nullチェックを追加します - プロパティを null 許容にする意図を表現します
- プロパティ初期化子を使用して、既定 (空) 値でコレクションをインラインで初期化します
- コンストラクターで、プロパティに既定 (空) 値を割り当てます
Pizza.Cheesesプロパティのエラーを修正するには、Pizza.csのプロパティ定義を変更して、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(); }上のコードでは以下の操作が行われます。
-
getという名前のsetおよび_cheesesプロパティ アクセサーをインターセプトするために、新しいバッキング フィールドが追加されています。 これは Null 許容 (?) として宣言され、初期化されないままになります。 -
getアクセサーは、null 合体演算子 (??) を使用する式にマップされています。 この式では、_cheesesではないと仮定して、nullフィールドが返されます。nullの場合は、_cheesesを返す前に、new List<PizzaCheese>()が_cheesesに追加されます。 -
setアクセサーも式にマップされ、null 合体演算子を使用します。 コンシューマーがnull値を割り当てると、ArgumentNullException がスローされます。
-
すべてのピザにトッピングがあるわけではないので、
nullプロパティに対してPizza.Toppingsは有効な値である可能性があります。 この場合、これを null 許容として表現することは理にかなっています。を null 許容にできるように、
Toppingsのプロパティ定義を変更します。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(); }これで、
Toppingsプロパティは Null 許容として表現されました。強調表示された行を 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! */
上記のコードでは、
Toppingsの場合にnew List<PizzaTopping>();をnullに割り当てるために null 合体演算子が使用されています。
完成したソリューションを実行する
すべての変更を保存して、ソリューションをビルドします。
dotnet buildこのビルドは警告やエラーなしで完了します。
アプリを実行します。
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csprojアプリは (エラーなしで) 完了まで実行され、次の出力が表示されます。
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!
まとめ
このユニットでは、null 許容コンテキストを使用して、コード内で発生する可能性がある NullReferenceException を特定して防止しました。