C# null 演算子

Tip

この記事は、少なくとも 1 つのプログラミング言語を知り、C# を学習している開発者向けの 基礎 セクションの一部です。 プログラミングを初めて使用する場合は、最初に「 はじめ に」チュートリアルから始めてください。 完全な演算子リファレンスについては、言語リファレンスの メンバー アクセス演算子null 結合演算子を 参照してください。

C# には、null セーフコードを簡潔にする演算子がいくつか用意されています。 これらの演算子を使用すると、コード全体で if (x != null) ガードを入れ子にするのではなく、null セーフ アクセス、フォールバック値、および null テストを 1 つの式で表現できます。

この記事では、null 条件付きアクセスの ?.?[] 、null 結合の ?? 、null 結合割り当ての ??= 、および null パターン マッチングの is null/is not null について説明します。

null 条件付きメンバー アクセス ?.

?.演算子は、オブジェクトが null 以外の場合にのみメンバーにアクセスします。 オブジェクトが null の場合、式全体は null をスローするのではなく、NullReferenceException と評価されます。

string? name = null;

// Without ?., accessing a member on null throws NullReferenceException:
// int len = name.Length; // throws if name is null

// ?. returns null instead of throwing:
int? len = name?.Length;
Console.WriteLine(len.HasValue); // False

name = "C#";
Console.WriteLine(name?.Length); // 2

?. 演算子は、短絡評価を行う: 左側が null の場合、右側のすべてがスキップされます。 メソッド呼び出しは実行せず、副作用も発生しません。

1 つの式で複数の ?. 演算子を連結できます。 チェーンは、最初に検出された null で停止します。

string? input = null;

// Chain ?. across multiple method calls — short-circuits at the first null:
string? upper = input?.Trim()?.ToUpperInvariant();
Console.WriteLine(upper ?? "(none)"); // (none)

input = "  hello  ";
Console.WriteLine(input?.Trim()?.ToUpperInvariant()); // HELLO

null 条件付きインデクサー アクセス ?[]

?[]演算子は、インデクサーと配列アクセスに同じ短絡動作を適用します。 コレクション自体が null場合に使用します。

string[]? tags = null;

// ?[] accesses an element only when the collection is non-null
string? first = tags?[0];
Console.WriteLine(first ?? "(none)"); // (none)

tags = ["csharp", "dotnet", "nullable"];
Console.WriteLine(tags?[0]);          // csharp

null 条件演算子を連結する

複数の ?. 演算子を連結して、null 参照の可能性があるパスを走査します。 最初のnullでチェーンが短絡します。

var order = new Order("ORD-001", null);

// Each ?. short-circuits when null: Customer is null, so Address and City are never accessed
string? city = order.Customer?.Address?.City;
Console.WriteLine(city ?? "(no city)"); // (no city)

var fullOrder = new Order("ORD-002",
    new Customer("Alice", new Address("123 Main St", "Springfield", "IL")));

Console.WriteLine(fullOrder.Customer?.Address?.City); // Springfield

Customernullされると、AddressCityも評価されません。 式全体が nullを返します。

スレッドセーフなデリゲートの呼び出し

?. は、デリゲートを呼び出したりイベントを発生させたりするための、スレッド セーフでクリーンな方法を提供します。 デリゲート式は 1 回だけ評価されるため、null チェックと呼び出しの間に別のスレッドがサブスクライブ解除するウィンドウはありません。

EventHandler? clicked = null;

// No subscribers — ?.Invoke does nothing instead of throwing NullReferenceException
clicked?.Invoke(null, EventArgs.Empty);

clicked += (_, _) => Console.WriteLine("Button clicked!");

// With a subscriber — ?.Invoke calls the handler
clicked?.Invoke(null, EventArgs.Empty);
// Output: Button clicked!

このパターンは、古い if (clicked != null) clicked(...) イディオムを置き換えます。

null 合体演算子 ??

??演算子は、null 以外の場合は左側のオペランドを返し、左がnull場合は右側のオペランドを返します。 これを使用して既定値を指定します。

