Claude Media
Claude CodeでC#のnullable警告を潰す — CS8618・CS8600の直し方

Claude CodeでC#のnullable警告を潰す — CS8618・CS8600の直し方

C#のnullable参照型を有効にすると出るCS8618・CS8600系の警告を、警告コード別の直し方に分けてClaude Codeへ渡す手順です。requiredの使い分けと、null!を増やさせない運用も扱います。

C#のnullable参照型を有効にすると、既存のコードに数十から数百の警告が一度に出ます。件数が多くても、原因は数種類の警告コードに集まります。コードごとに直し方を決めてからClaude Codeに渡すと、場当たり的な ! の貼り付けを避けて片付けられます。

ここでは、警告を数えて分類する手順、CLAUDE.md に書く修正ルール、CS8618・CS8600・CS8602それぞれの直し方、最後に警告をエラーへ上げて再発を止める設定までを順に並べます。

nullable警告が出る条件と、出ないコード

nullable参照型は、コンパイル時だけで働く機能です。実行時の挙動は変わりません。型に string? のような注釈を付けると、コンパイラが「nullかもしれない値をnullでない変数へ入れていないか」「nullかもしれない値を確認せず使っていないか」を静的に追って警告します。

プロジェクトの設定は Nullable で決まります。

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

指定できる値は enable・disable・warnings・annotations の4つです。値を書かなければ disable になります。.NET 6以降のテンプレートで作ったプロジェクトは、最初から enable が入っています。古いプロジェクトでは .csproj に自分で足す必要があります。

警告が出ない場所にも注意が必要です。生成コードのファイルでは、グローバル設定にかかわらずnullableの文脈が無効になります。.g.cs・.generated.cs・.designer.cs で終わるファイル名や、先頭コメントの <auto-generated> が該当します。Claudeに「警告が0になるまで直して」と頼む前に、0件に見える原因が生成ファイルの除外ではないかを確かめておくと取り違えません。

まず警告を警告コード別に数える

最初にやるのは修正ではなく集計です。ビルド出力から警告コードを抜き出し、件数順に並べます。

dotnet build --no-incremental 2>&1 \
  | grep -o 'warning CS86[0-9][0-9]' \
  | sort | uniq -c | sort -rn

--no-incremental は前回の結果を使い回さず、全体を再コンパイルさせるオプションです。結果の上位に並ぶコードが、今回の作業の中心になります。

警告コードの意味はMicrosoftの警告一覧で次のように分かれています。

警告コードメッセージの要旨直し方の系統
CS8618メッセージの要旨コンストラクター終了時に非nullのメンバーが未初期化直し方の系統初期化する・nullableにする・属性で伝える
CS8600メッセージの要旨nullかもしれない値を非null型へ変換直し方の系統右辺の確認・左辺をnullableに
CS8601メッセージの要旨nullかもしれない参照を代入直し方の系統同上
CS8603メッセージの要旨nullかもしれない参照を返している直し方の系統戻り値の型をnullableにする・確認する
CS8604メッセージの要旨引数にnullかもしれない値を渡している直し方の系統呼ぶ前に確認する・引数をnullableにする
CS8625メッセージの要旨nullリテラルを非null参照型へ変換直し方の系統型をnullableにする
CS8602メッセージの要旨nullかもしれない参照の逆参照直し方の系統null確認を足す・属性で状態を伝える
CS8764・CS8765メッセージの要旨オーバーライドした戻り値・引数のnull性が基底と不一致直し方の系統基底の注釈に合わせる

直し方を3つの系統に分けておくと、Claudeへの指示が短くなります。

修正の方針をCLAUDE.mdに書く

CLAUDE.md は、Claudeがセッションの開始時に読む永続的な指示です。警告コード別の方針をここに置けば、毎回プロンプトで繰り返さずに済みます。次は、この記事の方針を CLAUDE.md に写した例です。

## nullable警告の直し方
 
- 確認コマンドは `dotnet build --no-incremental`。警告が減ったことを出力で確かめてから次へ進む
- `!`(null許容演算子)と `= null!` は最後の手段。足したら理由を1行コメントにする
- CS8618: 先にコンストラクターか `required` で初期化できないかを考える。
  任意項目なら `string?` にする。`= string.Empty` は空文字が正当な値のときだけ
- CS8600/8601/8603/8604: 右辺に `??` か null確認を足す。型を `?` にすると
  呼び出し側で新しい警告が出るので、その警告も同じ手順で直す
- CS8602: null確認を足す。確認を関数に切り出すなら `[NotNullWhen(true)]` を付ける
- 警告を消すための `#pragma warning disable` と `<NoWarn>` は使わない
- 生成コード(*.g.cs など)は直さない

最後の2行が効きます。件数を減らす近道として #pragma warning disable に頼る修正は、警告を隠すだけで原因を残します。禁止を明文化しておけば、レビューで見る対象は「! の増減」だけに絞れます。

コマンドの許可も先に整えます。dotnet build を毎回確認されると、修正と検証のループが止まります。Claude Codeの許可ルールは Bash(コマンド *) の形で書きます。

