演習 - Null Safety 戦略を適用する
前のユニットでは、コードで Null 許容の意図を表現する方法を学習しました。 次のユニットでは、これまでに学習した内容を既存の C# プロジェクトに適用します。
Note
このモジュールでは、ローカル開発に .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-safety
Visual 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 行目では、
pizza.Cheeses
プロパティに対してAdd
メソッドが呼び出されています。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 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... 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 build
の前にdotnet clean
コマンドを使用することにより、プロジェクト内のすべてのアセンブリを強制的にリビルドできます。.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(); }
上のコードでは以下の操作が行われます。
_cheeses
という名前のget
およびset
プロパティ アクセサーをインターセプトするために、新しいバッキング フィールドが追加されています。 これは Null 許容 (?
) として宣言され、初期化されないままになります。get
アクセサーは、null 合体演算子 (??
) を使用する式にマップされています。 この式では、null
ではないと仮定して、_cheeses
フィールドが返されます。null
の場合は、_cheeses
を返す前に、_cheeses
がnew List<PizzaCheese>()
に追加されます。set
アクセサーも式にマップされ、null 合体演算子を使用します。 コンシューマーがnull
値を割り当てると、ArgumentNullException がスローされます。
すべてのピザにトッピングがあるわけではないので、
Pizza.Toppings
プロパティに対してnull
は有効な値である可能性があります。 この場合、これを null 許容として表現することは理にかなっています。Pizza.cs のプロパティ定義を変更して
Toppings
を 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(); }
これで、
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! */
上記のコードでは、
null
の場合にToppings
をnew List<PizzaTopping>();
に割り当てるために 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
を特定して防止しました。