意図の表現

完了

前のユニットでは、NullReferenceException を防ぐために C# コンパイラで静的分析が実行される方法について学習しました。 また、Null 許容コンテキストを有効にする方法についても学習しました。 このユニットでは、Null 許容コンテキスト内で意図を明示的に表現する方法について詳しく学習します。

変数の宣言

Null 許容コンテキストを有効にすると、コンパイラがコードをどのように見ているかをよりよく理解できます。 Null 許容が有効になったコンテキストから生成された警告に対してアクションを実行できます。そして、そうすることで、意図を明示的に定義します。 たとえば、引き続き FooBar コードを分析し、宣言と割り当てを詳しく調べてみましょう。

// Define as nullable
FooBar? fooBar = null;

FooBar? が追加されていることに注目してください。 これによって、fooBar を Null 許容にすることを明示的に意図していることをコンパイラに示しています。 fooBar を Null 許容にすることは意図していないが、警告は回避したいという場合は、次を検討してください。

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

この例では、null 免除 (!) 演算子を null に追加しています。これは、この変数を明示的に null として初期化することをコンパイラに指示します。 コンパイラでは、この参照が null であることに関する警告を発行しません。

可能であれば、null 非許容変数を宣言するときに null 以外の値を割り当てることをお勧めします。

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

演算子

前のユニットで説明したように、C# では、null 許容参照型に関連した意図を表現するための演算子がいくつか定義されています。

null 免除 (!) 演算子

前のセクションで、null 免除演算子 (!) を紹介しました。 これは、CS8600 警告を無視するようにコンパイラに指示します。 これは、コンパイラに対して、自分が何をしているか分かっている (意図的に行っている) ということを知らせる 1 つの方法ですが、これには、"実際に自分が何をしているか分かっている" 必要があるという但し書きが付きます。

Null 値許容コンテキストが有効になっているときに null 非許容型を初期化する場合は、コンパイラに明示的に免除を要求する必要がある場合があります。 次に例を示します。

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

// The FooBar type definition for example.
record FooBar(int Id, string Name);

前の例では、Findnull を返す可能性があるため、FooBar fooBar = fooList.Find(f => f.Name == "Bar"); で CS8600 警告が生成されます。 この null は、このコンテキストでは null 非許容である fooBar に割り当てられます。 しかし、この考案された例では、Find が決して null を返さないことが分かっています。 null 免除演算子を使用することで、この意図をコンパイラに示すことができます。

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

fooList.Find(f => f.Name == "Bar") の末尾にある ! に注目してください。 これは、Find メソッドによって返されるオブジェクトが null の可能性があることが分かっていることをコンパイラに示します。

Null 免除演算子は、メソッド呼び出しやプロパティ評価の前にインラインでオブジェクトに適用することもできます。 考案された別の例を考えてみましょう。

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

前の例の場合:

  • GetFooList は、null 許容型 List<FooBar>? を返す静的メソッドです。
  • fooList には、GetFooList によって返される値が割り当てられます。
  • fooList に割り当てられた値が null の可能性があるため、コンパイラは fooList.Find(f => f.Name == "Bar"); に対して警告を出します。
  • fooListnull ではないと仮定した場合、Findnull を返す "可能性があります" が、そうならないことが分かっているため、null 免除演算子が適用されます。

次のように、null 免除演算子を fooList 適用して、警告を無効にできます。

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

Note

null 免除演算子は慎重に使用する必要があります。 単に警告を表示しないためにこれを使用することは、発生する可能性がある null 値の間違いを検出するのを助けてくれなくていいからとコンパイラに通知することを意味します。 どうしても必要な場合にのみ、慎重に使用してください。

詳細については、「! (null 免除) 演算子 (C# リファレンス)」を参照してください。

null 合体 (??) 演算子

Null 許容型を操作しているときに、それらが現在 null かどうかを評価し、特定のアクションを実行する必要がある場合があります。 たとえば、null 許容型に null が割り当てられている場合や、初期化されていない場合は、null 以外の値を割り当てる必要がある場合があります。 ここで、null 合体演算子 (??) が役に立ちます。

次に例を示します。

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

前述の C# コードでは:

  • salesTax パラメーターは、Null 許容の IStateSalesTax として定義されています。
  • メソッド本体内で、salesTax は null 合体演算子を使用して条件付きで割り当てられます。
    • これにより、salesTaxnull として渡された場合、値が設定されます。

ヒント

これは、機能的には次の C# コードと同等です。

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

次の例では、null 合体演算子が役立つ、もう 1 つの一般的な C# 表現形式を示しています。

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

前述の C# コードでは、次のことが行われます。

  • ジェネリック型パラメーターが new() に制約されるジェネリック ラッパー クラスを定義しています。
  • コンストラクターは、既定で null に設定されている T source パラメーターを受け取ります。
  • ラップされた _source は、条件付きで new T() に初期化されます。

詳細については、「?? および ??= 演算子 (C# リファレンス)」を参照してください。

null 条件 (?.) 演算子

null 許容型を操作しているときに、null オブジェクトの状態に基づいて条件付きでアクションを実行する必要がある場合があります。 たとえば、前のユニットでは、null の逆参照による NullReferenceException を示すために FooBar レコードが使用されました。 これは、ToString が呼び出されたときに発生しました。 この同じ例で考えてみましょう。ただし、今回は null 条件演算子を適用します。

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

前述の C# コードでは、次のことが行われます。

  • fooBar を条件付きで逆参照し、ToString の結果を str 変数に割り当てます。
    • str 変数の型は string? (Null 許容文字列) です。
  • str の値 (なし) が標準出力に書き込まれます。
  • Console.Write(null) の呼び出しは有効なので、警告はありません。
  • Console.Write(str.Length) を呼び出そうとすると、null を逆参照する可能性があるため、警告が出されます。

ヒント

これは、機能的には次の C# コードと同等です。

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

演算子を組み合わせることにより、意図をさらに表現できます。 たとえば、?.?? 演算子を連結できます。

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

詳細については、「?. および ?[] (null 条件) 演算子」を参照してください。

まとめ

このユニットでは、コードで NULL 値の許容の意図を表現する方法を学習しました。 次のユニットでは、学習した内容を既存のプロジェクトに適用します。

自分の知識をチェックする

1.

string 参照型の default 値は何ですか?

2.

null の逆参照で予想される動作はどのようなものですか?

3.

この throw null; C# コードが実行されると何が起こりますか?

4.

Null 許容参照型に関して最も正確なステートメントはどれですか?