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 のよくある key
および message
) があります。 この API には、これらのパラメーターの "null 状態" に関連する次の規則があります。
- 呼び出し元では、
`key` の引数として`null` を渡すことはできません。 - 呼び出し元では、
`message` の引数として、値が`null` の変数を渡すことができます。 `TryGetMessage` メソッドから`true` が返された場合、`message` の値は null ではありません。 戻り値がfalse
の場合、message
の値は null です。
key
の規則は簡潔に表すことができます。key
は null 非許容参照型である必要があります。 null
である変数が許容されますが、成功時には out
の引数が null
ではないことが保証されます。 これらのシナリオでは、予測を記述するために、より豊富なボキャブラリが必要です。 後述する NotNullWhen
属性は、message
パラメーターに使用される引数の "null 状態" を示します。
注意
これらの属性を追加すると、API の規則に関する詳細情報がコンパイラに与えられます。 呼び出し元のコードが、null 許容が有効なコンテキストでコンパイルされると、呼び出し元がこれらの規則に違反したときに、コンパイラによって警告されます。 このような属性では、実装に対するその他のチェックが有効になりません。
属性 | カテゴリ | 説明 |
---|---|---|
null 非許容のパラメーター、フィールド、またはプロパティを null にすることができます。 | ||
Null 許容のパラメーター、フィールド、またはプロパティは、null にすることができません。 | ||
null 非許容のパラメーター、フィールド、プロパティ、または戻り値は、null にすることができます。 | ||
Null 許容のパラメーター、フィールド、プロパティ、または戻り値は、null にすることができません。 | ||
メソッドから指定された |
||
メソッドから指定された |
||
指定されたパラメーターの引数が null でない場合、戻り値、プロパティ、または引数は null ではありません。 | ||
メソッドおよびプロパティ ヘルパー メソッド | メソッドから戻ったときに、列挙されたメンバーは null になりません。 | |
メソッドおよびプロパティ ヘルパー メソッド | 指定された |
|
メソッドまたはプロパティが返されることはありません。 つまり、常に例外がスローされます。 | ||
関連付けられた bool パラメーターに指定された値がある場合、このメソッドまたはプロパティから制御が返されることはありません。 |
前述の説明は、各属性動作に関するクイック リファレンスです。 以下のセクションでは、これらの属性の動作と意味についてさらに詳しく説明します。
事前条件: `AllowNull` と `DisallowNull`
適切な既定値が設定されているため、
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;
null 許容の認識されないコンテキストで上記のコードをコンパイルする場合、何も問題はありません。 null 許容参照型を有効にすると、
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
これと、この記事で説明されている他の属性を使用するには、
前の例では、引数に
- その変数の一般的なコントラクトは、
`null` にできないため、null 非許容参照型が必要であるというものです。 - 呼び出し元から
null
が引数として渡されるシナリオもありますが、よくある使用方法ではありません。
ほとんどの場合、プロパティ、または
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
上記のコードは、
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
null 許容コンテキストでは、ReviewComment
get
アクセサーから null
の既定値が返される可能性があります。 コンパイラからは、アクセスの前にチェックする必要があることが警告されます。 さらに、
- 主なシナリオでは (多くの場合、最初にインスタンス化されるときに)、変数が
`null` である可能性があります。 - 変数は、明示的に
`null` に設定することはできません。
このような状況は、もともと "
[ 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)
検索された名前が見つからなかったときに
public Customer? FindCustomer(string lastName, string firstName)
ジェネリックの NULL 値の許容に関する記事で説明されている理由により、この手法では API と一致する静的分析が生成されない場合があります。 同様のパターンに従うジェネリック メソッドがある場合があります。
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
検索された項目が見つからない場合、このメソッドからは MaybeNull
の注釈を追加することで、項目から見つからない場合にメソッドから null
を返すことを明確にすることができます。
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
上記のコードを使用すると、戻り値が実際には null になる "可能性がある" ことを呼び出し側に通知できます。 また、型が null 非許容であっても、メソッドから null
式が返される可能性があることもコンパイラに通知します。 型パラメーター T
のインスタンスを返すジェネリック メソッドがある場合、NotNull
属性を使用して 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 参照で
public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided
上記のコードは、既存のコントラクトを明確に表しています。呼び出し元では
無条件の事後条件は、次の属性を使用して指定します。
[ MaybeNull](xref:System.Diagnostics.CodeAnalysis.MaybeNullAttribute) :null 非許容の戻り値は null である可能性があります。[ NotNull](xref:System.Diagnostics.CodeAnalysis.NotNullAttribute) :null 許容の戻り値が null になることはありません。
条件付きの事後条件: `NotNullWhen` 、`MaybeNullWhen` 、`NotNullIfNotNull`
bool IsNullOrEmpty([NotNullWhen(false)] string? value)
これにより、戻り値が
string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.
.NET Core 3.0 の場合、上記のように
これらの属性のもう 1 つの用途は、ref
および out
引数の事後条件は、戻り値を通じて伝達されます。 前述のメソッド (Null 許容が無効なコンテキスト) について考えてみましょう。
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
上記のメソッドは、一般的な .NET 表現形式に従います。戻り値は、
Null 許容の有効なのコンテキストでは、NotNullWhen
属性を使用してその表現方法を伝達することができます。 Null 許容参照型のパラメーターに注釈を付けるときは、message
を string?
にして属性を追加します。
bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}
前述の例では、null
である可能性があり、メソッドから true
が返された場合は null ではないと認識されます。
最後にもう 1 つ属性があります。これも必要になる可能性があります。 戻り値の null 状態は、1 つまたは複数の引数の null 状態に依存する場合があります。 特定の引数が
string GetTopLevelDomainFromFullUrl(string url)
string? GetTopLevelDomainFromFullUrl(string? url)
これも機能しますが、多くの場合、呼び出し元での追加の
[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)
前の例では、パラメーター url
に演算子 nameof
を使用しています。 この機能は C# 11 で使用できます。 C# 11 以前の場合、パラメーターの名前を文字列として入力する必要があります。 戻り値と引数の両方に、
条件付きの事後条件は、これらの属性を使用して指定します。
[ 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 ではありません。
ヘルパー メソッド: MemberNotNull
と MemberNotNullWhen
これらの属性は、コンストラクターからヘルパー メソッドに共通コードをリファクタリングする際の目的を指定するために使用します。 C# コンパイラによって、コンストラクターとフィールド初期化子が分析され、各コンストラクターから戻る前に、すべての null 非許容参照フィールドが初期化されていることが確認されます。 ただし、C# コンパイラによって、すべてのヘルパー メソッドを介したフィールドの割り当てが追跡されるわけではありません。 コンストラクターで直接ではなく、ヘルパー メソッドでフィールドが初期化されると、コンパイラから警告
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();
}
}
呼び出したメソッドからスローされたときに 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 状態を完全に記述するのに役立ちます。
- 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
パラメーターに指定された値がある場合、このメソッドまたはプロパティから制御が返されることはありません。
.NET