null 許容参照型

C# 8.0 より前は、すべての参照型が Null 許容でした。 "Null 許容参照型" とは、C# 8.0 で導入された機能のグループを指します。これを使用すると、コードでランタイムにより System.NullReferenceException がスローされる可能性を最小限に抑えることができます。 "Null 許容参照型" には、これらの例外を回避するために役立つ 3 つの機能が含まれており、これには参照型を明示的に "Null 許容" とマークする機能が含まれます。

  • 変数を逆参照する前に null を使用できるかどうかを判断する、改善された静的フロー分析。
  • フロー分析が "null 状態" を決定するために API に注釈を付ける属性。
  • 開発者が変数に意図された "null 状態" を明示的に宣言するために使用する変数の注釈。

既存のプロジェクトでは、Null 状態の分析と変数の注釈が既定で無効になっています。これは、すべての参照型が引き続き Null 許容になることを意味します。 .NET 6 以降、"新しい" プロジェクトでは既定で有効になっています。 "Null 許容" の注釈コンテキストを宣言してこれらの機能を有効にする方法については、「Null 許容コンテキスト」を参照してください。

この記事の残りの部分では、コードが null 値を 逆参照している 可能性がある場合に、これらの 3 つの機能領域がどのようにして警告を生成するかについて説明します。 変数を逆参照すると、次の例に示すように、. (ドット) 演算子を使用してメンバーの 1 つにアクセスできることになります。

string message = "Hello, World!";
int length = message.Length; // dereferencing "message"

値が null である変数を逆参照すると、ランタイムは System.NullReferenceException をスローします。

null 状態分析

"*null 状態分析" は、参照の "null 状態" を追跡します。 このスタティック分析は、コードが null を逆参照する可能性がある場合に警告を生成します。 これらの警告に対処して、ランタイムが System.NullReferenceException をスローした場合のインシデントを最小限に抑えることができます。 コンパイラでは、スタティック分析を使用して、変数の "null 状態" が決定されます。 変数は "null 以外" または "null の可能性あり" のどちらかになります。 コンパイラは、2 つの方法で変数が "null 以外" であると判断します。

  1. 変数が "null 以外" であることがわかっている値に割り当てられている。
  2. 変数が null かどうかがチェックされ、そのチェック以降に変更されていない。

コンパイラが "null 以外" として判断していない変数はすべて "null の可能性あり" と見なされます。 この分析では、誤って null 値を逆参照する可能性がある状況で警告が表示されます。 コンパイラは、"null 状態" に基づいて警告を生成します。

  • 変数が "null 以外" である場合、その変数は安全に逆参照される可能性があります。
  • 変数が "null の可能性あり" である場合、その変数を逆参照する前に null でないことを確認する必要があります。

次の例を確認してください。

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

前の例では、コンパイラは、最初のメッセージが出力される際に message が "null の可能性あり" と判断します。 2 番目のメッセージに対する警告はありません。 コードの最後の行では、originalMessage が null である可能性があるため、警告が生成されます。 次の例は、ノードのツリーをルートにトラバースし、そのトラバーサル中に各ノードを処理する、より実用的な使用を示しています。

void FindRoot(Node node, Action<Node> processNode)
{
    for (var current = node; current != null; current = current.Parent)
    {
        processNode(current);
    }
}

前のコードでは、変数 current の逆参照に関する警告は生成されません。 スタティック分析では、"null の可能性あり" である場合に current が絶対に逆参照されないことが決定されます。 変数 current は、current.Parent にアクセスする前、および currentProcessNode アクションに渡される前に null かどうかがチェックされます。 前の例は、初期化、割り当て、または null との比較の際に、コンパイラがローカル変数の "null 状態" を判断する方法を示しています。

注意

C# 10 では、割り当てと null 状態分析を明確にするための多くの機能強化が追加されました。 C# 10 にアップグレードすると、誤検知である null 許容警告が少なくなります。 改善の詳細については、機能仕様の明確な割り当て改善に関するページを参照してください。

API シグネチャの属性