{
  "permissions": {
    "allow": [
      "Bash(dotnet build *)",
      "Bash(dotnet test *)"
    ]
  }
}

これをプロジェクトの .claude/settings.json に置きます。* はサブコマンドの後ろに置くのが決まりです。Bash(dotnet *) のように広げると dotnet publish や dotnet nuget push まで許可されるため、build と test に絞っています。許可ルールの詳細はClaude Codeの/permissionsコマンドの記事にまとめています。

作業の進め方は計画から入る

件数が多いときは、いきなり直させずに計画モードで方針を出させます。計画モードのClaudeはファイルを読んで計画を示すだけで、承認するまで編集しません。

claude --permission-mode plan

プロンプトは、公式のワークフロー集にあるバグ修正の型に合わせます。再現コマンドを渡し、まず方法を提案させ、そのあと適用させる3段階です。

dotnet build --no-incremental を実行して、CS86xx の警告を警告コード別に集計して。
件数が多い上位3つについて、CLAUDE.md の方針に沿った直し方を、
該当ファイルの例を1つずつ挙げて提案して。まだ編集しないで。

提案を読んで方針を承認したら、プロジェクト単位で一気に直すのではなく、警告コードを1つずつ消す単位で進めさせます。1コードを消すたびにビルドし、警告の総数が減ったことを確かめる流れです。計画モードの詳しい使い方は計画モードの記事で扱っています。

CS8618: 初期化されないプロパティを直す

CS8618は最も件数が多く、直し方も複数あります。次のクラスを例にします。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

FirstName と LastName はどちらも初期化が保証されていません。Microsoftの説明では、直し方は次の4系統です。

  1. コンストラクターやフィールド初期化子で、非nullのメンバーをすべて初期化する
  2. メンバーの一部をnullableにする
  3. 初期化を担うヘルパーメソッドに属性を付けて、どのメンバーが設定されるかを伝える
  4. 別のコードで初期化されることを null! で伝える

新しく作るコードなら、1が第一候補です。

public class Person
{
    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }
 
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

ただし、コンストラクターを足すとオブジェクト初期化子で作っている呼び出し側がすべて壊れます。そのようなときの選択肢が required です。

requiredを使う場面と使わない場面

C# 11で入った required は、そのメンバーをオブジェクト初期化子で必ず設定するよう呼び出し側に求めます。設定し忘れるとコンパイルエラーになります。

public class Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public string? MiddleName { get; set; }
}

new Person { FirstName = "Ada", LastName = "Lovelace" } は通り、LastName を省くとエラーです。CS8618も出ません。DTOやリクエストモデルのように、プロパティ設定で組み立てる型に向きます。

使うときの制約もあります。

  • required はメンバーを、含む型と同じかそれ以上の可視性にする必要がある。setterかinitも要る
  • new() 制約付きの型引数に、required メンバーを持つ型は使えない
  • [SetsRequiredMembers] を付けたコンストラクターは、required の初期化チェックをコンパイラが行わなくなる。付けるのは本当に全メンバーを初期化しているコンストラクターだけにする

Claudeに任せるとき、最後の [SetsRequiredMembers] が要注意です。警告を黙らせる目的で付けられると、null! と同じく検査が外れます。CLAUDE.md に「[SetsRequiredMembers] を足したらコンストラクターの全メンバー代入を確認する」と一行足しておくと安全です。

文字列の既定値で埋めるとき

任意ではないが、最初は空で構わない文字列には、= string.Empty が使えます。

public string FirstName { get; set; } = string.Empty;

警告は消えますが、「名前が未入力」と「名前が空文字」を区別できなくなります。空文字が正当な値かどうかは、型の意味を知っている人が判断する部分なので、計画の段階で確認しておきます。

複数コンストラクターとヘルパーメソッド

コンストラクターが複数あり、初期化をprivateなヘルパーに切り出しているコードでは、コンパイラはヘルパーの中身を追いません。[MemberNotNull] 属性でどのメンバーが設定されるかを伝えます。

using System.Diagnostics.CodeAnalysis;
 
public class Student : Person
{
    public string Major { get; set; }
 
    public Student(string firstName, string lastName, string major)
        : base(firstName, lastName)
    {
        SetMajor(major);
    }
 
    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = default)
    {
        Major = major ?? "Undeclared";
    }
}

属性を付けた後でも警告が残る場合は、属性の対象メンバー名と実際に代入しているプロパティ名がずれていないかを見ます。

EF CoreのDbSetはnull! が正当な例外

Entity Framework Coreの DbContext では、DbSet<T> プロパティを = null! で初期化する書き方がMicrosoftの例にもあります。基底の DbContext が実行時に設定するため、コンパイラが追えないだけだからです。CLAUDE.md の「! は最後の手段」に対して例外を明記しておくと、Claudeがこの正当な null! を消そうとして迷走するのを避けられます。

CS8600系: nullかもしれない値を非null変数へ

CS8600・CS8601・CS8603・CS8604・CS8625は、同じ構造の警告です。nullかもしれない式が、非nullの場所に入っています。

