演習 - 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 をインストールしてください

サンプル コードを取得して調べる

  1. コマンド ターミナルで、サンプル GitHub リポジトリをクローンし、クローンしたディレクトリに切り替えます。

    git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety
    cd mslearn-csharp-null-safety
    
  2. Visual Studio Code でプロジェクト ディレクトリを開きます。

    code .
    
  3. 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.Cheesesnull のため、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 許容コンテキストを有効にし、ビルドに対する影響を調べます。

  1. 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 許容コンテキストが有効になります。

  2. 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 許容コンテキストが有効になります。

  3. 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
    
  4. dotnet build コマンドを使用してサンプル ソリューションをもう一度ビルドします。

    dotnet build
    

    今度はエラーや警告なしで、ビルドは成功します。 前のビルドは正常に完了しましたが、警告がありました。 ソースは変更されていなかったため、ビルド プロセスでコンパイラは再度実行されません。 ビルドでコンパイラが実行されないため、警告はありません。

    ヒント

    dotnet build の前に dotnet clean コマンドを使用することにより、プロジェクト内のすべてのアセンブリを強制的にリビルドできます。

  5. .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> の使用は省略可能です。 ただし、これによって警告を見落さなくなるので、使用することをお勧めします。

  6. 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 許容にする意図を表現します
  • プロパティ初期化子を使用して、既定 (空) 値でコレクションをインラインで初期化します
  • コンストラクターで、プロパティに既定 (空) 値を割り当てます
  1. 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 を返す前に、_cheesesnew List<PizzaCheese>() に追加されます。
    • set アクセサーも式にマップされ、null 合体演算子を使用します。 コンシューマーが null 値を割り当てると、ArgumentNullException がスローされます。
  2. すべてのピザにトッピングがあるわけではないので、Pizza.Toppings プロパティに対して null は有効な値である可能性があります。 この場合、これを null 許容として表現することは理にかなっています。

    1. 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 許容として表現されました。

    2. 強調表示されている行を 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 の場合に Toppingsnew List<PizzaTopping>(); に割り当てるために null 合体演算子が使用されています。

完成したソリューションを実行する

  1. すべての変更を保存して、ソリューションをビルドします。

    dotnet build
    

    このビルドは警告やエラーなしで完了します。

  2. アプリを実行します。

    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 を特定して防止しました。