C# コンパイラによって解釈される null 状態のスタティック分析の属性

Null 許容の有効なコンテキストの場合、コンパイラによってコードの静的分析が実行され、すべての参照型変数の "null 状態" が判断されます。

  • "null 以外": 静的分析によって、変数の値が null 以外であることが判断されます。
  • "null の可能性あり": 静的分析では、変数に null 以外の値が割り当てられていることを判断できません。

これらの状態を使用すると、null 値を参照する可能性がある場合にコンパイラから警告が生成され、System.NullReferenceException がスローされます。 これらの属性により、引数と戻り値の状態に基づいて、引数、戻り値、オブジェクト メンバーの "null 状態" に関するセマンティック情報がコンパイラに提供されます。 API にこのセマンティック情報の注釈が適切に付けられていると、コンパイラからより正確な警告が生成されます。

この記事では、null 許容参照型の各属性とその使用方法について簡単に説明します。

最初に例を見てみましょう。 ライブラリに、リソース文字列を取得するための次の API があるとします。 このメソッドはもともと null 許容の未指定コンテキストでコンパイルされました。

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

前述の例では、.NET のよくある `Try*` パターンに従っています。 この API には、2 つの参照パラメーター (key および message) があります。 この API には、これらのパラメーターの "null 状態" に関連する次の規則があります。

  • 呼び出し元では、`key` の引数として `null` を渡すことはできません。
  • 呼び出し元では、`message` の引数として、値が `null` の変数を渡すことができます。
  • `TryGetMessage` メソッドから `true` が返された場合、`message` の値は null ではありません。 戻り値が false, の場合、message の値は null です。

key の規則は簡潔に表すことができます。key は null 非許容参照型である必要があります。 `message` パラメーターはより複雑です。 引数として null である変数が許容されますが、成功時には out の引数が null ではないことが保証されます。 これらのシナリオでは、予測を記述するために、より豊富なボキャブラリが必要です。 後述する NotNullWhen 属性は、message パラメーターに使用される引数の "null 状態" を示します。

注意

これらの属性を追加すると、API の規則に関する詳細情報がコンパイラに与えられます。 呼び出し元のコードが、null 許容が有効なコンテキストでコンパイルされると、呼び出し元がこれらの規則に違反したときに、コンパイラによって警告されます。 このような属性では、実装に対するその他のチェックが有効になりません。

