csharp-nullable-reference-types について
C# の nullable reference types を導入し、null 許容契約を設計するためのスキルガイドです。段階的な移行、API 属性、レガシー連携、互換性の確認を扱います。
主な機能
- 01nullable 注釈とフロー解析
- 02段階的な NRT 移行
- 03API の null 契約設計
- 04コード分析属性の指針
- 05公開 API の互換性確認
活用シーン
- 01レガシーコードへの NRT 導入
- 02nullable 警告とガードの確認
- 03ライブラリ API の null 動作の注釈
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill csharp-nullable-reference-typesインストール後、Claude Code で/csharp-nullable-reference-typesを入力して読み込まれたか確認します。
同梱ファイルnrt-migration-playbook-reference.mdnullable-attributes-reference.md
詳細
C# Nullable Reference Types
使用する場面
- まだ導入されていないコードベースに nullable reference types(NRT)を導入するとき
T?/nullable 注釈を使用する C# コードを記述またはリファクタリングするときSystem.Diagnostics.CodeAnalysisの nullable 属性で API に注釈を付けるとき- null 許容性のコントラクトが重要な public/internal API を設計するとき
- 注釈のない API やレガシー API をラップし、下流の呼び出し側でも NRT の恩恵を受けられるようにするとき
- null 状態分析、ガードヘルパー、
fieldキーワードが適切に使用されているかコードをレビューするとき
主な目標
- シグネチャで null の意図を明示し、実行時の
NullReferenceExceptionを防ぎます。 - 公式の nullable 属性を使用して、型システムで直接表現できないコントラクトを明示します。
- 大規模な一括書き換えを行わずに、レガシーコードベースへ NRT を段階的に導入します。
null 許容性の基本モデル
null 非許容と null 許容
string— null 非許容の参照型です。コンパイラーはインスタンスがnullにならないと想定し、nullまたは null の可能性がある値を代入すると警告を出します。string?— null 許容の参照型です。変数がnullになる可能性があるため、コンパイラーは逆参照の前に null チェックを要求します。
string name = "Alice";
name = null; // Warning: assigning null to non-nullable.
string? nickname = null;
Console.WriteLine(nickname.Length); // Warning: possible null dereference.
null 状態分析(フロー)
コンパイラーは、参照が確実に null ではないか、null の可能性があるかを追跡します。null チェックと代入によって、この状態が更新されます。
string? message = GetMessageOrNull();
if (message != null)
{
// message is definitely non-null in this block.
Console.WriteLine(message.Length);
}
// Outside the if, message is maybe null again.
null 許容値を逆参照する前に、明示的な null チェック(if (x != null)、is not null、パターンマッチング)を行います。早い段階で null 許容性を絞り込み、null ではない状態を維持します。**null 条件付き代入(C# 14)**を使うと、customer?.Order = CreateOrder(); と記述できます。右辺は、レシーバーが null でない場合にのみ評価されます。
null 抑制演算子(!)
x! は「ここでは x を null ではないものとして扱う」ようコンパイラーに指示します。影響するのは分析のみで、実行時の動作は変わりません。
- 実際の不変条件によって null でないことが保証され、コンパイラーがそれを認識できない場合にのみ
!を使用します。 - 警告への一般的な対処として
!を使用しないでください。制御フローのリファクタリング、属性の追加、適切なメンバー初期化を優先します。
_customer = LoadCustomerFromOrm()!; // ORM guarantees this is not null in valid state.
警告を抑制する前にコードを整理する
ガード節やパターンマッチングが成功すれば、現在のスコープ内に null 安全な領域がすでに作られています。! を追加する前に、null 許容値をチェック済みの境界で一度だけ受け渡し、その後のコードでは null 非許容の状態を保ちます。
- ガード節またはパターンマッチングで早めに絞り込みます。
- null 許容のフィールドやプロパティは、チェックする前にローカル変数へコピーします。これにより、繰り返し読み取る値が分析中に変わるのを防げます。
- メソッドの制御フローが複雑な場合は、null ではない経路を、null 非許容のパラメーターを持つローカル関数または private メソッドに分けることを検討します。
T?、繰り返しのチェック、!を実装全体に広げず、境界で null を処理します。
public void Process(Order? order)
{
if (order?.Customer is not { } customer)
{
return;
}
// The pattern match already proved that customer is non-null here.
Console.WriteLine(customer.Name);
}
nullable 分析を満たすことだけを目的に関数を切り出さないでください。大規模な実装や分岐の多い実装も簡潔になる場合には、明示的に null 非許容の関数境界を設けます。制御フロー、シグネチャ、または nullable 分析属性では表現できない実際の外部不変条件がある場合にのみ、! を使用します。
プロジェクトの構成
新しいコードでは NRT を有効にします。
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
レガシーコードベースでは、ファイル単位のディレクティブ(#nullable enable、#nullable disable、#nullable enable warnings、#nullable enable annotations)を使って段階的に有効にします。CS86xx の nullable 警告を重視し、新しいプロジェクトでは TreatWarningsAsErrors の設定、または nullable 警告をエラーとして扱うことを検討します。
段階的な導入戦略、#nullable ディレクティブのリファレンス、レガシーとの相互運用、既知の静的分析の制限事項(配列、default(struct))については、nrt-migration-playbook-reference.md を参照してください。
API 設計ルール(シグネチャ)
これらのルールは、パブリック API と内部 API、およびモデルに適用されます。
パラメーター — null を許可しない場合は、null 非許容型を使用し、パブリック API には実行時ガードを追加します。
public void SendEmail(string recipient)
{
ArgumentNullException.ThrowIfNull(recipient);
// Implementation
}
null を許可し、意味がある場合は、T? を使用し、null の解釈方法をドキュメントに記載して、適切な null の動作を実装します。
戻り値の型 — メソッドが null を返さない場合は Customer、正当に null を返す可能性がある場合は Customer? を使用します(呼び出し側はチェックする必要があり、コンパイラーによってそのチェックが強制されます)。
public Customer GetRequiredCustomer(Guid id); // Throws on failure.
public Customer? TryGetCustomer(Guid id); // Returns null on failure.
プロパティとフィールド — パラメーターと戻り値の型と同じルールに従います。null 非許容メンバーは、コンストラクター、オブジェクト初期化子で使用する required プロパティ、field を使用した遅延プロパティ、または [MemberNotNull] で注釈されたヘルパーを通じて初期化する必要があります。
public class Order
{
public required string Id { get; init; }
public required Customer Customer { get; init; }
public string? Comment { get; init; } // Optional.
}
コントラクトが入力/出力の動作、条件付きの動作、またはメンバーの初期化に依存する場合は、nullable-attributes-reference.md で説明されている null 許容属性を適用します。
ライブラリのパブリック API 互換性
null 許容注釈と null 許容解析属性は、リリース済み API コントラクトの一部として扱ってください。T と T? は同じ CLR 型であるため、注釈のみの変更は一般にバイナリ互換ですが、null 許容が有効なコンシューマーに警告を発生させることで、ソース互換性を損なう可能性があります。コンシューマーが警告をエラーとして扱っている場合、これらの警告がビルド失敗につながることもよくあります。
リリース前に、パブリック API の null 許容性の変更を確認してください。特に、以下の変更には注意が必要です。
- 出力を
TからT?に弱める、または[MaybeNull]を追加する。 - 入力を
T?からTに厳しくする、または[DisallowNull]を追加する。 class?、class、notnullなどのジェネリック制約を変更する。- 仮想メンバー、インターフェイス、デリゲート、および実装の注釈や属性を変更する(不一致がコンパイラー警告を発生させるため)。
以前は null 許容性が注釈されていなかった API に注釈を追加する場合も、同様のソース互換性の問題が生じる可能性があります。注釈付きの API の公開範囲を最後にリリースしたバージョンと比較し、null 許容が有効なコンシューマーでテストしてください。また、意図的にソース互換性を損なう変更については、ライブラリの互換性ポリシーに従ってドキュメントに記載するか、バージョンを変更してください。
field キーワード(C# 14 / .NET 10)
field コンテキストキーワードを使うと、明示的なバッキングフィールドを宣言せずにプロパティアクセサーの本体を記述できます。これは NRT の主要な用途(遅延初期化プロパティ)であり、コンパイラーは特別な null 耐性 解析を実行するため、コンストラクターで不要な CS8618 が発生しません。
public class C
{
public C() { } // No warning: the getter is null-resilient.
string Prop => field ??= GetPropValue();
}
field の null 許容性ルールの全容(null 耐性あり/なしのゲッター、[field: AllowNull, MaybeNull] による回避策、セッター/コンストラクターの解析)については、nullable-attributes-reference.md を参照してください。
参照ファイル
- nullable-attributes-reference.md:
System.Diagnostics.CodeAnalysis属性の完全なカタログです。事前条件(AllowNull、DisallowNull)、事後条件(MaybeNull、NotNull)、条件付き事後条件(NotNullWhen、MaybeNullWhen、NotNullIfNotNull)、ヘルパーメソッド(MemberNotNull、MemberNotNullWhen)、到達不能コード用ヘルパー(DoesNotReturn、DoesNotReturnIf)、およびfieldキーワードの null 許容性ルールを網羅しています。それぞれに意図、パターン、エージェント向けルールが記載されています。 - nrt-migration-playbook-reference.md:段階的な導入戦略、
#nullableディレクティブのリファレンス、レガシー/注釈なし API との相互運用、古いターゲットフレームワーク向けの null 許容属性のポリフィル(トレードオフと追加前に確認する意思決定プロセスを含む)、既知の静的解析の制限(null 非許容参照の配列、参照フィールドを含むdefault(struct))、警告への対処、および生成チェックリストの全項目を記載しています。
生成チェックリスト(概要)
- プロジェクト —
<Nullable>enable</Nullable>が設定されていること。避けられないレガシーコードの周辺でのみ無効にします。 - 型 — 必須のパラメーター/戻り値/プロパティには非 null 許容型を使用します。
nullが有効であり、想定される場合にのみT?を使用します。 - 初期化 — コンストラクター、
requiredとオブジェクト初期化子、fieldによる遅延ゲッター、または[MemberNotNull]ヘルパーを使用します。ドキュメント化された回避策としての場合を除き、null!は避けます。 - null チェック — パブリックな境界で明示的なガードを設けます。制御フローで対象を絞り、複雑なコードを読みやすくできる場合にのみ非 null 許容のヘルパーを抽出します。
!は明確な不変条件がある場合にのみ使用します。 - 属性 — 型システムでは表現できない契約を示すために適用します(リファレンスファイルを参照)。
- 相互運用 — BCL/注釈付きライブラリを信頼します。注釈のない API をラップする場合は、独自のガードと属性を追加します。
- 警告 — 決して無視しないでください。
!や#pragmaで抑制するのではなく、設計を修正するか属性を追加します。 - 互換性 — リリース済みのライブラリでは、パブリックな null 許容性の変更をソース互換性の破壊につながる可能性がある変更として確認し、null 許容が有効な利用側でテストします。
参考資料
- Null 許容参照型(概要)
- null 状態の静的分析のための属性
- null 許容への移行戦略
- 破壊的変更:null 許容参照型の注釈の変更
- チュートリアル:null 許容/非 null 許容参照型で設計意図を表現する
- C# 14 の新機能(拡張メンバー、
fieldキーワードの正式リリース、null 条件付き代入) fieldコンテキストキーワード(機能仕様)