null 状態分析には、API のセマンティクスを理解するための開発者からのヒントが必要です。 一部の API では null チェックが提供され、変数の "null 状態" が "null の可能性あり" から "null 以外" に変更されます。 他の API は、入力引数の "null 状態" に応じて、"null 以外" または "null の可能性あり" の式を返します。 たとえば、メッセージを表示する次のようなコードについて考えます。

public void PrintMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

検査に基づくと、どの開発者もこのコードを安全と見なし、警告は生成されません。 コンパイラは、IsNullOrWhiteSpace が null チェックを提供することを知りません。 IsNullOrWhiteSpacefalse を返す場合にのみ、コンパイラに message が "null 以外" であることを通知するための属性を適用します。 前の例では、シグネチャに null 状態の message を示す NotNullWhen が含まれています。

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

属性は、メンバーの呼び出しに使用されるオブジェクト インスタンスの null 状態の引数、戻り値、およびメンバーに関する詳細情報を提供します。 各属性の詳細については、null 許容参照属性に関する言語リファレンス記事を参照してください。 .NET 5 では、すべての .NET ランタイム API に注釈が付けられています。 スタティック分析を改善するには、API に注釈を付け、引数と戻り値の "null 状態" に関するセマンティック情報を提供します。

Null 許容変数の注釈

"null 状態" 分析は、ほとんどの変数に対して堅牢な分析を提供します。 コンパイラには、メンバー変数に関するより多くの情報が必要です。 コンパイラは、パブリック メンバーがアクセスされる順序を想定することができません。 任意のパブリック メンバーに任意の順序でアクセスできます。 オブジェクトを初期化するには、アクセス可能な任意のコンストラクターを使用できます。 メンバー フィールドが null に設定されている可能性がある場合、コンパイラは、各メソッドの開始時点でその "null 状態" が必ず "null の可能性あり" になると想定します。

変数が null 許容参照型null 非許容参照型 かを宣言できる注釈を使用します。 これらの注釈は、変数の "null 状態" に関する重要なステートメントを作成します。

  • 参照が null になることはない。 null 非許容参照変数の既定の状態は、"null 以外" です。 コンパイラでは最初に変数が null ではないことをチェックしないで逆参照しても安全であることを保証する規則が適用されます。
    • 変数は、null 以外の値に初期化される必要があります。
    • 変数に値 null を割り当てることはできません。 コンパイラは、コードが null にすべきではない変数に "null の可能性あり" の式を割り当てるときに、警告を発行します。
  • 参照は null になることがある。 null 許容参照変数の既定の状態は、"null の可能性あり" です。 コンパイラは、null 参照が正しくチェックされていることを確認する規則を適用します。
    • 値が null ではないことをコンパイラが保証できる場合にのみ、変数を逆参照できます。
    • これらの変数は、既定値 null で初期化される場合があり、他のコードで値 null を割り当てられる可能性があります。
    • コードが null である可能性がある変数に "null の可能性あり" の式を割り当てると、コンパイラは警告を発行しません。

null が想定されていない参照変数は、すべて "null 以外" の "null 状態" になります。 最初は null である可能性がある参照変数は、すべて "null の可能性あり" の "null 状態" になります。

null 許容参照型 は、null 許容値型と同じ構文を使用して記述し、変数の型の後に ? を追加します。 たとえば、次の変数宣言は、null 許容型の文字列変数 name を表します。

string? name;

型の名前に ? が追加されていないすべての変数は、null 非許容参照型 です。 この機能を有効にすると、既存のコードのすべての参照型変数がそれに含まれます。 ただし、暗黙的に型指定されたローカル変数 (var を使用して宣言) は、null 許容参照型 です。 前のセクションで示した通り、スタティック分析によってローカル変数の "null 状態" が決定され、それが "null の可能性あり" かどうかが決定されます。

場合によっては、変数が null ではないとわかっているときに警告をオーバーライドする必要がありますが、コンパイラはその "null 状態" が "null の可能性あり" と判断します。 変数名の後に null 免除演算子 ! を使用して "null 状態" を強制的に "null 以外" にします。 たとえば、変数 namenull ではないことがわかっているのに、コンパイラで警告が出る場合は、次のようにコードを記述することで、コンパイラの分析をオーバーライドできます。