string? username = null;

// ?? returns the right-hand value when the left-hand is null
string display = username ?? "Guest";
Console.WriteLine(display); // Guest

username = "alice";
display  = username ?? "Guest";
Console.WriteLine(display); // alice

?? は右連想であるため、 a ?? b ?? ca ?? (b ?? c)として評価されます。 最初の null 以外の値が優先されます。 一般的なパターンとして、?.??でチェーンし、?.を使用してnull可能なチェーンを安全に走査します。チェーンが??を返した場合にはnullで既定値に置き換えます。 完全な例については、「 null 演算子を結合する」を参照してください。

null 合体代入演算 ??=

??=演算子は、変数がnullされている場合にのみ、変数に右側の値を割り当てます。 遅延初期化に使用します。

List<string>? cache = null;

// ??= assigns only when the variable is null
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

// cache is already non-null, so LoadData() isn't called again
cache ??= LoadData();
Console.WriteLine(cache.Count); // 3

static List<string> LoadData() => ["alpha", "beta", "gamma"];

右側の式は、変数が nullされている場合にのみ評価されます。 変数に既に値がある場合、右側はまったく評価されません。

null 条件付き代入 (C# 14)

C# 14 以降では、割り当てターゲットとして ?.?[] を使用できます。 代入は、左側のオブジェクトが null 以外の場合にのみ実行されます。

AppConfig? config = new AppConfig();

// Assigns only when config is non-null (C# 14)
config?.Theme = "dark";
Console.WriteLine(config?.Theme); // dark

AppConfig? missing = null;
missing?.Theme = "light";                         // no-op: missing is null
Console.WriteLine(missing?.Theme ?? "(no config)"); // (no config)

右側は、左側が null でないことがわかっている場合にのみ評価されます。

null パターンマッチング:is nullis not null

is nullパターンとis not null パターンは、式がnullされているかどうかをテストします。

string? input = null;

// is null is the preferred test — unaffected by operator overloading
if (input is null)
{
    Console.WriteLine("No input provided.");
}

// == null also works, but a custom == operator can change its behavior
if (input == null)
{
    Console.WriteLine("Still no input.");
}

null チェックでは、is nullよりも== nullを優先します。 ==演算子はオーバーロードできます。つまり、型にカスタム等値演算子が定義されている場合、x == nulltrueされていないときでもxnullを返す可能性があります。 is null パターンは、演算子のオーバーロードに関係なく、常に実際の null 参照をテストします。

string? value = "hello";

if (value is not null)
{
    Console.WriteLine(value.ToUpper()); // HELLO
}

null 演算子を結合する

実際には、多くの場合、これらの演算子のいくつかを組み合わせます。 1 つの式で、ディープ オブジェクト グラフを安全に走査し、フォールバックを適用し、結果を保護できます。

Order? order = GetPendingOrder();

// Chain ?. for safe traversal, ?? for a fallback, is null for a clear guard
string city = order?.Customer?.Address?.City ?? "unknown";

if (order is null)
{
    Console.WriteLine("No pending order.");
}
else
{
    Console.WriteLine($"Shipping to: {city}");
}
// Output: No pending order.

null を許す演算子 !

!後置演算子は、nullable 警告を抑制します。 !を追加して、コンパイラに "この式は間違いなく null ではありません" ことを伝えます。この演算子は実行時には影響しません。 コンパイラの null 状態分析にのみ影響します。

string? name = FindUser("alice");

// Use ! only when you have information the compiler doesn't.
// FindUser guarantees a non-null result for known usernames.
int length = name!.Length;
Console.WriteLine(length); // 5

!控えめに使用してください。また、コンパイラにはない情報がある場合にのみ使用してください。 たとえば、引数チェック ロジックを検証するために意図的に null を渡すテストや、既知の入力に対して null 以外の戻り値がコントラクトによって保証されるメソッドの呼び出しなどがあります。 !を過剰に使用すると、null 許容参照型の目的が無効になります。 全容については、「null 許容参照型」を参照してください。

こちらも参照ください