dotnet-csharp-nullable-reference-types について
新旧.NETコードベース向けに、C#のNullable Reference Types(NRT)の注釈と移行を扱うスキルです。段階的な有効化、null状態の契約、よくある誤りを説明します。
主な機能
- 01NRTの段階的な有効化
- 02nullable・non-nullable型の注釈
- 03属性によるnull契約の表現
- 04null免除演算子の誤用の確認
- 05EF Coreのnull許容マッピングの把握
活用シーン
- 01既存プロジェクトでNRTを段階的に有効化
- 02Tryパターンのメソッドのnull注釈を改善
- 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.0 | enable(テンプレート内) | 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 を段階的に有効化します。
- プロジェクトに
<Nullable>enable</Nullable>を設定します - 既存のすべてのファイルの先頭に
#nullable disableを追加します(スクリプトまたは IDE ツールを使用) - ファイルごとに
#nullable disableを削除し、警告を修正します - 進捗を追跡します。残っている
#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 つです。