name!.Length;

Null 許容参照型と Null 許容値型は、同様のセマンティック概念を提供します。変数は値またはオブジェクトを表したり、その変数を null にしたりできます。 ただし、Null 許容参照型と Null 許容値型は異なる方法で実装されます。Null 許容値型は System.Nullable<T> を使用して実装され、Null 許容参照型はコンパイラによって読み取られる属性によって実装されます。 たとえば、string?string は両方とも、同じ型 System.String で表されます。 ただし、int?int はそれぞれ System.Nullable<System.Int32>System.Int32 で表されます。

ジェネリック

ジェネリックには、任意の型パラメーター TT? を処理するための詳細な規則が必要です。 これまでの経緯や、null 許容値型と null 許容参照型では実装が異なるため、規則は必然的に詳細になります。 null 許容値型は、System.Nullable<T> 構造体を使用して実装されます。 null 許容参照型は、コンパイラにセマンティック ルールを提供する型の注釈として実装されます。

C# 8.0 では、Tstruct または class に制約せずに T? を使用するとコンパイルされませんでした。 これにより、T? は、コンパイラによって明確に解釈されました。 C# 9.0 では、この制限は、制約のない型パラメーター T に対して次の規則を定義することで削除されました。

  • T の型引数が参照型である場合、T? は、対応する null 許容参照型を参照します。 たとえば、Tstring の場合、T?string? です。
  • T の型引数が値型である場合、T? は同じ値型 T を参照します。 たとえば、Tint の場合、T?int です。
  • T の型引数が null 許容参照型である場合、T? はその同じ null 許容参照型を参照します。 たとえば、Tstring? の場合、T?string? です。
  • T の型引数が null 許容値型である場合、T? はその同じ null 許容値型を参照します。 たとえば、Tint? の場合、T?int? です。

戻り値の場合、T?[MaybeNull]T に相当します。引数値の場合、T?[AllowNull]T に相当します。 詳細については、言語リファレンスの null 状態の分析のための属性に関する記事をご覧ください。

制約を使用して、さまざまな動作を指定できます。

  • class 制約は、T が null 非許容参照型 (例: string) である必要があることを意味します。 Tstring? のような null 許容型参照を使用すると、コンパイラによって警告が生成されます。
  • class? 制約は、T が null 非許容 (string) または null 許容参照型 (例: string?) のどちらかの参照型である必要があることを意味します。 型パラメーターが null 許容参照型 (string? など) である場合、T? の式は、その同じ null 許容参照型 (string? など) を参照します。
  • notnull 制約は、T が null 非許容参照型または null 非許容値型である必要があることを意味します。 型パラメーターに null 許容参照型または null 許容値型を使用すると、コンパイラによって警告が生成されます。 さらに、T が値型の場合、戻り値はその値型であり、対応する null 許容値型ではありません。

これらの制約は、T の使用方法に関する情報をコンパイラに提供するのに役立ちます。 これは、開発者が T の型を選択するときに役立ち、ジェネリック型のインスタンスが使用されている場合に、より優れた "null 状態" 分析を提供します。

null 許容コンテキスト

System.NullReferenceException をスローから保護する新機能は、既存のコードベースで有効にすると、中断を伴う可能性があります。

  • 明示的に型指定された参照変数はすべて、null 非許容参照型として解釈されます。
  • ジェネリック内の class 制約の意味は、null 非許容参照型を意味するように変更されました。
  • これらの新しい規則により、新しい警告が生成されます。

