C#のif・switch・nullチェックを短く書く。パターンマッチとディスカード

C#や.NETのバージョンアップを調べていて、「そういえば、if文やnullチェックも短く書けるようになっていたな」と思い出しました。ほかの言語でもパターンマッチを見かけます。C#だと何が省けるのか、前の書き方と今の書き方を並べてみました。

型を調べた値をそのまま使えたり、switchで条件と返す値を一行ずつ並べられたり。短くなるだけでなく、何を見て分岐しているかも追いやすくなります。

書き方を比べる例なので、変数の宣言などは省いています。customerが持つのは、有効かどうかを表すIsActive、年齢のAge、注文のOrderです。動きはC# 13(.NET 9)、C# 14(.NET 10)、C# 15 preview(.NET 11 RC 2)の単体テストで確認しました。null条件付き代入はC# 14以降で確認しています。

型を調べたあと、同じ値をキャストしなくてよい

例えば、valueが文字列なら長さを出す処理です。型を調べてからキャストしていたところが、C# 7からは一行で済みます。

// C# 6以前:型を調べてから、もう一度キャストする
if (value is string)
{
    var text = (string)value;
    Console.WriteLine(text.Length);
}

// C# 7以降(.NET 8 / 9 / 10 / 11 共通):型判定と変数への受け取りを一緒に書く
if (value is string text)
{
    Console.WriteLine(text.Length);
}

value is string textなら、文字列だったときだけtextに入ります。ifの中では、そのtextをそのまま使えます。valueがnullなら、この分岐には入りません。

表は、2026年11月6日に1,000件ループあたりの平均実行時間とメモリ割当を測った結果です。BenchmarkDotNet 0.14.0のReleaseビルド、ShortRun、InProcessNoEmitToolchainを使い、ウォームアップ3回、本測定3回、起動1回。環境はWSL2 / Ubuntu 24.04.4 LTS / Core i7-10510Uです。

全ケースを.NET SDK 11.0.100-rc.2.26504.105、LangVersion=previewでコンパイルし、実行ランタイムを.NET 8〜11 RC 2へ切り替えました。表の列は実行ランタイムです。C# 14以降の構文を.NET 8・9で動かした列は、公式サポート対象外の組合せでの検証です。

計測ではConsole.WriteLineを含めず、文字列の長さの加算や対象件数のカウントに置き換えています。代入の比較で使うCreateOrder()は、既存の文字列"ORD-001"を返すようにしました。

画面に収まらないときは、表を横にスクロールできます。

書き方(1,000件ループ) .NET 8 .NET 9 .NET 10 .NET 11 RC 2 メモリ割当
if (value is string) + (string)value 1.28 μs 1.56 μs 1.26 μs 914.47 ns 0 B
if (value is string text) 938.04 ns 1.83 μs 1.44 μs 920.39 ns 0 B

null確認とプロパティの条件を、まとめて書ける

次は「顧客がnullではなく、有効で、18歳以上なら対象」という例です。C# 8でプロパティパターン、C# 9でis not nullと>= 18のような比較パターンが加わりました。

// C# 7以前:!= null とプロパティ条件を並べる
if (customer != null && customer.IsActive && customer.Age >= 18)
{
    Console.WriteLine("対象");
}

// C# 8:プロパティパターンは使えるが、>= 18 の比較パターンはまだ書けない
if (customer is { IsActive: true } c && c.Age >= 18)
{
    Console.WriteLine("対象");
}

// C# 9以降(.NET 8 / 9 / 10 / 11 共通)①:is not null で null 判定
if (customer is not null && customer.IsActive && customer.Age >= 18)
{
    Console.WriteLine("対象");
}

// C# 9以降(.NET 8 / 9 / 10 / 11 共通)②:プロパティパターンと比較パターンをまとめる
if (customer is { IsActive: true, Age: >= 18 })
{
    Console.WriteLine("対象");
}

同じcustomerを何度も書かなくても、IsActiveとAgeを見れば、どんな顧客が対象か分かります。customerがnullなら、この条件には当たりません。

nullチェックだけならis not nullも使えます。is nullとis not nullは、型に==や!=の独自実装があっても、それを呼ばずにnullかどうかを見ます。

書き方(1,000件ループ) .NET 8 .NET 9 .NET 10 .NET 11 RC 2 メモリ割当
C# 7以前:customer != null && ... 1.09 μs 1.45 μs 1.55 μs 939.64 ns 0 B
C# 8:customer is { IsActive: true } c && c.Age >= 18 1.34 μs 1.30 μs 1.02 μs 1.04 μs 0 B
C# 9+:customer is not null && ... 1.13 μs 1.49 μs 1.13 μs 1.25 μs 0 B
C# 9+:customer is { IsActive: true, Age: >= 18 } 1.13 μs 1.50 μs 1.03 μs 962.92 ns 0 B

