意図の表現
前のユニットでは、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);
前の例では、Find
が null
を返す可能性があるため、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");
に対して警告を出します。fooList
がnull
ではないと仮定した場合、Find
はnull
を返す "可能性があります" が、そうならないことが分かっているため、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 合体演算子を使用して条件付きで割り当てられます。- これにより、
salesTax
がnull
として渡された場合、値が設定されます。
- これにより、
ヒント
これは、機能的には次の 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 値の許容の意図を表現する方法を学習しました。 次のユニットでは、学習した内容を既存のプロジェクトに適用します。