これらの機能を既存のプロジェクトで使用するには、明示的にオプトインする必要があります。 これにより移行パスが提供され、下位互換性が維持されます。 null 許容コンテキストでは、コンパイラによる参照型変数の解釈方法を細かく制御できます。 null 許容注釈コンテキスト は、コンパイラの動作を決定します。 null 許容注釈コンテキスト には、次の 4 つの値があります。

  • disabled: コンパイラは、C# 7.3 以前と同様に動作します。
    • Null 許容の警告は無効になっています。
    • 参照型の変数はすべて、null 許容参照型です。
    • 型に ? サフィックスを使用して、変数を null 許容参照型として宣言することはできません。
    • null 免除演算子 ! は使用できますが、効果がありません。
  • "有効": コンパイラで、すべての null 参照分析とすべての言語機能が有効になります。
    • 新しい Null 許容のすべての警告が有効になります。
    • ? サフィックスを使用して、null 許容参照型を宣言できます。
    • 他の参照型の変数はすべて、null 非許容参照型です。
    • null 免除演算子は、null に割り当てられる可能性があるという警告を抑制します。
  • "警告": コンパイラは、すべての null 分析を実行し、コードが null を逆参照する可能性がある場合に警告を出力します。
    • 新しい Null 許容のすべての警告が有効になります。
    • null 許容参照型を宣言する ? サフィックスを使用すると警告が生成されます。
    • 参照型の変数はすべて null にすることができます。 ただし、? サフィックスを使用して宣言しない限り、メンバーは、すべてのメソッドが左中かっこで囲まれた "null 以外" の "null 状態" になります。
    • null 免除演算子 ! を使用できます。
  • annotations: コンパイラで null 分析が実行されないか、コードが null を逆参照する可能性がある場合に警告を出力します。
    • Null 許容の新しい警告はすべて無効になっています。
    • ? サフィックスを使用して、null 許容参照型を宣言できます。
    • 他の参照型の変数はすべて、null 非許容参照型です。
    • null 免除演算子 ! は使用できますが、効果がありません。

プロジェクトの null 許容注釈コンテキストと null 許容警告コンテキストは、 .csproj ファイルの <Nullable> 要素を使用して設定することができます。 この要素は、コンパイラによって型の null 値の許容が解釈される方法と、発せられる警告を構成します。 次の表に、許容される値と、指定されるコンテキストの概要を示します。

Context 逆参照の警告 割り当ての警告 参照型 ? サフィックス ! 演算子
disabled 無効 無効 すべて Null 許容です 使用できません 影響はありません
enabled Enabled Enabled ? で宣言されている場合を除き、null 非許容です。 null 許容型を宣言します null 割り当ての可能性に対する警告を抑制します
warnings Enabled 適用できません すべて Null 許容ですが、メソッドの始め波かっこでは、メンバーは not null と見なされます 警告を生成します null 割り当ての可能性に対する警告を抑制します
annotations 無効 無効 ? で宣言されている場合を除き、null 非許容です。 null 許容型を宣言します 影響はありません

C# 8 より前にコンパイルされたコードまたは "無効な" コンテキスト内の参照型変数は、"null 許容未指定" です。 null リテラルまたは " null の可能性あり" の変数を、"null 許容未指定" である変数に割り当てることができます。 ただし、"null 許容未指定" 変数の既定の状態は、"null 以外" です。

プロジェクトに最適な設定を選択できます。

  • 診断や新機能に基づいて更新したくないレガシ プロジェクトには、"無効" を選択します。
  • コードが System.NullReferenceException をスローする可能性がある場所を判断するには、"警告" を選択します。 コードが変更され、null 非許容参照型が有効になる前に、これらの警告に対処することができます。
  • 警告が有効になる前にデザインの意図を表現するには、"注釈" を選択します。
  • null 参照の例外から保護する新しいプロジェクトとアクティブなプロジェクトには "有効" を選択します。

:

<Nullable>enable</Nullable>

ディレクティブを使用して、ソース コード内の任意の場所にこれらと同じコンテキストを設定することもできます。 これらは、大規模なコードベースを移行する場合に最も役立ちます。

  • #nullable enable:null 許容注釈コンテキストと null 許容警告コンテキストを、有効 に設定します。
  • #nullable disable:null 許容注釈コンテキストと null 許容警告コンテキストを、無効 に設定します。
  • #nullable restore:null 許容注釈コンテキストと null 許容警告コンテキストを、プロジェクトの設定に戻します。
  • #nullable disable warnings:null 許容警告コンテキストを 無効 に設定します。
  • #nullable enable warnings:null 許容警告コンテキストを 有効 に設定します。
  • #nullable restore warnings:null 許容警告コンテキストをプロジェクトの設定に戻します。
  • #nullable disable annotations:Null 許容の注釈コンテキストを 無効 に設定します。
  • #nullable enable annotations:Null 許容の注釈コンテキストを 有効 に設定します。
  • #nullable restore annotations:注釈の警告コンテキストをプロジェクト設定に復元します。

