【C#】パターンマッチング完全ガイド|全パターン種別・switch式・is式・list/positional/relational・C#バージョン別機能まで

【C#】パターンマッチングの強化|switch式の活用 C#

C# のパターンマッチングは C# 7.0 で導入されて以来、C# 8〜12 にかけて型・定数・関係・論理・プロパティ・位置・List の10種類近いパターンに拡張され、単なる switch 文の置き換えではなく、条件分岐全体の書き方を変える機能に進化しました。

本記事では全パターン種別を網羅的に整理し、switch 式・is 式・when 節の使い分け、C# バージョン別の追加機能、網羅性(CS8509)警告、records / Deconstruct との連携、実践的な設計パターンまで体系的に解説します。

スポンサーリンク

C# バージョン別パターン機能の歴史

パターンマッチングは段階的に拡張されてきました。どのバージョンで何が使えるかを把握しておくと、ターゲットフレームワークに応じた書き方を選べます。

C# バージョン 追加されたパターン・機能
C# 7.0 (VS 2017 / .NET Framework 4.6+ / .NET Core 1.0+) is 型 変数 宣言パターン、switch 文の case 型 変数when
C# 8.0 (.NET Core 3.0) switch 、プロパティパターン、位置パターン(Deconstruct)、タプルパターン、破棄パターン _
C# 9.0 (.NET 5) 関係パターン(<, >=)、論理パターン(and / or / not)、括弧パターン、型パターンのみの省略形
C# 10.0 (.NET 6) 拡張プロパティパターン({ Prop.Nested: ... }
C# 11.0 (.NET 7) List パターン[1, 2, ..])、スライスパターン(.. var rest
C# 12.0 (.NET 8) 主要コンストラクタ・コレクション式と組み合わせた使用が容易に
使えるパターンは言語バージョンで決まる
プロジェクトの言語バージョンは <LangVersion> で指定できます。.NET 8 SDK 以上であれば latest を指定すると C# 12 の全パターンが使えます。古い .NET Framework 4.x でも C# 7.3 程度までは使えますが、プロパティパターン以降は .NET Core 3.0+ / C# 8 以上が必要です。

10種類のパターン種別 — 早見表

パターン 構文例 意味 導入
定数パターン case 42: 値が一致するか C# 7.0
宣言パターン is Person p 型チェック+変数宣言 C# 7.0
型パターン is Person(変数なし) 型チェックのみ C# 9.0
var パターン var x 必ずマッチ+変数束縛 C# 7.0
破棄パターン _ マッチするが値は使わない C# 8.0
プロパティパターン { Age: >= 18 } プロパティ値でマッチ C# 8.0
位置パターン (1, var y) Deconstruct / タプル要素でマッチ C# 8.0
関係パターン > 0, <= 100 比較演算子でマッチ C# 9.0
論理パターン and, or, not パターンを組み合わせる C# 9.0
List パターン [1, 2, .., 9] コレクションの並びでマッチ C# 11.0

パターンを使う3つの場所 — switch式・switch文・is式

パターンが使える3つの文脈
object value = 42;

// ① switch 式(C# 8+)— 式として値を返す
string label = value switch
{
    int n when n > 0   => $"正の整数: {n}",
    int n              => $"整数: {n}",
    string s           => $"文字列: {s}",
    null               => "null",
    _                  => "その他",
};

// ② switch 文(C# 7+)— 従来のステートメント
switch (value)
{
    case int n when n > 0:
        Console.WriteLine("正の整数");
        break;
    case string s:
        Console.WriteLine($"文字列: {s}");
        break;
    default:
        break;
}

// ③ is 式(C# 7+)— 単純な型チェック+変数宣言
if (value is int number)
    Console.WriteLine($"数値: {number}");

// ④ is 式でプロパティ・定数パターン(C# 8+)
if (value is Person { Age: >= 18 } adult)
    Console.WriteLine($"成人: {adult}");

// ⑤ is 式で否定パターン(C# 9+)
if (value is not null)
    Console.WriteLine("値がある");
文脈 用途 ポイント
switch 値を返す分岐 戻り値の型が一意に決まる。網羅性チェック(CS8509)
switch 副作用を伴う分岐 casebreak or return が必須
is 単一条件の判定+変数束縛 if / while / 三項演算子などで気軽に使える

基本パターン — 定数・宣言・型・var・破棄

基本となる5つのパターン
// ① 定数パターン: リテラル・null・enum・const 値と一致するか
string status = code switch
{
    200 => "OK",
    404 => "Not Found",
    500 => "Internal Server Error",
    _   => "Unknown",
};

// ② 宣言パターン: 型チェック+変数宣言(最も使う)
if (obj is string s && s.Length > 3)
    Console.WriteLine(s.ToUpper());

// ③ 型パターン(C# 9+): 変数を使わない型チェックのみ
if (obj is string)
    Console.WriteLine("文字列です");

// ④ var パターン: 必ずマッチ+変数に束縛(型推論あり)
//    計算結果の中間変数を switch 内で使いたいときに便利
int result = input switch
{
    > 0            => 1,
    < 0            => -1,
    var zero       => zero, // マッチする変数として 0 を取り出せる
};

// ⑤ 破棄パターン _ : マッチするが値は使わない
//    switch 式の default 相当として使われる
string category = day switch
{
    1 or 2 or 3 or 4 or 5 => "平日",
    6 or 7                => "週末",
    _                     => "不正な曜日",
};

// 破棄は位置パターンの「この要素は何でもよい」としても機能する
if (point is (0, _)) // X=0 ならば Y は何でもよい
    Console.WriteLine("Y 軸上");
破棄 _default の違い
switch 文の default は節の位置によって扱いが変わりますが、switch 式の _パターンなので「それまでのパターンで全マッチしなかった場合」に確実に拾います。位置は最後でなくてもよいですが、到達不能コードになるため警告が出ます。慣習として _ => ... は最後に書いてください。

プロパティパターン — オブジェクトの中身で分岐

プロパティパターンの基本
public sealed record Customer(string Name, int Age, string Country, decimal Balance);

// 単純プロパティマッチ
string discount = customer switch
{
    { Age: >= 65 }                        => "シニア20%",
    { Age: < 13 }                         => "子供30%",
    { Country: "Japan", Balance: > 10000 } => "VIP10%",
    _                                     => "通常",
};

// ネストしたプロパティパターン(C# 8+ でも path 記法は拡張プロパティパターン=C# 10+)
public sealed record Address(string City, string Country);
public sealed record Order(Customer Customer, Address ShipTo);

string shippingFee = order switch
{
    { ShipTo: { Country: "Japan" } }  => "国内",    // C# 8+ のネスト記法
    { ShipTo.Country: "USA" }         => "アメリカ", // C# 10+ の拡張プロパティパターン
    _                                 => "その他",
};
プロパティパターンで変数を束縛
// プロパティパターンの中で変数を取り出すと、その値を右辺で使える
string message = customer switch
{
    { Age: >= 65, Name: var n }              => $"{n} さんシニア割引",
    { Age: var a, Name: var n } when a < 13  => $"{n} さん({a}歳)子供割引",
    _                                        => "通常料金",
};

// is 式でも使える
if (order is { Customer: { Age: >= 18 } adult, ShipTo.City: var city })
    Console.WriteLine($"{adult.Name} へ {city} に配送");

位置パターン — Deconstruct / タプル要素でマッチ

Deconstruct を使った位置パターン
// record または Deconstruct を持つ型で使える
public sealed record Point(int X, int Y);

// 位置パターン(X, Y の順序)
string quadrant = point switch
{
    (0, 0)              => "原点",
    (> 0, > 0)          => "第1象限",
    (< 0, > 0)          => "第2象限",
    (< 0, < 0)          => "第3象限",
    (> 0, < 0)          => "第4象限",
    (_, 0)              => "X軸上",
    (0, _)              => "Y軸上",
};

// タプルパターン: 複数の値を同時にマッチ(引数が2つ以上の条件で便利)
public enum Light { Red, Yellow, Green }

string nextAction = (light, isEmergency) switch
{
    (Light.Red, true)    => "停止",
    (Light.Red, false)   => "停止して待機",
    (Light.Yellow, _)    => "減速",
    (Light.Green, _)     => "進行",
};

// 位置パターン内にプロパティパターンをネスト(C# 8+)
string priceCategory = order switch
{
    (_, { Country: "Japan" })    => "国内",
    var (c, a) when c.Age > 65   => $"シニア@{a.City}",
    _                            => "その他",
};

関係パターン・論理パターン(C# 9+)

関係パターンと論理演算の組み合わせ
// 関係パターン: <, <=, >, >= をパターンの中で直接使える
string bmiCategory = bmi switch
{
    < 18.5 => "低体重",
    < 25.0 => "標準",
    < 30.0 => "肥満(1度)",
    _      => "肥満(2度以上)",
};

// 論理パターン: and / or / not でパターンを組み合わせる
bool isPrintableAscii = ch is >= ' ' and <= '~';
bool isLetterOrDigit  = ch is (>= 'a' and <= 'z')
                          or (>= 'A' and <= 'Z')
                          or (>= '0' and <= '9');
bool isNotNull        = value is not null;

// switch 式での組み合わせ
string range = number switch
{
    0                    => "ゼロ",
    > 0 and <= 100       => "1〜100",
    > 100 and <= 1000    => "101〜1000",
    > 1000 and not 9999  => "1000超(9999は除く)",
    < 0                  => "負数",
    _                    => "その他",
};

// not パターンで型を絞る
object? obj = GetValue();
if (obj is not null and string s)  // null 以外かつ string
    Console.WriteLine(s);

// 括弧パターン: 優先順位を明示
bool inRange = n is (> 0 and < 10) or (> 90 and < 100);
論理パターンの優先順位に注意
パターン演算子の優先順位は not > and > or です。例えば is A or B and Cis A or (B and C) と解釈されます。期待と違う動作になった場合は迷わず括弧で優先順位を明示してください。読み手の負担も減らせます。

List パターン(C# 11+)— 配列・リストの並びでマッチ

List パターンとスライスパターン
// 基本: 長さと要素の並びでマッチ
int[] arr = { 1, 2, 3 };
bool isOneTwoThree = arr is [1, 2, 3];     // true
bool startsWithOne = arr is [1, ..];       // true(先頭が 1、残りは任意)
bool endsWithThree = arr is [.., 3];       // true
bool hasMiddle     = arr is [_, 2, _];     // 長さ3で真ん中が 2

// スライスパターン: 残りを変数に束縛
int[] data = { 1, 2, 3, 4, 5 };
if (data is [var first, .. var middle, var last])
{
    Console.WriteLine(first);                   // 1
    Console.WriteLine(string.Join(",", middle)); // 2,3,4
    Console.WriteLine(last);                    // 5
}

// switch 式と組み合わせ
string Describe(int[] nums) => nums switch
{
    []                 => "空",
    [var only]         => $"1要素: {only}",
    [var a, var b]     => $"2要素: {a}, {b}",
    [1, 2, .. var rest] => $"1,2 で始まり残り {rest.Length} 要素",
    [.., var last]     => $"最後は {last}",
};

// List や Span でも使える(ICollection + GetEnumerator 等を満たす型)
List<string> tags = new() { "C#", "dotnet", "pattern" };
bool hasThree = tags is [_, _, _];
List パターンの対象は配列・List・Span など
List パターンは Length / Count[int](インデクサ)を持つ型で使えます。スライスパターン .. var rest が使えるのは、[Range]Span<T>.Slice・配列の GetSubArray 等)をサポートする型に限られます。独自コレクションを List パターン対応にするには Count[int]Slice(int, int) を実装してください。

when 節 — パターンで表現できない条件を補う

when 節でパターンに任意の条件を追加
// when: パターン+任意の bool 式の組み合わせ
// パターンだけでは表現しにくい条件(複数変数の関係など)に使う

string classify = (min, max) switch
{
    (var lo, var hi) when lo > hi => "範囲不正",
    (0, 0)                        => "空範囲",
    (var lo, var hi)              => $"{lo}〜{hi}",
};

// 型パターン+when で「型が X かつ条件を満たす」
int SpecialLength(object obj) => obj switch
{
    string s when s.Length > 100 => -1,          // 長すぎる
    string { Length: 0 }         => 0,           // 空文字列
    string s                     => s.Length,
    int[] a when a.Length > 1000 => -1,
    int[] a                      => a.Length,
    _                            => throw new ArgumentException(),
};

// when は is 式でも使える(ただし is の文法上は式の外にくる)
if (value is int i && i % 2 == 0)  // 慣習的にこれが使われる
    Console.WriteLine("偶数");

// switch 文の when
switch (customer)
{
    case { Age: >= 18 } adult when adult.Country == "JP":
        // 日本在住の成人
        break;
}
when 節は最後の手段
関係パターン・プロパティパターン・論理パターンで表現できる条件は、when ではなくパターン側に書くのが慣習です。パターンで書ける条件は網羅性チェック(CS8509)の対象になりますが、when 節の条件はコンパイラが解析できないため、網羅性の判断が弱くなってしまいます。「複数変数にまたがる条件」「外部状態に依存する条件」など、パターンで書けない時だけ when を使います。

網羅性チェック(CS8509)と到達不能コード(CS8510)

switch 式の網羅性をコンパイラに検証させる
// コンパイラは「すべての入力パターンが網羅されているか」を静的に判定する
// 網羅されていない場合 CS8509 警告を出す(実行時に MatchFailureException が起きる危険)

public enum Shape { Circle, Square, Triangle }

// NG: Triangle が漏れている → CS8509 警告
double Area(Shape s, double size) => s switch
{
    Shape.Circle => Math.PI * size * size,
    Shape.Square => size * size,
    // Triangle と未定義値が漏れている → 警告
};

// OK ①: すべての enum 値+未定義値を拾う
double AreaSafe(Shape s, double size) => s switch
{
    Shape.Circle   => Math.PI * size * size,
    Shape.Square   => size * size,
    Shape.Triangle => size * size / 2,
    _              => throw new ArgumentOutOfRangeException(nameof(s)),
};

// OK ②: sealed record / 判別共用体的な階層で網羅性を得る
public abstract record Payment;
public sealed record CreditCard(string Number) : Payment;
public sealed record BankTransfer(string Iban)  : Payment;
public sealed record Cash                       : Payment;

decimal Fee(Payment p) => p switch
{
    CreditCard   => 0.03m,
    BankTransfer => 0.01m,
    Cash         => 0m,
    // sealed 階層なので網羅性が検出される
};

records・Deconstruct との強力な連携

records を判別共用体(Discriminated Union)として使う
// sealed record を階層化すると判別共用体として使え、
// 位置パターンで各ケースを網羅的に処理できる

public abstract record Expr;
public sealed record Num(double Value)          : Expr;
public sealed record Add(Expr L, Expr R)        : Expr;
public sealed record Mul(Expr L, Expr R)        : Expr;
public sealed record Neg(Expr Inner)            : Expr;

// 再帰的パターンマッチで数式評価
static double Eval(Expr e) => e switch
{
    Num(var v)       => v,
    Add(var a, var b) => Eval(a) + Eval(b),
    Mul(var a, var b) => Eval(a) * Eval(b),
    Neg(var x)       => -Eval(x),
    _                => throw new NotSupportedException(),
};

// 使用例: (1 + 2) * -3 を評価
Expr expr = new Mul(new Add(new Num(1), new Num(2)), new Neg(new Num(3)));
Console.WriteLine(Eval(expr)); // -9

// 定数だけ先に処理するパターン
static Expr Simplify(Expr e) => e switch
{
    Add(Num(0), var x)     => x,               // 0 + x → x
    Add(var x, Num(0))     => x,               // x + 0 → x
    Mul(Num(1), var x)     => x,               // 1 * x → x
    Mul(Num(0), _) or Mul(_, Num(0)) => new Num(0), // 0 * x → 0
    Neg(Neg(var x))        => x,               // -(-x) → x
    _                      => e,
};

パフォーマンス — コンパイラ生成コードの理解

パターンマッチングのパフォーマンスは概ね if-else の連鎖と同等かそれ以上です。定数パターンのみの switch ではジャンプテーブルバイナリサーチにコンパイルされる場合があり、多数の分岐で高速です。

パターンの性質 コンパイラの戦略 備考
定数パターン(整数 / enum)多数 ジャンプテーブル or バイナリサーチ O(1) or O(log n) で分岐
定数パターン(文字列) ハッシュベースの分岐テーブル O(1) に近い
型パターン(多数) 型チェックの直列 if-else 頻度の高い型を上に置くと速い
関係・論理・プロパティ 条件式の直列展開 網羅的に書けば最適化されやすい
List パターン Length 比較+要素アクセスの直列展開 要素数の違いは先にふるい分け
パフォーマンスを気にする前に読みやすさを優先
分岐数が数十までならswitch式とif-elseの差はほぼ誤差です。「ジャンプテーブル化できるか」を気にして書き方を歪めるよりも、パターンマッチング本来の意図を明確に表現できる構造を優先してください。本当にホットパスなら BenchmarkDotNet で計測してから判断すれば十分です。

実践パターン集

パターン① — HTTP ステータスコードの分類
static string Classify(int code) => code switch
{
    < 100 or >= 600     => "不正",
    < 200               => "情報",
    < 300               => "成功",
    < 400               => "リダイレクト",
    < 500               => "クライアントエラー",
    _                   => "サーバーエラー",
};
パターン② — null安全な値取得(Null-Object パターン)
public sealed record User(string Name, int Age);

static string Greet(User? user) => user switch
{
    null                     => "ゲストさん、ようこそ",
    { Age: < 18, Name: var n } => $"{n}くん、こんにちは",
    { Name: var n }          => $"{n} さん、ようこそ",
};
パターン③ — Result 型(Success/Failure)の分岐
public abstract record Result<T>;
public sealed record Success<T>(T Value)    : Result<T>;
public sealed record Failure<T>(string Error) : Result<T>;

static void HandleResult(Result<int> result)
{
    switch (result)
    {
        case Success<int>(var value):
            Console.WriteLine($"成功: {value}");
            break;
        case Failure<int>(var msg):
            Console.WriteLine($"失敗: {msg}");
            break;
    }
}
パターン④ — 型階層による多態分岐(Visitor の代替)
public abstract record Shape;
public sealed record Circle(double Radius)                     : Shape;
public sealed record Rectangle(double Width, double Height)    : Shape;
public sealed record Triangle(double Base, double Height)      : Shape;

static double Area(Shape s) => s switch
{
    Circle(var r)           => Math.PI * r * r,
    Rectangle(var w, var h) => w * h,
    Triangle(var b, var h)  => b * h / 2,
};

// 新しい形状を追加したい時は Shape を継承して record を作り
// Area の switch に1行追加するだけ(かつ網羅性警告で漏れを検出)
パターン⑤ — 設定の検証
public sealed record Config(string Host, int Port, string? ApiKey, int Timeout);

static string? Validate(Config c) => c switch
{
    { Host: null or "" }           => "Host が未設定",
    { Port: <= 0 or > 65535 }      => "Port が範囲外",
    { ApiKey: null or "" }         => "ApiKey が必要",
    { Timeout: < 0 }               => "Timeout は 0 以上",
    _                              => null,  // 問題なし
};

よくある質問

Qswitch 式と switch 文はどう使い分ければよいですか?
A「値を返す・1つの式として扱える」処理なら switch 式、「副作用(複数文の実行・例外スロー・break/return による制御)」が必要なら switch 文です。現代のC#では、値を返したいときはほぼ常に switch 式を選ぶのがベストプラクティスで、if 文を switch 式に置き換えることで網羅性チェックと宣言的な書き方の両方が得られます。
QCS8509(網羅性警告)を消す一番良い方法は?
A一番安全なのは末尾に破棄パターン _ => throw new UnreachableException() を追加することです(.NET 7+ の UnreachableException、それ以前は InvalidOperationException)。こうすれば「全ケースを網羅したつもり」が崩れた時(enum に値が追加された等)に実行時に早期検知できます。単に _ => default にすると、バグがサイレントに通過するため避けてください。
Qプロパティパターンとタプルの位置パターンはどちらを使うべき?
Aレコード型で Deconstruct が自然なら位置パターン (x, y)、プロパティ名の明示で読み手に意図を伝えたいならプロパティパターン { X: 0, Y: 0 } を使います。位置が3つ以上になるとどの要素が何か分かりづらくなるため、フィールド数が多い型ではプロパティパターンが読みやすくなります。両者は混在可能で、Order { Customer: (var name, _), ShipTo.City: "Tokyo" } のように組み合わせられます。
QList パターンと LINQ のどちらを使うべきですか?
A「並びの構造」を見たいなら List パターン、「条件にマッチする要素の集合」を扱いたいなら LINQ です。例えば「配列の先頭が 1 で末尾が 9」は arr is [1, .., 9] が自然で、「すべての要素が偶数」は arr.All(x => x % 2 == 0) が自然です。パース系(トークン列の先頭が特定のキーワード等)は List パターンが非常に強力です。
Qパターンマッチングは例外フィルター(when 句)でも使えますか?
Acatch (Exception) when (...)when 節では通常の bool 式しか書けませんが、中に is 式を使うことで実質的にパターンマッチングができます。例: catch (HttpRequestException ex) when (ex is { StatusCode: HttpStatusCode.NotFound })。例外フィルターの詳細は 例外フィルター(when句)完全ガイドを参照してください。

まとめ

目的 使うパターン
固定値との比較 定数パターン 0, 1, 2 => ...
型判定+キャスト 宣言パターン is Person p
範囲チェック 関係パターン >= 0 and < 100
オブジェクトの中身で分岐 プロパティパターン { Age: >= 18 }
タプル・record の分解 位置パターン (0, var y)
配列・リストの並び List パターン [1, .., 9]
複数条件の組み合わせ 論理パターン int and > 0
パターンで表せない条件 when when a < b
全ケース網羅の保証 破棄パターン+throw _ => throw ...

関連する文法機能は以下を参照してください。if文・switch文完全ガイドで分岐処理の基礎、record型完全ガイドで判別共用体の実装、例外フィルター(when句)完全ガイドで例外処理との連携、Nullable型完全ガイドで null パターンの活用を解説しています。