switch式なら、条件と返す値が並ぶ

状態の番号から表示名を返すような処理なら、C# 8以降のswitch式で条件と返す値を一行ずつ対応させられます。

// C# 7以前:if を並べて返す
static string GetStatusName(int status)
{
    if (status == 0) return "未処理";
    if (status == 1) return "処理済み";
    return "不明";
}

// C# 8以降(.NET 8 / 9 / 10 / 11 共通):switch式と破棄パターン(_)
static string GetStatusName(int status) => status switch
{
    0 => "未処理",
    1 => "処理済み",
    _ => "不明"
};

0でも1でもなければ、最後の_で「不明」を返します。この_はどの値にも一致する破棄パターンです。

この例だと、「0なら未処理、1なら処理済み、それ以外は不明」が上から読めます。分岐の中で何行も処理するなら、ifやswitch文のほうが素直に読めることもあります。

書き方(1,000件ループ) .NET 8 .NET 9 .NET 10 .NET 11 RC 2 メモリ割当
if (status == ...) の並び 2.05 μs 4.04 μs 2.27 μs 1.75 μs 0 B
status switch { ... _ => } 2.36 μs 3.10 μs 2.70 μs 2.25 μs 0 B

outの値を使わない時も、_で表せる

TryParseで、数値に変換できたかどうかだけ分かればいいことがあります。そんなときはout _です。

// C# 6以前:受け取り用の変数を宣言して渡す
int number;
bool isNumber = int.TryParse(input, out number);

// C# 7以降(.NET 8 / 9 / 10 / 11 共通):使わない出力値を out _ で破棄する
bool isNumber = int.TryParse(input, out _);

out _の_は、出てきた値を捨てるディスカードです。さっきのswitch式では、最後に残った場合を受ける条件でした。変換後の数値も使うなら、out int numberなどで受け取ります。

書き方(1,000件ループ) .NET 8 .NET 9 .NET 10 .NET 11 RC 2 メモリ割当
int number; int.TryParse(input, out number) 13.23 μs 14.21 μs 12.50 μs 10.96 μs 0 B
int.TryParse(input, out _) 13.31 μs 12.09 μs 12.84 μs 11.31 μs 0 B

C# 14では、nullの時に代入しない処理も短く書ける

nullかどうかを見てから代入するところも、C# 14で短くなりました。customerがnullでないときだけOrderに代入するなら、ifで囲んでいた部分が?.を使った一行になります。これはnull条件付き代入という機能です。

// C# 13まで:if で null を確認してから代入する
if (customer is not null)
{
    customer.Order = CreateOrder();
}

// C# 14以降:null条件付き代入で1行にする
customer?.Order = CreateOrder();

customerがnullなら、右辺のCreateOrder()も呼ばれません。.NET 10と.NET 11 RC 2で試すと、nullのときはCreateOrder()が呼ばれた回数は0回でした。

書き方(1,000件ループ) .NET 8 .NET 9 .NET 10 .NET 11 RC 2 メモリ割当
if (customer is not null) customer.Order = ... 3.63 μs 4.54 μs 3.52 μs 3.12 μs 0 B
customer?.Order = CreateOrder()(C# 14+) 4.23 μs 3.36 μs 4.61 μs 3.72 μs 0 B

?.で代入を飛ばした後も、customerはnullかもしれません。Nullableが有効な状態で、次の行でそのままcustomer.OrderにアクセスするとCS8602が出ます。nullならエラーを返す処理では、ifでその分岐が見えるほうが読みやすいです。

どのC#から書けるか

ここまでの書き方が入ったバージョンを並べました。

記法 導入されたバージョン
value is string textのような型判定と変数への受け取り C# 7
out _による値の破棄 C# 7
プロパティパターン C# 8
switch式と、その中の_パターン C# 8
is not null、>= 18などの比較・andなどの論理パターン C# 9
customer?.Order = ...というnull条件付き代入 C# 14

プロパティパターンに>= 18を組み合わせた例は、比較のパターンも使うのでC# 9以降です。

短く書けるようになった。AIのコードも条件まで見る

C#の書き方を昔と並べてみると、短く書けて、条件も追いやすくなったところがけっこうあります。今のifやswitch文で読みやすければ、そのままで十分です。

今回測った処理では、書き換え前後ともメモリ割当は0 Bでした。平均実行時間は増えたものも減ったものもあって、一番差が出たところでも1,000件あたり約2.1 μs。この例なら、読みやすさで書き方を選べそうです。

AIに実装を任せると、普段は自分で使わない書き方も出てきます。レビューでは、元のコードと条件が同じか、nullのときに何が起きるか、プロジェクトのC#で使えるかを見る。初めて見る記法なら、今回のように前の書き方と並べて動きを確かめます。

確認した公式情報

2026年11月7日確認。

コメント

タイトルとURLをコピーしました