migrate-nullable-references について
既存のコードベースで C# の nullable reference types を有効にし、関連する警告を解消するスキルです。API の注釈を正確にし、移行中のランタイム動作を変えないことを重視します。
主な機能
- 01nullable 移行の準備状況を評価
- 02プロジェクト単位またはファイル単位で展開
- 03nullable の逆参照警告を解消
- 04公開 API の null 許容性を注釈
- 05既存の警告抑制を監査
- 06ランタイム動作を維持
活用シーン
- 01C# プロジェクトで nullable reference types を有効化
- 02CS86xx 警告を段階的に解消
- 03ライブラリ API の null 許容性を改善
- 04NRT 移行後の抑制設定を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill migrate-nullable-referencesインストール後、Claude Code で/migrate-nullable-referencesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/aspnet-core.mdbreaking-changes.mdef-core.mdnullable-attributes.mdscripts/Get-NullableReadiness.ps1詳細
Nullable Reference Migration
既存のコードベースで C# の nullable reference types(NRT)を有効にし、すべての警告を体系的に解消します。最終的には、<Nullable>enable</Nullable> が設定され、nullable に関する警告がゼロで、公開 API サーフェスに正確な注釈が付いたプロジェクト(またはソリューション)になります。これにより、コンパイラーと利用者の双方に信頼できる null 許容情報が提供されます。
使用する場合
- 既存の C# プロジェクトまたはソリューションで nullable reference types を有効にする場合
- この機能を有効にした後、CS86xx の nullable 警告を体系的に解消する場合
- ライブラリの公開 API サーフェスに注釈を付け、利用者に正確な null 許容情報を提供する場合
- nullable 注釈が追加された依存関係を更新し、新たな警告が発生した場合
- NRT がすでに有効なコードベースの抑制を分析し、削除できるかどうかを判断する場合
使用しない場合
- プロジェクトで
<Nullable>enable</Nullable>がすでに設定され、警告がゼロの場合 — 不要な抑制の削除を目的に見直したい場合を除き、移行は完了しています(ステップ 6 を参照) - ユーザーが警告を修正せずに抑制することだけを望んでいる場合(これは推奨しないでください)
- コードの対象が C# 7.3 以前で、nullable reference types がサポートされていない場合
入力
| 入力 | 必須 | 説明 |
|---|---|---|
| プロジェクトまたはソリューションのパス | はい | 移行する .csproj、.sln、またはビルドのエントリーポイント |
| 移行範囲 | いいえ | project-wide(デフォルト)または file-by-file — 展開戦略を指定します |
| ビルドコマンド | いいえ | プロジェクトのビルド方法(例:dotnet build、msbuild、またはリポジトリ固有のビルドスクリプト)。指定されていない場合はリポジトリから検出します |
| テストコマンド | いいえ | テストの実行方法(例:dotnet test、またはリポジトリ固有のテストスクリプト)。指定されていない場合はリポジトリから検出します |
ワークフロー
🛑 実行時の動作変更はゼロです。 NRT の移行は、メタデータと注釈に関する作業に厳密に限定されます。生成される IL を変更してはなりません — 新しい分岐、新しい null チェック、制御フローの変更、メソッド呼び出しの追加や削除は行わないでください。許容される変更は、nullable 注釈(
?)、nullable 属性([NotNullWhen]など)、!演算子(メタデータのみ)、および#nullableディレクティブだけです。移行中に実行時の null ガードの欠落や潜在的なバグが見つかった場合は、その場で修正しないでください。代わりに、その箇所に// TODO: Consider adding ArgumentNullException.ThrowIfNull(param)コメントを挿入することを提案し、別の変更としてユーザーが対応できるようにしてください。動作を修正する変更を注釈のコミットに決して混在させないでください。
コミット戦略: 論理的な区切りごとにコミットしてください —
<Nullable>を有効にした後(ステップ 2)、逆参照の警告を修正した後(ステップ 3)、宣言に注釈を付けた後(ステップ 4)、nullable 属性を適用した後(ステップ 5)、抑制を整理した後(ステップ 6)です。これにより、各コミットの範囲が明確になり、レビューしやすくなるほか、後のステップで設計上の問題が判明して再検討が必要になった場合にも作業の消失を防げます。ファイル単位で移行する場合は、各ファイルまたは関連するファイルのまとまりごとに個別にコミットしてください。
ステップ 1:準備状況を評価する
省略可:
scripts/Get-NullableReadiness.ps1 -Path <project-or-solution>を実行すると、以下のチェックを自動化できます。このスクリプトは、<Nullable>、<LangVersion>、<TargetFramework>、<WarningsAsErrors>の設定と、#nullable disableディレクティブ、!演算子、#pragma warning disable CS86xxによる抑制の数を報告します。機械で読み取り可能な出力には-Jsonを使用してください。
- プロジェクトのビルド方法とテスト方法を特定します。ビルドスクリプト(例:
build.cmd、build.sh、Makefile)、.slnファイル、または個別の.csprojファイルを確認してください。リポジトリでカスタムビルドスクリプトを使用している場合は、このワークフロー全体を通じてdotnet buildではなく、そのスクリプトを使用してください。 dotnet --versionを実行して、SDK がインストールされていることを確認します。Nullable 参照型(NRT)には C# 8.0 以降(.NET Core 3.0/.NET Standard 2.1以降)が必要です。.csproj(またはプロパティがリポジトリレベルで設定されている場合はDirectory.Build.props)を開き、<LangVersion>と<TargetFramework>を確認します。複数のターゲットフレームワークを指定している場合は、すべての TFM を記録してください。
言語バージョンまたはターゲットフレームワークが不十分な場合は中止してください。
<LangVersion>が 8.0 未満の場合、または C# 7.x が既定となるフレームワーク(明示的な<LangVersion>を指定していない.NET Framework 4.xなど)をプロジェクトが対象としている場合、NRT をそのまま有効にすることはできません。ユーザーには、変更が必要な内容(<LangVersion>8.0</LangVersion>以上を設定するか、.NET Core 3.0+/.NET 5+を対象にすること)を明確に説明し、変更して続行するか、移行を中止するかを確認してください。黙って先に進んだり、変更が承認されていると仮定したりしないでください。
<Nullable>がすでに設定されているか確認します。enableに設定されている場合は、ステップ 5 に進み、残りの警告を監査します。- プロジェクトの種類を判定します。これは、移行全体を通じてアノテーションの優先順位を決める基準になります。
- ライブラリ:まずパブリック API の契約に重点を置きます。パブリックなパラメーターまたは戻り値の型に付ける
?は、利用側が依存する契約の変更となります。正確かつ慎重に対応してください。 - アプリケーション(Web、コンソール、デスクトップ):デシリアライズ、データベースクエリ、ユーザー入力、外部 API のレスポンスなど、境界での null 安全性に重点を置きます。内部の処理には、より柔軟にアノテーションを付けられます。
- テストプロジェクト:アノテーションの厳密さの優先度は低めです。null が想定されないテストのセットアップやアサーションでは、
!をより積極的に使用してください。テストコードが問題なくコンパイルできることを重視します。
- ライブラリ:まずパブリック API の契約に重点を置きます。パブリックなパラメーターまたは戻り値の型に付ける
ステップ 2:段階的な導入方法を選ぶ
コードベースの規模と活動状況に応じて、次のいずれかの方法を選びます。ユーザーに方法を推奨し、続行前に確認してください。
複数プロジェクトからなるソリューション: 依存関係の順序に沿って移行します。まず共有ライブラリとコアプロジェクトを移行し、その後、それらを利用するプロジェクトに進みます。依存先を先にアノテーションすることで、利用側に連鎖する警告がなくなり、同じ作業を二度行わずに済みます。
どの方法を選ぶ場合も、中心から外側へ進めてください。まず、依存関係が少なく、広く利用されているコアドメインモデル、DTO、共有ユーティリティ型から始めます。これらを先にアノテーションすると、コードベース全体に連鎖する警告がなくなり、作業効率が最も高まります。次に、コア型に依存する上位レイヤーのサービス、コントローラー、UI コードへと進みます。この方法なら、各段階で発生する警告の数を抑えられ、大規模なプロジェクト全体での有効化による大量の警告に圧倒されるのを防げます。変更セットをレビューしやすく、焦点を絞れるように、プロジェクトごと、またはレイヤーごとに少なくとも 1 つの PR を作成することを推奨します。必要なアノテーションが比較的少ない場合は、プロジェクト全体で一度に有効化し、単一の PR で対応してもかまいません。
方法 A — プロジェクト全体で有効化する(小規模から中規模のプロジェクト)
ソースファイルが概ね 50 個未満のプロジェクトや、チームが一度に作業を完了したい場合に適しています。
.csprojの<PropertyGroup>に<Nullable>enable</Nullable>を追加します。- ビルドし、すべての警告に対処します。
戦略 B — 警告を先に解消し、その後アノテーションを追加する(大規模または活発に開発中のプロジェクト)
コードベースが大きい場合や、複数のコントリビューターによって活発に開発されている場合に適しています。
.csprojに<Nullable>warnings</Nullable>を追加します。型の意味を変えずに警告を有効にします。- ビルドし、Step 3 以降のすべての警告を修正します。
<Nullable>enable</Nullable>に変更してアノテーションを有効にします。これにより、警告が再び大量に発生します。- Step 4 以降のアノテーション段階で発生した警告を解消します。
戦略 C — ファイルごとに対応する(非常に大規模なプロジェクト)
プロジェクト全体で有効にすると、管理できないほど大量の警告が発生する、大規模なレガシーコードベースに適しています。
- プロジェクトレベルで
<Nullable>disable</Nullable>を設定します(または設定を省略します)。 - 移行する各ファイルの先頭に
#nullable enableを追加します。 - 依存関係の順にファイルの優先順位を付けます。まず共有ユーティリティとモデルから始め、次に上位のコンシューマーに進みます。
ビルドのチェックポイント:
<Nullable>を有効にした後(または最初のファイル群に#nullable enableを追加した後)、クリーンビルドを実行します(例:dotnet build --no-incrementalを実行するか、事前にbin/objを削除します)。増分ビルドでは変更されたファイルのみが再コンパイルされるため、変更されていないファイルの警告が見えなくなります。最初に発生した警告の数を記録します。これを今後減らしていく基準値とします。プロジェクトが引き続きコンパイルできることを確認する前に、警告の修正に進まないでください。このワークフローの以降のすべてのビルドチェックポイントでも、クリーンビルドを使用します。
Step 3: 逆参照に関する警告を修正する
優先順位: 依存関係の順にファイルを確認します。他のコードが依存するコアモデルと共有ユーティリティから始め、その後、上位のコンシューマーに進みます。各ファイル内では、まず public メンバーと protected メンバー(契約を定義するもの)を修正し、次に internal メンバーと private メンバーを修正します。この順序にすると、連鎖的に発生する警告を最小限に抑えられます。コア型のアノテーションを修正すると、コンシューマー側の警告が自動的に解消されることもよくあります。
プロジェクトをビルドし、逆参照に関する警告を順に修正します。特によく発生する警告は次のとおりです。
| 警告 | 意味 | 一般的な修正方法 |
|---|---|---|
| CS8602 | null の可能性がある参照の逆参照 | アノテーションのみの修正を優先します。null が有効な場合は上流の型を nullable(T?)にするか、この時点で値が null ではないと確認できる場合は ! を使用します。null チェックや ?. の追加は実行時の動作を変更します。その対応は別のコミットに留保してください(上記の動作変更ゼロのルールを参照) |
| CS8600 | null の可能性がある値を nullable でない型に変換 | null が有効な場合は対象の型に ? を追加するか、値が決して null ではないと確認できる場合は ! を使用します。null ガードの追加は実行時の動作を変更します |
| CS8603 | null 参照を返す可能性 | メソッドが実際に null を返す可能性がある場合は、戻り値の型を nullable(T?)に変更します。メソッドが実際に null を返す可能性がある場合、! で抑制してはいけません。代わりに戻り値の型を修正します。これは NRT 移行で最も重要なルールです。nullable でない戻り値の型は、null が決して返されないことをすべての呼び出し元に約束するものです |
| CS8604 | null 参照の可能性がある引数 | null が有効な場合はパラメーターを nullable として指定するか、引数が確実に null ではない場合は ! を使用します。引数を渡す前に null チェックを追加すると、実行時の動作が変更されます |
❌ 逆参照に関する警告の手っ取り早い修正として
?.を使ってはいけません。obj.Method()をobj?.Method()に置き換えると、実行時の動作がひそかに変更されます。例外がスローされる代わりに、呼び出しがスキップされます。null を許容したい場合に限り、?.を使用してください。
❌ 警告を抑制するために、やみくもに
!を追加してはいけません。!はそれぞれ、値が決して null ではないという主張です。その主張が誤っていれば、NullReferenceExceptionを隠してしまうことになります。代わりに、null チェックを追加するか、型を nullable にしてください。 ❌ 戻り値の型を非 null 許容に保つために、return null!を決して使用しないでください。 メソッドがnullを返す場合、戻り値の型はT?でなければなりません。return null!と記述すると、非 null 許容のシグネチャの背後に null が隠れます。呼び出し側はシグネチャを信頼して null チェックを省略し、実行時にNullReferenceExceptionが発生します。これはnull!、default!、およびコンパイラーに非 null 許容の位置で null を受け入れさせるあらゆるキャストに当てはまります。戻り値に!を使ってよいのは、その値が 確実に null ではないものの、コンパイラーがその理由を判断できない場合に限られます。
⚠️ 実行時の型を変更する意図がない限り、値型に
?を付けないでください。 参照型の場合、?はメタデータにのみ影響します。値型(int、列挙型、構造体)の場合、?を付けると型がNullable<T>に変わり、メソッドのシグネチャ、バイナリレイアウト、ボックス化の動作が変わります。
警告ごとの判断フローチャート:
- ここでは設計上、null は有効な値ですか?
- はい → 宣言に
?を付けます(null 許容にします)。 - いいえ → 手順 2 に進みます。
- 不明 → 続行する前にユーザーに確認します。
- はい → 宣言に
- この時点で値が決して null にならないことを証明できますか?
- はい。コンパイラーが把握できないコードパスによって証明できます → 理由を説明するコメントを添えて
!を付けます。 - はい。ガードを追加すれば証明できます → null チェック(
if、??、is not null)を追加します。 - いいえ → 型は null 許容にする必要があります(手順 1 に戻ります。答えは「はい」です)。
- はい。コンパイラーが把握できないコードパスによって証明できます → 理由を説明するコメントを添えて
ガイダンス:
- null 許容演算子(
!)よりも、明示的な null チェック(if、is not null、??)を優先してください。 - null 許容演算子は、値が決して null にならないことを証明できるがコンパイラにはそれができない場合にのみ使用し、その理由を説明するコメントを追加してください。
- ガード句ライブラリ(例: Ardalis.GuardClauses、Dawn.Guard)はしばしばパラメーターを
[NotNull]で修飾し、ガード呼び出し後に null 状態を絞り込みます。Guard.Against.NullOrEmpty(value, nameof(value))の後では、コンパイラはすでにstring?をstringに絞り込んでいます。後続の代入で冗長な!を追加しないでください。コンパイラが助けを必要とすると決めつける前に、ガードメソッドが[NotNull]を使用しているかどうかを確認してください。 - メソッドが正当に null を返す場合は、戻り値の型を
T?に変更してください。null 許容でないシグネチャの背後に null を隠さないでください。 Debug.Assert(x != null)は、ifチェックとまったく同様に、コンパイラに対する null 状態のヒントとして機能します。メソッドやブロックの先頭でこれを使用して、不変条件をフローアナライザーに伝え、そのスコープ内の後続の!演算子を排除してください。注意:Debug.Assertはコンパイラに情報を伝えますが、Release ビルドでは除去されます。実行時の null に対して保護するものではありません。パブリック API の境界では、明示的な null チェックまたはArgumentNullExceptionを優先してください。- 内部メソッドのすべての呼び出し箇所で
!を追加していることに気づいたら、代わりにそのパラメーターを null 許容にすることを検討してください。!は、コンパイラが真に非 null を証明できない場合のために取っておいてください。 - ブール値を返すヘルパーメソッドの結果が、null 許容パラメーターが非 null であることを保証する場合(例:
if (IsValid(x))がx != nullを意味する場合)、すべての呼び出し箇所で!を使用するよりも、ヘルパーのパラメーターに[NotNullWhen(true)]を追加することを優先してください。これはメタデータのみの変更(動作の変更なし)であり、下流の!演算子を排除しつつ、コンパイラに実際のフロー情報を提供します。 - 構築後に常に設定されるフィールド(例: フレームワーク、
Init()メソッド、ビルダーパターンによるもの)については、すべての使用箇所で!を追加するよりも、フィールド宣言で= null!を優先してください。50 回アクセスされるフィールドには、50 個のfield!アサーションではなく、1 つの= null!があるべきです。これにより、型システムではフィールドを非 null のままにしつつ、遅延初期化を認めることができます。可能であれば、初期化メソッドに[MemberNotNull]を組み合わせてください。 - 制約のない型パラメーターで
defaultを返すジェネリックメソッド(例:FirstOrDefault<T>)については、T?ではなく[return: MaybeNull] Tを使用してください。制約のないジェネリックにT?と書くと、値型のシグネチャがNullable<T>に変更され、メソッドのシグネチャとバイナリレイアウトが変わります。[return: MaybeNull]は元のシグネチャを維持しつつ、参照型に対して戻り値が null になる可能性があることを伝えます。 - LINQ の
Where(x => x != null)はT?をTに絞り込みません。コンパイラは、ジェネリックメソッドに渡されるラムダを通じて null 許容性を追跡できません。正しい型の絞り込みを行って null をフィルタリングするには、source.OfType<T>()を使用してください。
ビルドチェックポイント: 逆参照の警告を修正したら、アノテーションの警告に進む前にビルドして、CS8602/CS8600/CS8603/CS8604 の警告が残っていないことを確認してください。
ステップ 4: 宣言にアノテーションを付ける
まず、各メンバーの設計目的に基づいて意図する null 許容性を決定してください。このパラメーターは null を受け入れるべきでしょうか? この戻り値は null になり得るでしょうか? それに応じてアノテーションを付け、その結果生じる警告に対処してください。警告にアノテーションを導かせないでください。そうすると、? で過剰にアノテーションを付けたり、コンパイラを黙らせるために ! を散りばめたりすることになります。
ユーザーに確認すべき場合: API コントラクトを推測してはいけません。使用頻度や命名規則だけから null 許容の意図を推測してはいけません。コードまたはドキュメントで意図が明示されていない場合は、ユーザーに確認してください。具体的には、次の変更を行う前に確認してください。(1)公開メソッドの戻り値の型を null 許容に変更する、または公開パラメーターに
?を追加する。これは、利用側が依存する API コントラクトを変更します。(2)設計意図が明確でない場合に、プロパティを null 許容にするか必須にするかを決める。(3)コンテキストから null が有効な状態か判断できない場合に、null チェックと!のどちらを使うか選ぶ。使用状況から答えが明らかな内部/プライベートメンバーについては、確認せずに進めてください。
❌ 警告に引きずられて注釈を付けないでください。 まず各メンバーで意図されている null 許容性を判断してから、注釈を付けてください。警告を消すためにあらゆる箇所へ
?を追加するのは目的に反し、呼び出し側に不要な null チェックを強いることになります。あらゆる箇所へ!を追加すると、バグが隠れてしまいます。
⚠️ 戻り値の型には、コンパイラーを満足させるだけでなく、意味上の null 許容性を反映してください。 よくある間違いは、実装で
default!やキャストを使ってコンパイラーの要求を満たしているために、戻り値の型から?を削除することです。メソッドが設計上 null を返す可能性がある場合は、コンパイラーが警告するかどうかにかかわらず、戻り値の型を null 許容にする必要があります。主なパターンは次のとおりです。
*OrDefaultという名前のメソッド(FirstOrDefault、SingleOrDefault、FindOrDefault)→ 「or default」は参照型の場合「or null」を意味するため、戻り値の型は null 許容(T?、object?、dynamic?)でなければなりません。ExecuteScalarおよび同様のデータベースメソッド → 該当する行がない場合、結果はDBNull.Valueまたは null になり得るため、戻り値の型はobject?でなければなりません。Find、TryGet*(out パラメーター)、および検索メソッド → 項目が存在しない可能性がある場合、戻り値の型は null 許容にしてください。- 失敗、未検出、または空の入力時に null を返すとドキュメントに記載されている、または設計されているメソッド → 戻り値の型を null 許容にしてください。
実装で
!やdefault!を使って null を隠していると、コンパイラーは戻り値の型に?がないことを検出できません。そのため、利用側にとって注釈が誤ったものになります。利用側は null 非許容のシグネチャを信頼して null チェックを省き、実行時にNullReferenceExceptionが発生することになります。
⚠️ 既存の
ArgumentNullExceptionチェックを削除しないでください。 null 非許容のパラメーター注釈はコンパイル時のヒントにすぎず、実行時に null が渡されるのを防ぐものではありません。古い C# バージョンを使う呼び出し側、他の .NET 言語、リフレクション、または!を使う呼び出し側は、依然として null を渡すことができます。
⚠️ 実行時の null 検証が欠けている公開 API メソッドには注意を促してください。ただし、チェックは追加しないでください。 注釈を付ける際は、各
publicおよびprotectedメソッドを確認してください。パラメーターが null 非許容(Tであり、T?ではない)なら、実行時の null チェック(ArgumentNullException.ThrowIfNull(param)やif (param is null) throw new ArgumentNullException(...)など)があるべきです。チェックがないと、実行時に null が渡された場合、メソッドの入口で明確なArgumentNullExceptionが発生する代わりに、メソッド本体の深い箇所でNullReferenceExceptionが発生します。null ガードの追加は実行時の動作変更であり、NRT 移行の一部にしてはいけません。代わりに、その箇所に// TODO: Consider adding ArgumentNullException.ThrowIfNull(param)コメントを挿入するかどうかをユーザーに確認してください。呼び出し側で NRT が有効になっていない可能性があるライブラリでは、特に重要です。 null に対する動作が定義されているメソッドでは、nullable パラメーターを受け取るようにします。 null の入力を適切に処理し、null を返す、既定値を返す、または例外をスローする代わりに失敗結果を返すメソッドでは、パラメーターをTではなくT?にします。BCL もこの規約に従っています。Path.GetPathRoot(string?)は null の入力に対して null を返しますが、Path.GetFullPath(string)は例外をスローします。null によって例外が発生する場合に限り、非 nullable パラメーターを使用してください。メソッドが実際には null を許容するのに、パラメーターを非 nullable としてマークすると、呼び出し側は呼び出し前に不要な null チェックを追加する必要があります。判断に迷うケース: パラメーターについて null が検証、無害化、またはドキュメントで説明されていない場合は、次の点を検討します。(1)独自のコードベースで null が渡されることはありますか?ある場合 → nullable。(2)呼び出し側が null を「既定値」や何もしないことを示すプレースホルダーとして使う可能性はありますか?ある場合 → nullable。(3)同じ領域の類似メソッドは null を受け入れますか?受け入れる場合 → 一貫性を保つため nullable。(4)メソッドは null をほとんど意識せず、たまたま正常に動作するだけで、API の目的上、null に意味がない場合 → non-nullable。パラメーターを nullable にするか non-nullable にするか迷った場合は、nullable を優先してください。より安全であり、後から制約を厳しくできます。
参照解除の警告を解消した後、注釈に関する警告に対処します。
| 警告 | 意味 | 典型的な修正方法 |
|---|---|---|
| CS8618 | 非 nullable フィールド/プロパティがコンストラクターで初期化されていない | メンバーを初期化するか、nullable(?)にするか、required(C# 11+)を使用します。コンストラクターの後、コンストラクター外(たとえばフレームワークのライフサイクルメソッド、Init() の呼び出し、またはビルダーパターン)で必ず設定されるフィールドには、= null! を使用して、すべての使用箇所で非 nullable を維持したまま意図を宣言します。ヘルパーメソッドがフィールドを初期化する場合は、そのメソッドに [MemberNotNull(nameof(field))] を付けて、呼び出し後にフィールドが非 null であることをコンパイラーに知らせます |
| CS8625 | null リテラルを非 nullable 型に変換できない | 対象を nullable にするか、非 null の値を指定します |
| CS8601 | null 参照が代入される可能性がある | CS8600 と同じ方法を使用します |
各型について、次の点を判断します。このメンバーは null になることがありますか?
- はい → 宣言に
?を追加します。 - いいえ → すべてのコンストラクターパスで初期化されるようにするか、
required(C# 11+)としてマークします。 - いいえ。ただし、コンストラクターの後で設定されます(たとえばフレームワークのメソッド、ビルダー、または二段階初期化パターンによって設定される場合)→ フィールド宣言に
= null!を使用します。これにより、フィールドの型は使用箇所で常に非 nullable に保たれ、「アクセス前に必ず設定されることを保証する」とコンパイラーに伝えられます。すべての使用箇所に!を追加するよりも、はるかに望ましい方法です。50 回アクセスするフィールドなら、= null!を 1 回使う代わりに!演算子を 50 回付けることになります。特定のメソッドで初期化する場合は、そのメソッドに[MemberNotNull(nameof(field))]を付けることも検討してください。
注釈の対応は、まず public および protected API に重点を置きます。これらが利用者の依存する契約を定義するためです。内部コードやプライベートコードでは、外部の呼び出し側に影響しないため、より広く ! を使用できます。
公開ライブラリ:破壊的変更を追跡します。 プロジェクトが他のユーザーに利用されるライブラリの場合は、
nullable-breaking-changes.mdファイル(または同等のファイル)を作成し、利用者に影響する可能性のある公開 API の変更をすべて記録してください。参照型に?を追加することはメタデータのみの変更であり、バイナリ互換性を破壊する変更ではありませんが、NRT を有効にしている利用者にとってはソース互換性を破壊する変更です。新たな警告やエラーが発生します。記録すべき主な変更:
- 戻り値の型を
TからT?に変更(利用者は null を処理する必要があります)- パラメーターを
T?からTに変更(利用者は null を渡せなくなります)- パラメーターを
TからT?に変更(呼び出し側の既存の null チェックが不要になります。影響は小さいものの、記録する価値があります)- 値型のパラメーターまたは戻り値に
?を追加(TがNullable<T>に変わり、バイナリ互換性を破壊します)- これまで存在しなかった
ArgumentNullExceptionガードの追加- 注釈付けの際に発見して修正した動作上の変更(例:null を暗黙に受け入れていたメソッドが、例外をスローするようになった)
このファイルをユーザーに提示してレビューしてもらってください。リリースノートのベースとしても使用できます。
特に注意する点:
- DTO とドメインモデル:クラスの役割に応じて、異なる null 許容性の方針を適用してください。DTO とシリアル化モデルは信頼境界(JSON、フォーム、外部 API)を越えるため、シリアライザーによって強制されない限り、プロパティはデフォルトで null 許容にしてください。宣言された型にかかわらず、デシリアライズされたデータは常に null になり得るためです。null 以外の制約を強制するには、
required(C# 11 以降)、[JsonRequired](.NET 7 以降)、または実行時検証を使用してください。ドメインモデルは内部不変条件を表すため、コンストラクターで強制する null 非許容プロパティを優先し、無効な状態を表現できないようにしてください。移行時に最も問題が起きやすいのは、この区別です。DTO をドメインモデルとして扱うと、実行時にNullReferenceExceptionが発生します。ドメインモデルを DTO として扱うと、あらゆる場所で不要な null チェックが必要になります。 - イベントハンドラーとデリゲート:
EventHandler? handler = SomeEvent; handler?.Invoke(...)というパターンは慣用的です。 - 構造体の参照型フィールド:構造体の参照型フィールドは、
default(T)を使用すると null になります。構造体でdefaultが有効な使用方法である場合、これらのフィールドは null 許容にする必要があります。defaultが想定されない場合(構造体が特定の API によってのみ作成される場合)は、すべての利用者に不要な null チェックを強いることを避けるため、null 非許容のままにしてください。 - Dispose 後の状態:オブジェクトの有効なライフタイム全体でフィールドまたはプロパティが null 以外であり、
Dispose後に null になる可能性がある場合は、null 非許容のままにしてください。破棄後のオブジェクトの使用は契約違反です。その場合に注釈を緩和しないでください。 - オーバーライドとインターフェイスの実装:オーバーライドは、基底メソッドが宣言する型よりも厳密な(null 非許容の)戻り値の型を返せます。実装が null を返さず、基底クラスまたはインターフェイスが
T?を返す場合、オーバーライドの戻り値をTとして宣言できます。パラメーターの型は基底クラスと完全に一致させる必要があります。 - 広くオーバーライドされる仮想戻り値の型:多くのクラスがオーバーライドする仮想メソッドまたは抽象メソッドでは、既存のオーバーライドが実際に null を返すかどうかを検討してください。よく null を返す場合(
Object.ToString()など)は、戻り値をT?として注釈付けしてください。呼び出し側がそれを知る必要があります。null を返すオーバーライドがほとんどない場合(Exception.Messageなど)は、Tとして注釈付けしてください。広くオーバーライドされる仮想メソッドで判断に迷う場合は、T?を優先してください。 IEquatable<T>とIComparable<T>:参照型はIEquatable<T?>とIComparable<T?>(null 許容のTを使用)を実装する必要があります。呼び出し側がEqualsやCompareToに null を渡すことはよくあるためです。Equals(object?)のオーバーライド:Equals(object? obj)のオーバーライドでは、パラメーターに[NotNullWhen(true)]を追加してください。Equalsがtrueを返す場合、引数は null ではないことが保証されます。これにより、呼び出し側は等価性の検査後に冗長な null チェックを省略できます。
ビルドのチェックポイント: 宣言への注釈付け後、ビルドを実行し、null 許容属性に進む前に CS8618/CS8625/CS8601 の警告が残っていないことを確認してください。
ステップ 5: 高度なシナリオ向けに null 許容属性を適用する
単純な ? 注釈では null コントラクトを表現できない場合は、System.Diagnostics.CodeAnalysis の属性を適用します。完全な属性一覧([NotNullWhen]、[MaybeNullWhen]、[MemberNotNull]、[AllowNull]、[DisallowNull]、[DoesNotReturn] など)と各属性の使用ガイダンスについては、references/nullable-attributes.md を参照してください。
ビルドチェックポイント: null 許容属性を適用したら、ビルドして属性が対象の警告を解決し、新しい警告を発生させていないことを確認します。
ステップ 6: 抑制をクリーンアップする
任意:
scripts/Get-NullableReadiness.ps1を再実行して、プロジェクト全体の#nullable disableディレクティブ、!演算子、#pragma warning disable CS86xx抑制の現在の数を取得します。
- 一時的な回避策として追加された
#nullable disableディレクティブや!演算子を検索します。 - それぞれについて、その抑制がまだ必要かどうかを判断します。
- 不要になった抑制を削除します。残すものについては、その理由を説明するコメントを追加します。
#pragma warning disable CS86を検索して抑制された null 許容警告を見つけ、根本的な問題を代わりに修正できないか評価します。
ビルドチェックポイント: 抑制を削除したら、再度ビルドします。
#nullable disableや!を削除すると、修正が必要な新しい警告が表面化する可能性があります。
ステップ 7: 検証する
- プロジェクトをビルドし、null 許容警告がゼロであることを確認します。
- プロジェクトファイル(リポジトリ全体の場合は
Directory.Build.props)に<WarningsAsErrors>nullable</WarningsAsErrors>を追加して、null 許容のリグレッションを恒久的に防止します。これはdotnet build /warnaserror:nullableに相当するプロジェクトファイルの設定です。 - 既存のテストを実行し、リグレッションがないことを確認します。
- プロジェクトがライブラリの場合は、公開 API サーフェスを調査して、null 許容注釈が意図したコントラクトと一致していることを確認します(null を受け入れるパラメーターは
T?、null を拒否するパラメーターはT)。
移行が完了したと報告する前に検証してください。 警告がゼロであることだけでは、移行が正しいとは言えません。成功を報告する前に:(1) 公開 API シグネチャをスポットチェックし、
?注釈がコンパイラを黙らせるだけでなく実際の設計意図と一致していることを確認する、(2) 実行時の動作を変える?.演算子が追加されていないことを確認する(diff 内の?.を検索)、(3)ArgumentNullExceptionチェックが削除されていないことを確認する、(4)!演算子がまれであり、それぞれに正当化するコメントがあることを確認する。
検証
- プロジェクトファイルに
<Nullable>enable</Nullable>が含まれている(ファイル単位の戦略の場合はファイルごとに#nullable enable) - ビルドで CS86xx 警告がゼロになる
- リグレッションを防ぐために
<WarningsAsErrors>nullable</WarningsAsErrors>がプロジェクトファイルに追加されている - テストがリグレッションなしで成功する
- コメントで正当化されていない限り、
#nullable disableディレクティブが残っていない - null 抑制演算子(
!)がまれであり、それぞれに正当化するコメントがある - 公開 API シグネチャが null コントラクトを正確に反映している
- 公開ライブラリの場合: 破壊的変更が
nullable-breaking-changes.mdに文書化され、ユーザーによってレビューされている
コードレビューチェックリスト
null 許容への移行の変更は、通常の diff よりも広範なレビューを必要とします。
- 動作変更がないことを確認する:
?と!だけが追加されていることを確認します。偶発的な?.や、削除された null チェック、新しい分岐がないことです。生成される IL は null 許容メタデータ以外は変更されていないはずです。 - 明示的な注釈の変更をレビューする: パラメーターや戻り値の型に追加されたすべての
?について、意図した設計と一致していることを確認します。そのメソッドは本当に null を受け入れますか?本当に null を返すことができますか? - スコープ内の変更されていない API をレビューする:
<Nullable>enable</Nullable>を有効にすると、そのスコープ内の注釈のないすべての参照型が暗黙的に null 非許容になります。変更されていない公開メンバーをスキャンして、実際には null を受け入れるのに注釈が付いていないパラメーターがないか確認します。
NRT アノテーションによる破壊的変更(ライブラリ)
ライブラリについては、references/breaking-changes.md を参照してください。NRT アノテーションはパブリック API 契約の一部であり、誤ったアノテーションは利用側にとってソース互換性を損なう変更になります。
よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
警告を抑えるために、あちこちに ! を付ける | null 許容演算子はバグを隠します。null チェックを追加するか、型を null 許容型に変更してください |
警告を手早くなくすために、すべてを T? にする | ? を過剰に付けると本来の目的が損なわれ、呼び出し側で不要な null チェックが必要になります。null が有効な値である場合にのみ ? を使用してください |
| コンストラクターですべての非 null 許容メンバーを初期化していない | すべてのコンストラクターでフィールドとプロパティを初期化するか、required(C# 11 以降)を使用するか、メンバーを null 許容にしてください |
| シリアライザーがコンストラクターを回避するため、非 null 許容でも実行時の安全性は保証されない | シリアライザーはコンストラクターを呼び出さずにオブジェクトを作成するため、非 null 許容の DTO プロパティも実行時には null になる可能性があります。詳しくは、ステップ 4 の「DTO とドメインモデル」を参照してください |
| 生成コードで警告が発生する | <auto-generated> コメントが含まれている場合、生成ファイルは自動的に null 許容解析の対象から除外されます。それでも警告が出る場合は、生成ファイルの先頭に #nullable disable を追加するか、.editorconfig で generated_code = true を設定してください |
| マルチターゲットプロジェクトと古い TFM | NRT アノテーションは、C# 8.0 以降であれば古い TFM(例:.NET Standard 2.0)でもコンパイルできますが、[NotNullWhen] などの null 許容属性が存在しない場合があります。NuGet の Nullable などのポリフィルパッケージを使用するか、属性を内部で定義してください |
| 依存関係のアップグレード後に警告が再び表示される | 依存関係に null 許容アノテーションが追加されています。これは想定どおりであり、有益なことです。ステップ 3~5 と同様に、新たな警告を修正してください |
| アノテーションを付ける際に、意図せず動作を変更する | 型に ? を追加したり、式に ! を追加したりするのはメタデータのみの変更であり、生成される IL は変わりません。しかし、obj.Method() を obj?.Method()(null 条件演算子)に置き換えると、実行時の動作が変わります。例外がスローされる代わりに、呼び出しが何も通知されずにスキップされます。警告への手早い対処としてではなく、null を許容する意図がある場合にのみ ?. を使用してください |
値型(列挙型、構造体)に ? を追加する | 参照型の場合、? は実行時の影響がないメタデータアノテーションです。int や列挙型などの値型の場合、? によって型が Nullable<T> に変わり、メソッドシグネチャ、バイナリレイアウト、ボックス化の動作に影響します。値型を本当に null 許容にする意図がある場合を除き、? の追加対象が参照型のみであることを必ず確認してください |
| 既存の null 引数検証を削除する | 非 null 許容アノテーションはコンパイル時にのみ適用されるため、呼び出し側は実行時に null を渡すことができます。既存の ArgumentNullException チェックは残してください。詳しくはステップ 4 を参照してください |
var は代入式から null 許容性を推論する | var を使用すると、推論された型には代入式の null 許容性が含まれるため、T と T? を明示的に宣言する場合とは異なる結果になり、戸惑うことがあります。その後の実際の null 状態はフロー解析によって判断されますが、推論された宣言型には予期しない null 許容性が含まれる場合があります。宣言時の null 許容性を正確に指定する必要がある場合は、var ではなく型を明示してください |
| アノテーションのない(null 許容性が不明な)ライブラリを使用する | 依存関係が null 許容アノテーションを有効にしていない場合、コンパイラーはそのすべての型を「null 許容性不明」として扱います。そのため、逆参照や null の代入に関する警告は出ません。これは誤った安心感につながります。特に、概念上 null を返す可能性のあるメソッド(辞書検索や FirstOrDefault 形式の呼び出しなど)については、null 許容性不明の API の戻り値が null になる可能性があるものとして扱ってください。可能であれば、依存関係をアップグレードするか、呼び出しをラップしてください |
Entity Framework Core に関する考慮事項
プロジェクトで EF Core を使用している場合は、references/ef-core.md を参照してください。NRT を有効にすると、データベーススキーマの推論やマイグレーションの出力が変わる可能性があります。
ASP.NET Core に関する考慮事項
プロジェクトで ASP.NET Core を使用している場合は、references/aspnet-core.md を参照してください。NRT を有効にすると、MVC のモデル検証や JSON のシリアル化の動作が変わることがあります。
詳細情報
- Nullable reference types — この機能、nullable コンテキスト、コンパイラーによる解析の概要
- Nullable reference types (C# reference) — nullable 注釈と警告コンテキストに関する言語リファレンス
- Nullable migration strategies
- Embracing Nullable Reference Types — 導入のタイミングとエコシステムに関する考慮事項についての、Mads Torgersen によるガイダンス
- Resolve nullable warnings
- Attributes for nullable static analysis
- ! (null-forgiving) operator — 演算子とその使用場面に関する言語リファレンス
- EF Core and nullable reference types
- .NET Runtime nullable annotation guidelines — .NET ライブラリ自体に注釈を付ける際に使用される注釈の原則