string? TryGetMessage(int id) => "";
string msg = TryGetMessage(42);  // CS8600

Microsoftが挙げる直し方は4つです。

  • 左辺をnullableにする。ただし、その変数を使う箇所で新しい警告が出る
  • 代入前にnull確認を挟む
  • 右辺を生むAPIに属性を付ける
  • 右辺に ! を付ける

この中でClaudeに最初に試させたいのは2番目で、既定値があるなら ?? が使えます。

string notNullMsg = TryGetMessage(42) ?? "Unknown message id: 42";

?? の右側に何を置くかは、業務上の意味を決める部分です。「見つからなかったら例外にする」のか「既定文字列で続ける」のかをClaudeが勝手に選ぶと、挙動が変わります。計画の段階で、右辺を既定値で置き換える箇所と、例外を投げる箇所を分けて列挙させると、レビューが楽になります。

前と後

警告を消す直し方の優先順位

BEFORE

件数を減らす近道

string msg = TryGetMessage(42)!;

警告は消えるが、null が実際に返る場合はそのまま NullReferenceException になる。

AFTER

意図を残す直し方

string msg = TryGetMessage(42) ?? throw new InvalidOperationException("id 42 not found");

nullになりうることを型と式の両方に残し、起きたときの挙動が読み取れる。

CS8602: 逆参照の警告と属性

CS8602は、nullかもしれない変数のメソッドやプロパティを、確認なしに使ったときの警告です。

string? message = null;
Console.WriteLine(message.Length);  // CS8602

直し方は3系統で、null確認を足す、APIに属性を付けて状態を伝える、! で状態を強制する、のいずれかです。null確認の判定を IsPresent のような関数に切り出す場合は、[NotNullWhen(true)] を付けておくと、呼び出し側の if の中で ! が要らなくなります。

public static bool IsPresent([NotNullWhen(true)] string? value) =>
    !string.IsNullOrEmpty(value);
 
string? input = ReadInput();
if (IsPresent(input))
{
    Console.WriteLine(input.Length);  // 警告なし
}

属性を付ける対象が自社の共通ユーティリティなら、一度付ければその利用箇所すべての警告が減ります。Claudeに「同じ確認パターンが5箇所以上あるなら、判定関数と属性にまとめる案も出して」と頼むと、! の個別追加より変更が小さくなる箇所が見つかります。

配列とdefault構造体は警告が出ないままnullが残る

警告が消えてもnullが残る箇所が2つあります。Microsoftが既知の落とし穴として挙げているものです。

string[] values = new string[3];  // 要素は実行時に null
string[] initialized = ["a", "b", "c"];  // 全要素が初期化される

new string[3] は警告なしで通りますが、要素はすべて null です。default で作った構造体も、非nullの参照型フィールドが null のまま警告されません。default で作ると、コンストラクターも required も迂回されます。構造体は default を使わない書き方に直します。

警告が0になったことと、実行時にnullが出ないことは別です。配列確保や default の使用箇所は、警告0の後に目視で見直す項目として CLAUDE.md に足しておきます。

警告をエラーに上げて、戻りを止める

警告が片付いたら、nullable関連だけをエラーにして再発を止めます。WarningsAsErrors には nullable という指定があり、nullableに関する警告を一括でエラー扱いにできます。

<PropertyGroup>
  <Nullable>enable</Nullable>
  <WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

TreatWarningsAsErrors を true にすると、nullable以外の警告もすべてエラーになります。移行直後は影響が大きいので、まず nullable だけに絞るほうが無理がありません。

大きなソリューションでは、プロジェクトごとに段階を踏みます。まだ手を付けないプロジェクトは Nullable を disable のままにします。移行中のプロジェクトは warnings にするなど、段階を分けられます。ファイル単位では #nullable ディレクティブも使えます。新規ファイルから順に有効にする進め方も取れます。範囲は #nullable enable と #nullable restore で区切れます。

CIで警告の再発を機械的に止めたいときは、非対話モードの claude -p をスクリプトに組み込む手もあります。ただし、CIの失敗はそもそも WarningsAsErrors のビルドエラーが検出するので、Claudeを挟むのは「失敗の原因をPRコメントに要約させる」用途に絞るのが現実的です。-p の使い方は非対話モードの記事に書きました。

?? や required に直した箇所の挙動変化は、同じ dotnet test で拾えます。Web APIなら統合テストで検証する手順と組み合わせられます。

まとめ

nullable警告の片付けは、件数の多さより、直し方が複数あるせいで判断が散らばるところが難所です。警告コードごとに優先する直し方を CLAUDE.md に書いておくと、Claudeの修正がそろい、レビューで見る点は ! と [SetsRequiredMembers] の増減に絞れます。最後に WarningsAsErrors で nullable をエラーに上げれば、片付けた状態をビルドが守ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. learn.microsoft.com/en-us/dotnet/csharp/nullable-references
  2. learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/n…
  3. learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required
  4. code.claude.com/docs/en/common-workflows
  5. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn