mcpField
D
dotnet-csharp-nullable-reference-types

dotnet-csharp-nullable-reference-types

github.com/wshaddix開発8677 回インストール35 週間前に更新
リポジトリ

dotnet-csharp-nullable-reference-types について

新旧.NETコードベース向けに、C#のNullable Reference Types(NRT)の注釈と移行を扱うスキルです。段階的な有効化、null状態の契約、よくある誤りを説明します。

主な機能

  1. 01NRTの段階的な有効化
  2. 02nullable・non-nullable型の注釈
  3. 03属性によるnull契約の表現
  4. 04null免除演算子の誤用の確認
  5. 05EF Coreのnull許容マッピングの把握

活用シーン

  1. 01既存プロジェクトでNRTを段階的に有効化
  2. 02Tryパターンのメソッドのnull注釈を改善
  3. 03インターフェース実装間のnull許容性の不一致を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-csharp-nullable-reference-types

インストール後、Claude Code で/dotnet-csharp-nullable-reference-typesを入力して読み込まれたか確認します。

詳細

dotnet-csharp-nullable-reference-types

Null 許容参照型(NRT)の注釈戦略、レガシーコードベースの移行ガイダンス、AI エージェントがよく犯す注釈ミスについて説明します。NRT はすべての最新の .NET テンプレート(net6.0 以降)でデフォルトで有効ですが、既存のコードベースの多くでは引き続き移行が必要です。

相互参照:[skill:dotnet-csharp-coding-standards](null 処理のスタイル)、[skill:dotnet-csharp-modern-patterns](null を使ったパターンマッチング)。


クイックリファレンス:TFM ごとの NRT デフォルト

TFM<Nullable> のデフォルト備考
net8.0+enable(テンプレート内)新規プロジェクトでは NRT がデフォルトで有効です
net6.0/net7.0enable(テンプレート内)net8.0 と同じです
netstandard2.0/2.1未設定明示的に有効化する必要があります
net48 / older未設定明示的に有効化する必要があります

重要: TFM によって NRT が強制されるわけではありません。強制するのは <Nullable>enable</Nullable> MSBuild プロパティです。net8.0 にアップグレードしたレガシープロジェクトでは、有効化されていない場合があります。


NRT の有効化

プロジェクト全体(推奨)

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
ファイル単位(移行時)
#nullable enable   // top of file -- enables NRT for this file only
移行戦略

大規模なコードベースでは、NRT を段階的に有効化します。

  1. プロジェクトに <Nullable>enable</Nullable> を設定します
  2. 既存のすべてのファイルの先頭に #nullable disable を追加します(スクリプトまたは IDE ツールを使用)
  3. ファイルごとに #nullable disable を削除し、警告を修正します
  4. 進捗を追跡します。残っている #nullable disable ディレクティブの数を数えます

注釈パターン

Null 許容と Null 非許容
public class UserService
{
    // Non-nullable: must never be null
    private readonly IUserRepository _repo;

    // Nullable: explicitly may be null
    public User? FindByEmail(string email)
    {
        return _repo.FindByEmail(email); // may return null
    }

    // Non-nullable parameter: caller must provide non-null
    public async Task<User> GetByIdAsync(int id, CancellationToken ct = default)
    {
        return await _repo.GetByIdAsync(id, ct)
            ?? throw new NotFoundException($"User {id} not found");
    }
}
Null 許容属性

コンパイラーが推論できない null 許容の契約を表すには、System.Diagnostics.CodeAnalysis の属性を使用します。

using System.Diagnostics.CodeAnalysis;

// Output is non-null when method returns true
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
    value = _dict.GetValueOrDefault(key);
    return value is not null;
}

// Guarantees member is non-null after method returns
public class Connection
{
    public string? ConnectionString { get; private set; }

    [MemberNotNull(nameof(ConnectionString))]
    public void Initialize(string connectionString)
    {
        ConnectionString = connectionString
            ?? throw new ArgumentNullException(nameof(connectionString));
    }
}

// Return is non-null if input is non-null
[return: NotNullIfNotNull(nameof(input))]
public static string? Trim(string? input)
{
    return input?.Trim();
}

// Parameter must not be null when method returns (for assertion methods)
public static void EnsureNotNull([NotNull] object? value, string paramName)
{
    if (value is null)
    {
        throw new ArgumentNullException(paramName);
    }
}

// Method never returns normally (always throws)
[DoesNotReturn]
public static void ThrowNotFound(string message)
{
    throw new NotFoundException(message);
}
よく使われる属性の概要
属性適用先意味
[NotNullWhen(true)]out パラメーターメソッドが true を返すときは null ではありません
[NotNullWhen(false)]out パラメーターメソッドが false を返すときは null ではありません
[MemberNotNull]メソッド呼び出し後、指定したメンバーは null ではありません
[MemberNotNullWhen(true)]メソッドtrue を返すとき、指定したメンバーは null ではありません
[NotNullIfNotNull]戻り値指定したパラメーターが null でない場合、戻り値も null ではありません
[NotNull]パラメーター呼び出し後、パラメーターは null ではありません(アサーション)
[DoesNotReturn]メソッドメソッドは戻りません(常に例外をスローします)
[AllowNull]パラメーター/プロパティ型が null 非許容でも、呼び出し側は null を渡せます
[DisallowNull]パラメーター/プロパティ型が null 許容でも、呼び出し側は null を渡してはいけません
[MaybeNull]戻り値/out型が null 非許容でも、戻り値は null の場合があります
[MaybeNullWhen(false)]out パラメーターメソッドが false を返すとき、null の場合があります

エージェントが陥りやすい落とし穴

C# コードを生成する AI エージェントがよく犯す NRT のミスを紹介します。

1. 警告を抑制するために !(null 抑制演算子)を使用する
// WRONG -- hides real null bugs
var user = _repo.FindByEmail(email)!;  // will throw NRE if null
string name = user!.Name!;            // double suppression is a red flag

// CORRECT -- handle null explicitly
var user = _repo.FindByEmail(email)
    ?? throw new NotFoundException($"User with email {email} not found");

! 演算子は、コンパイラーでは検証できない知識がある場合にのみ使用してください(例:デバッグアサーションの後、既知のデータを使うテストコード)。

2. Null 許容の警告を無視する
// WRONG -- warning CS8602: Dereference of a possibly null reference
public string GetDisplayName(User? user)
{
    return user.Name; // possible NRE!
}

// CORRECT
public string GetDisplayName(User? user)
{
    return user?.Name ?? "Unknown";
}
3. インターフェイス実装で null 許容を誤る
// Interface says nullable
public interface IRepository
{
    User? FindById(int id);
}

// WRONG -- implementation changes contract
public class UserRepository : IRepository
{
    public User FindById(int id) // removed nullable -- inconsistent
    {
        return _db.Users.First(u => u.Id == id);
    }
}

// CORRECT -- preserve nullable contract
public class UserRepository : IRepository
{
    public User? FindById(int id)
    {
        return _db.Users.FirstOrDefault(u => u.Id == id);
    }
}
4. Try パターンのメソッドで [NotNullWhen] を付け忘れる
// WRONG -- compiler doesn't know result is non-null on success
public bool TryParse(string input, out Order? result)
{
    // ...
}

// After call: result is still Order? even when method returned true

// CORRECT
public bool TryParse(string input, [NotNullWhen(true)] out Order? result)
{
    // ...
}

// After call: result is Order (non-nullable) when method returned true
5. Null 許容値型と Null 許容参照型を混同する
// These are different systems!
int? nullableInt = null;       // Nullable<int> -- always existed
string? nullableStr = null;    // NRT annotation -- compile-time only, no runtime type change

// typeof(int?) != typeof(int), but typeof(string?) == typeof(string)

Null 許容に関するジェネリック制約

// Constrain to non-nullable reference types
public class Repository<T> where T : class
{
    public T Get(int id) => ...;        // T is non-nullable
    public T? Find(int id) => ...;      // T? is nullable
}

// Allow both nullable and non-nullable
public class Cache<T> where T : notnull
{
    public T GetOrDefault(string key, T defaultValue) => ...;
}

// Allow nullable type parameter (default)
public class Wrapper<T>
{
    public T? Value { get; set; }  // T? behavior depends on whether T is value or reference type
}

コレクションと Null 許容

// Dictionary: value might not exist
Dictionary<string, User> users = new();
if (users.TryGetValue(key, out var user))
{
    // user is non-null here (with proper NRT annotations in BCL)
}

// Array/List of nullable items
List<string?> names = ["Alice", null, "Bob"];
foreach (var name in names)
{
    if (name is not null)
    {
        Console.WriteLine(name.Length); // safe
    }
}

// Non-nullable collection with nullable lookup
IReadOnlyList<Order> orders = GetOrders();
Order? first = orders.FirstOrDefault(); // FirstOrDefault returns T? for reference types

EF Core と NRT

EF Core は必須列とオプション列の判定に NRT 注釈を使用します。

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; } = "";  // NOT NULL column
    public string? Notes { get; set; }               // NULL column
    public Address Address { get; set; } = null!;    // Required navigation (EF convention)
}

注: EF Core のナビゲーションプロパティでは、EF による初期化が保証されているため、= null! を使用しても問題ありません。これは null 抑制演算子を使用できる数少ない正当なケースの 1 つです。


参考資料

コメント