任意のコード行に対して、次のいずれかの組み合わせを設定できます。

警告コンテキスト 注釈コンテキスト vmmblue_2
プロジェクトの既定 プロジェクトの既定 既定
enabled disabled 分析の警告の修正
enabled プロジェクトの既定 分析の警告の修正
プロジェクトの既定 enabled 型の注釈の追加
enabled enabled 既に移行されているコード
disabled enabled 警告を修正する前にコードに注釈を付ける
disabled disabled 移行されたプロジェクトへのレガシ コードの追加
プロジェクトの既定 disabled ほとんどない
disabled プロジェクトの既定 ほとんどない

これら 9 つの組み合わせにより、コンパイラがコードに生成する診断をきめ細かく制御できます。 更新中の領域ではさらに多くの機能を有効にすることができます。まだ対処できない追加の警告は表示されなくなります。

重要

グローバルな Null 許容コンテキストは、生成されたコード ファイルに適用されません。 いずれの方法でも、Null 許容コンテキストは、生成済みとしてマークされているすべてのソース ファイルに対して "無効になります"。 これは、生成されたファイル内のどの API にも注釈が付けられないことを意味します。 ファイルが生成済みとしてマークされる方法は 4 つあります。

  1. .editorconfig で、そのファイルに適用されるセクションで generated_code = true を指定します。
  2. ファイルの先頭にあるコメントに <auto-generated> または <auto-generated/> を配置します。 これは、コメント内の任意の行に配置できますが、コメント ブロックはファイル内の最初の要素である必要があります。
  3. ファイル名を TemporaryGeneratedFile_ で開始します
  4. ファイル名の末尾を .designer.cs.generated.cs.g.cs、または .g.i.cs にします。

ジェネレーターは、#nullable プリプロセッサ ディレクティブを使用してオプトインできます。

既定の null 許容注釈および警告コンテキストは 無効 です。 これは、既存のコードを変更せずにコンパイルできて新しい警告は生成されないことを意味します。 .NET 6 以降では、新しいプロジェクトには、すべてのプロジェクト テンプレートに <Nullable>enable</Nullable> 要素が含まれています。

これらのオプションでは、null 許容参照型を使用するように既存のコードベースを更新するための 2 つの方法が提供されます。

既知の落とし穴

null 許容参照と null の安全性を決定するスタティック分析において、既知の落とし穴となっているのが、参照型を含む配列と構造体です。 どちらの場合も、警告を生成せずに、null 非許容参照を null に初期化できます。

構造体

null 非許容の参照型を含む構造体により、警告なしで default を割り当てることができます。 次の例を確認してください。

using System;

#nullable enable

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static class Program
{
    public static void PrintStudent(Student student)
    {
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
    }

    public static void Main() => PrintStudent(default);
}

前の例では PrintStudent(default) に警告はありませんが、null 非許容参照型 FirstNameLastName は null です。

もう 1 つの一般的なケースは、ジェネリック構造体を扱う場合です。 次の例を確認してください。

#nullable enable

public struct Foo<T>
{
    public T Bar { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string s = default(Foo<string>).Bar;
    }
}

前の例では、プロパティ Bar は実行時に null になり、null 非許容の文字列には警告なしで割り当てられます。

配列

配列も null 許容参照型の既知の落とし穴です。 警告が生成されない次の例を考えてみます。

using System;

#nullable enable

public static class Program
{
    public static void Main()
    {
        string[] values = new string[10];
        string s = values[0];
        Console.WriteLine(s.ToUpper());
    }
}

前の例では、null 非許容の文字列が保持され、その要素がすべて null に初期化されることが配列の宣言からわかります。 その後、変数 snull 値が割り当てられます (配列の最初の要素)。 最後に、変数 s が逆参照され、ランタイム例外が発生します。

関連項目