属性 カテゴリ 説明
[AllowNull](xref:System.Diagnostics.CodeAnalysis.AllowNullAttribute) [Precondition](#preconditions-allownull-and-disallownull) null 非許容のパラメーター、フィールド、またはプロパティを null にすることができます。
[DisallowNull](xref:System.Diagnostics.CodeAnalysis.DisallowNullAttribute) [Precondition](#preconditions-allownull-and-disallownull) Null 許容のパラメーター、フィールド、またはプロパティは、null にすることができません。
[MaybeNull](xref:System.Diagnostics.CodeAnalysis.MaybeNullAttribute) [事後条件](#postconditions-maybenull-and-notnull) null 非許容のパラメーター、フィールド、プロパティ、または戻り値は、null にすることができます。
[NotNull](xref:System.Diagnostics.CodeAnalysis.NotNullAttribute) [事後条件](#postconditions-maybenull-and-notnull) Null 許容のパラメーター、フィールド、プロパティ、または戻り値は、null にすることができません。
[MaybeNullWhen](xref:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute) [条件付き事後条件](#conditional-post-conditions-notnullwhen-maybenullwhen-and-notnullifnotnull) メソッドから指定された `bool` 値が返されるとき、null 非許容の引数が null である可能性があります。
[NotNullWhen](xref:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute) [条件付き事後条件](#conditional-post-conditions-notnullwhen-maybenullwhen-and-notnullifnotnull) メソッドから指定された `bool` 値が返されるとき、null 許容の引数は null になりません。
[NotNullIfNotNull](xref:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute) [条件付き事後条件](#conditional-post-conditions-notnullwhen-maybenullwhen-and-notnullifnotnull) 指定されたパラメーターの引数が null でない場合、戻り値、プロパティ、または引数は null ではありません。
[MemberNotNull](xref:System.Diagnostics.CodeAnalysis.MemberNotNullAttribute) メソッドおよびプロパティ ヘルパー メソッド メソッドから戻ったときに、列挙されたメンバーは null になりません。
[MemberNotNullWhen](xref:System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute) メソッドおよびプロパティ ヘルパー メソッド 指定された `bool` 値がメソッドから返された場合、列挙されたメンバーは null になりません。
[DoesNotReturn](xref:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute) [到達できないコード](#stop-nullable-analysis-when-called-method-throws) メソッドまたはプロパティが返されることはありません。 つまり、常に例外がスローされます。
[DoesNotReturnIf](xref:System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute) [到達できないコード](#stop-nullable-analysis-when-called-method-throws) 関連付けられた bool パラメーターに指定された値がある場合、このメソッドまたはプロパティから制御が返されることはありません。

前述の説明は、各属性動作に関するクイック リファレンスです。 以下のセクションでは、これらの属性の動作と意味についてさらに詳しく説明します。

事前条件: `AllowNull``DisallowNull`

適切な既定値が設定されているため、`null` を返すことのない読み取りおよび書き込みプロパティについて考えてみます。 呼び出し元では、その既定値に設定するときに、set アクセサーに `null` を渡します。 たとえば、チャット ルームで画面名を要求するメッセージング システムがあるとします。 何も指定されていない場合、システムによってランダムな名前が生成されます。

public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

null 許容の認識されないコンテキストで上記のコードをコンパイルする場合、何も問題はありません。 null 許容参照型を有効にすると、`ScreenName` プロパティが null 非許容参照になります。 これは `get` アクセサーでは正しい動作です。`null` が返されることはありません。 呼び出し元では、返されたプロパティで `null` をチェックする必要はありません。 しかし、ここでプロパティを `null` に設定すると、警告が生成されます。 この種類のコードをサポートするには、次のコードに示すように、 属性をプロパティに追加します。

[AllowNull]
public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

これと、この記事で説明されている他の属性を使用するには、`using` ディレクティブを追加する必要がある場合があります。 属性は、`set` アクセサーではなく、プロパティに適用されます。 `AllowNull` 属性では*事前条件*を指定し、引数にのみ適用されます。 `get` アクセサーには戻り値がありますが、パラメーターはありません。 したがって、`AllowNull` 属性は `set` アクセサーにのみ適用されます。

前の例では、引数に `AllowNull` 属性を追加する場合の検索方法が示されています。

  1. その変数の一般的なコントラクトは、`null` にできないため、null 非許容参照型が必要であるというものです。
  2. 呼び出し元から null が引数として渡されるシナリオもありますが、よくある使用方法ではありません。

ほとんどの場合、プロパティ、または `in``out`、および `ref` 引数にこの属性が必要になります。 `AllowNull` 属性は、通常は変数が null 以外だが、`null` を事前条件として許可する必要がある場合に最適です。

`DisallowNull` を使用するシナリオと比較してください。この属性を使用して、null 許容参照型の引数を `null` にできないことを指定します。 `null` は既定値であるが、クライアントでは null 以外の値にしか設定できないというプロパティについて考えてみます。 次のコードがあるとします。

public string ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

上記のコードは、`ReviewComment``null` である可能性はあるが、`null` に設定できないという設計を表すのに最適な方法です。 このコードが null 許容認識になると、 を使用して、呼び出し元に対して、この概念をより明確に表すことができます。

[DisallowNull]
public string? ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

null 許容コンテキストでは、ReviewCommentget アクセサーから null の既定値が返される可能性があります。 コンパイラからは、アクセスの前にチェックする必要があることが警告されます。 さらに、`null` である可能性があっても、呼び出し元で明示的に `null` に設定できないことが、呼び出し元に警告されます。 `DisallowNull` 属性では、"*事前条件*" も指定され、これは `get` アクセサーには影響しません。 以下について、これらの特性を監視する場合は、`DisallowNull` 属性を使用します。

  1. 主なシナリオでは (多くの場合、最初にインスタンス化されるときに)、変数が `null` である可能性があります。
  2. 変数は、明示的に `null` に設定することはできません。

このような状況は、もともと "*null として認識されていなかった*" コードでよく見られます。 オブジェクト プロパティが、2 つの異なる初期化操作で設定されている可能性があります。 一部のプロパティは、一部の非同期処理が完了した後にのみ設定される可能性があります。

`AllowNull` および `DisallowNull` 属性を使用すると、変数の事前条件が、これらの変数の null 許容注釈と一致しない可能性があることを指定できます。 これにより、API の特性の詳細が提供されます。 この追加情報は、呼び出し元で API を正しく使用するのに役立ちます。 次の属性を使用して、事前条件を指定することを忘れないでください。

  • [AllowNull](xref:System.Diagnostics.CodeAnalysis.AllowNullAttribute): null 非許容の引数は null にすることができます。
  • [DisallowNull](xref:System.Diagnostics.CodeAnalysis.DisallowNullAttribute): null 許容引数は null にすることはできません。

事後条件: `MaybeNull` および `NotNull`

次のシグネチャを持つメソッドがあるとします。

public Customer FindCustomer(string lastName, string firstName)

検索された名前が見つからなかったときに `null` を返す、このようなメソッドを記述した可能性があります。 `null` は、レコードが見つからなかったことを明確に示しています。 この例では、戻り値の型を `Customer` から `Customer?` に変更する可能性があります。 戻り値を null 許容参照型として宣言すると、この API の意図が明確になります。

public Customer? FindCustomer(string lastName, string firstName)

ジェネリックの NULL 値の許容に関する記事で説明されている理由により、この手法では API と一致する静的分析が生成されない場合があります。 同様のパターンに従うジェネリック メソッドがある場合があります。

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

検索された項目が見つからない場合、このメソッドからは `null` が返されます。 メソッドの戻り値に MaybeNull の注釈を追加することで、項目から見つからない場合にメソッドから null を返すことを明確にすることができます。

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

上記のコードを使用すると、戻り値が実際には null になる "可能性がある" ことを呼び出し側に通知できます。 また、型が null 非許容であっても、メソッドから null 式が返される可能性があることもコンパイラに通知します。 型パラメーター T のインスタンスを返すジェネリック メソッドがある場合、NotNull 属性を使用して null を返さないことを表現できます。

型が null 許容参照型である場合でも、戻り値または引数が null でないことを指定することもできます。 次のメソッドは、その最初の引数が `null` である場合にスローするヘルパー メソッドです。

public static void ThrowWhenNull(object value, string valueExpression = "")
{
    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

次のように、このルーチンを呼び出すことができます。

public static void LogMessage(string? message)
{
    ThrowWhenNull(message, $"{nameof(message)} must not be null");

    Console.WriteLine(message.Length);
}

null 参照型を有効にした後、確実に前のコードが警告なしでコンパイルされるようにする必要があります。 メソッドから制御が戻ったときに、value パラメーターが null でないことが保証されます。 しかし、null 参照で `ThrowWhenNull` を呼び出すことはできます。 `value` を null 許容参照型にして、`NotNull` 事後条件をパラメーター宣言に追加することができます。

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
    // other logic elided

上記のコードは、既存のコントラクトを明確に表しています。呼び出し元では `null` 値を持つ変数を渡すことはできますが、メソッドが例外をスローせずに制御を返す場合、引数が null になることはありません。

無条件の事後条件は、次の属性を使用して指定します。

  • [MaybeNull](xref:System.Diagnostics.CodeAnalysis.MaybeNullAttribute):null 非許容の戻り値は null である可能性があります。
  • [NotNull](xref:System.Diagnostics.CodeAnalysis.NotNullAttribute):null 許容の戻り値が null になることはありません。

条件付きの事後条件: `NotNullWhen``MaybeNullWhen``NotNullIfNotNull`

`string` メソッド は見慣れたものかもしれません。 引数が null または空の文字列の場合、このメソッドから `true` が返されます。 これは、null チェックの形式です。メソッドから `false` が返された場合、呼び出し元で引数の null チェックを行う必要はありません。 この null 許容認識のようなメソッドを作成するには、引数を null 許容参照型に設定し、`NotNullWhen` 属性を追加します。

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

これにより、戻り値が `false` であるコードでは、null チェックを行う必要がないことがコンパイラに通知されます。 属性を追加すると、`IsNullOrEmpty` で必要な null チェックが実行されることがコンパイラの静的分析に通知されます。`false` が返された場合、引数は `null` ではありません。

string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
    int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

注意

上記の例は C# 11 以降でのみ有効です。 C# 11 以降、nameof は、メソッドに適用された属性内で使用されるとき、パラメーターと型パラメーターの名前を参照できます。 C# 10 以前では、nameof 式の代わりに文字列リテラルを使用する必要があります。

.NET Core 3.0 の場合、上記のように メソッドに注釈が付けられます。 コードベースには、オブジェクトの状態で null 値をチェックする、同様のメソッドが存在する場合があります。 コンパイラではカスタムの null チェック メソッドが認識されないため、注釈を自分で追加する必要があります。 属性を追加すると、コンパイラの静的分析で、テストされた変数が null チェックされたかどうが認識されます。

これらの属性のもう 1 つの用途は、`Try*` パターンです。 ref および out 引数の事後条件は、戻り値を通じて伝達されます。 前述のメソッド (Null 許容が無効なコンテキスト) について考えてみましょう。

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

上記のメソッドは、一般的な .NET 表現形式に従います。戻り値は、`message` が見つかった値に設定されているかどうか、またはメッセージが見つからない場合は、既定値に設定されているかどうかを示します。 メソッドから `true` が返された場合、`message` の値は null ではありません。それ以外の場合、メソッドでは `message` が null に設定されます。

Null 許容の有効なのコンテキストでは、NotNullWhen 属性を使用してその表現方法を伝達することができます。 Null 許容参照型のパラメーターに注釈を付けるときは、messagestring? にして属性を追加します。

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message is not null;
}

前述の例では、`TryGetMessage` から `true` が返された場合、`message` の値は null ではないと認識されます。 同じように、コードベースで同様のメソッドに注釈を付ける必要があります。引数は null である可能性があり、メソッドから true が返された場合は null ではないと認識されます。

最後にもう 1 つ属性があります。これも必要になる可能性があります。 戻り値の null 状態は、1 つまたは複数の引数の null 状態に依存する場合があります。 特定の引数が `null` でない場合は常に、これらのメソッドから null 以外の値が返されます。 これらのメソッドに正しく注釈を付けるには、`NotNullIfNotNull` 属性を使用します。 次のメソッドがあるとします。

string GetTopLevelDomainFromFullUrl(string url)

`url` 引数が null でない場合、出力は `null` ではありません。 API が null 引数を受け取る可能性がある場合は、Null 許容参照を有効にした後に、さらに注釈を追加する必要があります。 次のコードに示すように、戻り値の型に注釈を付けることができます。

string? GetTopLevelDomainFromFullUrl(string? url)

これも機能しますが、多くの場合、呼び出し元での追加の `null` チェックの実装が強制されます。 コントラクトは、引数 `url``null` である場合にのみ、戻り値が `null` になるというものです。 このコントラクトを表すには、次のコードに示すように、このメソッドに注釈を付けます。

[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)

前の例では、パラメーター url に演算子 nameof を使用しています。 この機能は C# 11 で使用できます。 C# 11 以前の場合、パラメーターの名前を文字列として入力する必要があります。 戻り値と引数の両方に、`?` で注釈が付けられています。これは、どちらも `null` である可能性があることを示しています。 `url` 引数が `null` でない場合、属性によって、戻り値が null にならないことがさらに明確になります。

条件付きの事後条件は、これらの属性を使用して指定します。

  • [MaybeNullWhen](xref:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute): メソッドから指定された `bool` 値が返された場合、null 非許容の引数は null である可能性があります。
  • [NotNullWhen](xref:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute): メソッドから指定された `bool` 値が返された場合、null 許容引数は null にはなりません。
  • [NotNullIfNotNull](xref:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute):指定されたパラメーターの引数が null でない場合、戻り値は null ではありません。

ヘルパー メソッド: MemberNotNullMemberNotNullWhen

これらの属性は、コンストラクターからヘルパー メソッドに共通コードをリファクタリングする際の目的を指定するために使用します。 C# コンパイラによって、コンストラクターとフィールド初期化子が分析され、各コンストラクターから戻る前に、すべての null 非許容参照フィールドが初期化されていることが確認されます。 ただし、C# コンパイラによって、すべてのヘルパー メソッドを介したフィールドの割り当てが追跡されるわけではありません。 コンストラクターで直接ではなく、ヘルパー メソッドでフィールドが初期化されると、コンパイラから警告 `CS8618` が発行されます。 メソッドの宣言に を追加し、メソッド内で null 以外の値に初期化されるフィールドを指定します。 たとえば、次の例を考えてみましょう。

public class Container
{
    private string _uniqueIdentifier; // must be initialized.
    private string? _optionalMessage;

    public Container()
    {
        Helper();
    }

    public Container(string message)
    {
        Helper();
        _optionalMessage = message;
    }

    [MemberNotNull(nameof(_uniqueIdentifier))]
    private void Helper()
    {
        _uniqueIdentifier = DateTime.Now.Ticks.ToString();
    }
}

`MemberNotNull` 属性コンストラクターへの引数として複数のフィールド名を指定できます。

には `bool` 引数があります。 ヘルパー メソッドによってフィールドが初期化されたかどうかを示す `bool` がヘルパー メソッドから返される状況では、`MemberNotNullWhen` を使用します。

呼び出したメソッドからスローされたときに Null 許容の分析を停止する

一部のメソッド (通常は例外ヘルパーまたはその他のユーティリティ メソッド) は、常に例外をスローすることによって終了します。 あるいは、ヘルパーでは、ブール型引数の値に基づいて、例外がスローされる場合があります。

最初のケースでは、メソッド宣言に 属性を追加できます。 DoesNotReturn を使用して注釈が付けられたメソッドの呼び出しに続くメソッド内のコードは、コンパイラの "null 状態" 分析によって確認されません。 たとえば、次のメソッドがあったとします。

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

FailFast の呼び出しの後に、コンパイラからは警告が発行されません。

2 つ目のケースでは、メソッドのブール型パラメーターに 属性を追加します。 前の例は次のように変更できます。

private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
    if (isNull)
    {
        throw new InvalidOperationException();
    }
}

public void SetFieldState(object? containedField)
{
    FailFastIf(containedField == null);
    // No warning: containedField can't be null here:
    _field = containedField;
}

引数の値が DoesNotReturnIf コンストラクターの値と一致した場合、そのメソッド後は、コンパイラによって "null 状態" 分析が実行されません。

まとめ

null 許容参照型を追加すると、`null` である可能性のある変数に関する API の予測を記述するための、最初のボキャブラリが提供されます。 属性には、変数の null 状態を事前条件および事後条件として記述するために、より豊富なボキャブラリが用意されています。 これらの属性では、予測をより明確に記述し、API を使用して開発者により優れたエクスペリエンスを提供します。

null 許容コンテキストのライブラリを更新する際には、API のユーザーに正しい使用方法を示すために、これらの属性を追加します。 これらの属性は、引数と戻り値の null 状態を完全に記述するのに役立ちます。

  • AllowNull: null 非許容のフィールド、パラメーター、またはプロパティを null にすることができます。
  • DisallowNull: Null 許容のフィールド、パラメーター、またはプロパティを null にすることはできません。
  • MaybeNull: null 非許容のフィールド、パラメーター、プロパティ、または戻り値を null にすることができます。
  • NotNull: Null 許容のフィールド、パラメーター、プロパティ、または戻り値は、null にすることができません。
  • [MaybeNullWhen](xref:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute): メソッドから指定された `bool` 値が返された場合、null 非許容の引数は null である可能性があります。
  • [NotNullWhen](xref:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute): メソッドから指定された `bool` 値が返された場合、null 許容引数は null にはなりません。
  • NotNullIfNotNull: 指定されたパラメーターの引数が null でない場合、パラメーター、プロパティ、または戻り値は null ではありません。
  • DoesNotReturn: メソッドまたはプロパティから返されることはありません。 つまり、常に例外がスローされます。
  • DoesNotReturnIf: 関連付けられた bool パラメーターに指定された値がある場合、このメソッドまたはプロパティから制御が返されることはありません。