⚔️

⚔️ C#クエスト ― パターンマッチングの謎解き 🐉

に公開

C# のコードを読んでいると、
ときどき――
まるで 洞窟の奥に仕掛けられた謎 のような分岐に出会う。

if が何層にも重なり、
switch の通路が枝分かれし、
「どこを通れば正解なのか?」を確かめるたびに
何度も戻って読み直すことになる。

一つひとつの条件は単純なのに、
組み合わさると “洞窟の罠”のように先が読めなくなる

しかし――
この洞窟には、隠された“解き方”がある。

それが パターンマッチング

型・値・構造をそのまま読み取り、
分岐を“パターン”という 仕掛けの図面 に変えてくれる。

この記事では旅人とともに、
洞窟の奥に待ち受ける謎をひとつずつ解き明かし、
パターンマッチングがどのように
コードの“暗がり”を照らす灯りになるのかを探っていく。


第0章 プロローグ:洞窟の入り口に立つ旅人

旅人は、長い開発の道のりのなかで、
どうしても避けて通れない場所にたどり着いた。

目の前には、ぽっかりと口を開けた 洞窟
中は暗く、奥まで見通せない。
しかし、そこから吹き出す風には、
どこか懐かしい――そして厄介な気配があった。

洞窟の入口には、
ifswitch が雑然と書き込まれた古い石版が並んでいる。
どれも過去に誰かが残した“試行錯誤の跡”のようだ。
分岐が増えるたび、石版は重なり、
一目では意味が分からない。

旅人は、無意識のうちに息を飲んだ。

「この奥には、もっと複雑な“仕掛け”が待っているのだろうか…」
「本当に進んでいいのか?」

何度も振り返りそうになった。
ここから先は、今までのように
条件をただ積み上げるやり方では通用しない気がした。

しかし――
洞窟の奥から、かすかな光が揺れているのが見えた。
それは、ただの光ではなかった。
“謎を解くためのヒント” のように見えた。

旅人はそっと、手を伸ばす。
洞窟の入口を照らすランタンを持ち直し、
深く、ひとつ呼吸をした。

「戻るのは簡単だ。でも、進まなければ何も変わらない。」

次の瞬間、旅人は静かに、しかし力強く
洞窟の暗がりへ向かって一歩を踏み出した。


第1章 switch 式 ― 最初の灯り

洞窟に足を踏み入れると、
最初に現れたのは見慣れた石版だった。

そこには、かつて旅人が何度も書いてきた
おなじみの ifswitch が刻まれている。

string GetLabel(int value)
{
    if (value == 0) return "Zero";
    if (value == 1) return "One";
    if (value == 2) return "Two";
    return "Other";
}

やりたいことは単純なのに、
条件が増えると行も増え、
「抜けや重複がないか」を目で追い続けるしかない。

その隣には、古いスタイルの switch もある。

string GetLabel(int value)
{
    switch (value)
    {
        case 0:
            return "Zero";
        case 1:
            return "One";
        case 2:
            return "Two";
        default:
            return "Other";
    }
}

こちらのほうがまだ読みやすい。
ただし、return の連打や break の配置ミスなど、
別の罠も潜んでいる。


1.1 「式」としての switch との出会い

洞窟の奥に、別の石版が光っている。
そこには見慣れない形の文字が刻まれていた。

string GetLabel(int value) =>
    value switch
    {
        0 => "Zero",
        1 => "One",
        2 => "Two",
        _ => "Other"
    };

旅人は目を凝らす。

  • switch式(expression) として使われている
  • ケースごとに => で値を返している
  • 全体がひとつの “対応表” のように見える

今までの switch と違い、
「この入力にはこの結果」という対応だけが並んでいる

「ああ、これは“分岐のリスト”ではなく“マッピング”なんだ。」

旅人は直感的にそう理解した。


1.2 対応表という“地図”を見る感覚

式の switch には、次のような良さがある。

  • 「入力 → 出力」のペアが上下に揃う
  • 不要な breakreturn のノイズが減る
  • まとめて眺めるだけで 抜けや重複 に気づきやすい

洞窟の壁に貼られた古い地図と、
新しく描かれたシンプルな地図を見比べるようなものだ。

どちらも目的地にはたどり着ける。
しかし、どちらが「一目で分かるか」 は明らかだった。

旅人はランタンを掲げながら、
新たな switch 式を「洞窟の最初の灯り」として心に刻んだ。


第2章 プロパティパターン ― 鍵の形を読む

洞窟を進むと、
通路の途中にいくつもの “鍵穴” のような模様が現れた。

そこには、単に値だけでなく、
オブジェクトの状態やプロパティの組み合わせ が刻まれている。

旅人はかつて、こんなコードを書いていたことを思い出す。

string GetStatus(Order order)
{
    if (order == null) return "Unknown";

    if (order.State == State.Pending && order.IsPriority)
        return "Pending(Priority)";

    if (order.State == State.Pending)
        return "Pending";

    if (order.State == State.Shipped)
        return "Shipped";

    return "Other";
}

条件が増えるほど、
どの組み合わせがどこで扱われているか分かりにくくなる


2.1 when ガードで「条件」を分離する

洞窟の壁に、少し違った形の switch が刻まれていた。

string GetStatus(Order order) =>
    order switch
    {
        null => "Unknown",
        { State: State.Pending, IsPriority: true } => "Pending(Priority)",
        { State: State.Pending } => "Pending",
        { State: State.Shipped } => "Shipped",
        _ => "Other"
    };

ここでは、
オブジェクト全体 order に対して switch し、
パターンとしてプロパティの組み合わせ を書いている。

さらに、when ガードを使えば、
「基本の形」と「例外的な条件」を切り分けられる。

string GetStatus(Order order) =>
    order switch
    {
        null => "Unknown",
        { State: State.Pending } o when o.IsPriority => "Pending(Priority)",
        { State: State.Pending } => "Pending",
        { State: State.Shipped } => "Shipped",
        _ => "Other"
    };
  • プロパティパターン で「どんな形の order か」を書き
  • when ガード で「その中で、さらにどんな条件か」を書く

こうして、
if に埋もれていた条件の組み合わせは、
“鍵の形” として洞窟の壁に整理されていく。


2.2 「構造」で読むと見えてくるもの

プロパティパターンを使うと、コードはこう変わる。

  • 「State と IsPriority の組み合わせ」という 構造そのもの を見る
  • 条件の組み合わせ漏れがあれば、目で見て気づきやすい
  • モデルの設計(enum や bool の持ち方)を見直すきっかけにもなる

旅人は気づく。

「いままで、自分は“条件式”しか見ていなかった。
でも本当に大事なのは“どんな状態の組み合わせが存在するか”だったんだ。」

洞窟の壁に並ぶパターンは、
単なるコードではなく、
ドメインの状態一覧表 に見え始めていた。


第3章 タプルと関係パターン ― 迷路を地図に

さらに奥へ進むと、
通路が十字路になっている場所に出た。

床には (A, B) のような刻印があり、
その周囲には < 0, >= 0 などの記号が散りばめられている。

複数の条件が絡み合う “分岐の迷路” を、
座標として扱う仕掛け だ。


3.1 2つの条件を「座標」にするタプルパターン

たとえば、ゲームのステータス評価を考えてみよう。

string GetRank(int hp, int mp)
{
    if (hp <= 0) return "Dead";
    if (hp < 50 && mp < 10) return "Danger";
    if (hp >= 80 && mp >= 30) return "Safe";
    return "Normal";
}

条件が2つになるだけで、
if は一気に読みにくくなる。

タプルパターンを使うと、こう書ける。

string GetRank(int hp, int mp) =>
    (hp, mp) switch
    {
        (<= 0, _) => "Dead",
        (< 50, < 10) => "Danger",
        (>= 80, >= 30) => "Safe",
        _ => "Normal"
    };

ここでは、

  • (hp, mp)座標 として扱い
  • (< 50, < 10) のように 組み合わせをひとつのパターン にしている

_ は「気にしない値」だ。
「HP が 0 以下なら MP は見ない」という意思も、
パターンを見るだけで伝わってくる。


3.2 関係パターンと位置パターンで“区間”を表す

さらに踏み込むと、
範囲や順序 を扱う場面も出てくる。

string GetTemperatureLabel(double temp) =>
    temp switch
    {
        <= 0 => "Freezing",
        > 0 and < 20 => "Cold",
        >= 20 and < 30 => "Warm",
        >= 30 => "Hot"
    };

ここで使われているのが 関係パターン
<=, <, >= などの演算子を使って、
“この区間ならこのラベル” をきれいに並べられる。

また、レコードやタプルに対して 位置パターン も使える。

public record Point(int X, int Y);

string Describe(Point p) =>
    p switch
    {
        (0, 0) => "Origin",
        (0, _) => "On Y axis",
        (_, 0) => "On X axis",
        _ => "Somewhere"
    };

コンストラクタ順の位置で
(X, Y) を読む書き方だ。


3.3 迷路から「状態表」へ

旅人は、
タプルや位置パターン・関係パターンが
迷路を「地図」に変える仕掛け だと気づく。

  • 複数の条件を「座標」として扱う
  • 区間や範囲を「行」として並べる
  • 抜けや重複があれば、地図の“穴”として見つけられる

コードはもはや、
「どこに罠があるか分からない暗い通路」ではなく、
状態と結果が整理された見取り図 になっていた。


第4章 ドメインモデルと型パターン ― 姿を変える敵

洞窟のさらに奥には、
今までと少し違うタイプの仕掛けが待っていた。

そこには、さまざまな イベント の石像が並んでいる。

  • 注文が作られた
  • 注文がキャンセルされた
  • 商品が出荷された
  • 返金が行われた

一体ずつは単純だ。
しかし、それらがバラバラなクラスやフラグで表現されると、
分岐は一気に読みにくくなる。


4.1 パターンを書きやすいモデルとは?

旅人は、かつてこんなコードを書いていた。

class Event
{
    public string Type { get; init; } = "";
    public int OrderId { get; init; }
    public decimal? Amount { get; init; }
    // ... ほかにもいろいろ
}

string Handle(Event e)
{
    if (e.Type == "OrderCreated")
    {
        // ...
    }
    else if (e.Type == "OrderCancelled")
    {
        // ...
    }
    else if (e.Type == "Refunded" && e.Amount is > 0)
    {
        // ...
    }
    // ...
    return "OK";
}

すべてを「Type + 追加情報」で持つと、
文字列やフラグの組み合わせに依存する ことになる。

パターンマッチングの真価を発揮させるには、
モデル側を少し変えてやる必要がある。


4.2 record と enum で“状態の種類”をはっきりさせる

たとえば、イベントを次のように表現してみる。

public abstract record OrderEvent;

public record OrderCreated(int OrderId) : OrderEvent;

public record OrderCancelled(int OrderId, string Reason) : OrderEvent;

public record Refunded(int OrderId, decimal Amount) : OrderEvent;

ここでは

  • 型そのもの が「イベントの種類」を表している
  • それぞれのイベントに必要な情報だけを持たせている

このモデルに対してパターンマッチングを使うと、こうなる。

string Handle(OrderEvent e) =>
    e switch
    {
        OrderCreated(var id) =>
            $"Order {id} created",

        OrderCancelled(var id, var reason) =>
            $"Order {id} cancelled: {reason}",

        Refunded(var id, var amount) when amount > 0 =>
            $"Order {id} refunded: {amount}",

        _ => "Unknown event"
    };

ここでは、

  • 型パターン でイベントの種類を分け
  • 位置パターン でプロパティを取り出し
  • when ガード で追加条件を書く

という、これまで登場したパターンが
一つの switch の中にきれいに並んでいる。


4.3 「設計」がパターンマッチングの威力を決める

旅人は理解する。

「パターンマッチングは、
モデルがきちんと“状態の種類”を表しているほど書きやすくなる。」

  • なんでもかんでも string Typeint Kind で表すと、
    パターンは文字列・数値との比較だらけになる
  • enum や record のバリエーション を使うと、
    型そのものが“状態のバリエーション”になる

パターンマッチングは、
単に if の書き方を置き換えるテクニックではない。

「状態の種類」を型として設計し、
その組み合わせをパターンで表すための道具
なのだ。

洞窟の罠は、
敵の姿(ドメインモデル)を正しく見抜いたとき、
初めて安全に通り抜けられるようになる。


第5章 仕様が見える switch ― 真実への道

洞窟の最深部には、
ひときわ大きな石版が立っていた。

そこには、
これまでの旅を凝縮したような switch が刻まれている。


5.1 仕様がそのまま並んだ switch

たとえば、配送ステータスを決めるロジックを考えてみよう。

public enum ShippingState
{
    None,
    Pending,
    Shipped,
    Delivered,
    Returned
}

string GetMessage(ShippingState state) =>
    state switch
    {
        ShippingState.None      => "Not started",
        ShippingState.Pending   => "Pending",
        ShippingState.Shipped   => "On the way",
        ShippingState.Delivered => "Delivered",
        ShippingState.Returned  => "Returned",
        _ => throw new ArgumentOutOfRangeException(nameof(state))
    };

ここには、
「この状態ならこのメッセージ」という仕様そのもの が並んでいる。

もし将来、新しい状態 Lost を追加したらどうなるか。

public enum ShippingState
{
    None,
    Pending,
    Shipped,
    Delivered,
    Returned,
    Lost   // ★ 追加
}

switch 式で全ての状態を列挙して _ を書かなければ、
不足があればコンパイラが「網羅されていないパターンがある」と警告してくれる。

一方、_ を残している場合はコンパイル時には検出されず、
実行時に ArgumentOutOfRangeException を投げる“保険”として働く。

つまり、
仕様の網羅性を重視するなら _ を書かない方が安全であり、
新しい値が enum に追加された瞬間、コンパイルエラーで気づける。

洞窟の罠に落ちる前に、
石版そのものが先に警告を発してくれる のだ。


5.2 「バグが潜む if」から「仕様が見える switch」へ

if 連打のコードは、
どこか一箇所の条件を足し忘れても気づきにくい。

  • 条件式がバラバラに散らばる
  • 順番の意味が分かりにくい
  • 「この状態のとき、どこを通るのか?」を頭の中でシミュレーションする必要がある

一方、パターンマッチングを使った switch は、
仕様そのものを“一覧”として見せてくれる。

  • 状態の種類ごとに「行」が決まる
  • 抜けている行があれば一目で分かる
  • 将来状態が増えたとき、コンパイルエラーで気づける

旅人は、
洞窟で見てきた数々の石版を思い出しながら、
静かにうなずく。

「パターンを書くということは、
仕様を“漏れなく・ダブりなく”並べていく作業なんだ。」


エピローグ いま、if を疑い、パターンを信じるか?

洞窟を抜けると、
空は少しだけ明るくなっていた。

振り返れば、
入口近くにあった if の石版たちも、
もはや以前ほど不気味には見えない。

旅人は思う。

  • すべてをパターンマッチングで書く必要はない
  • シンプルな条件なら、if のほうが軽やかで分かりやすい場面も多い
  • しかし、状態が増え、分岐が複雑になるほど、
    「仕様が見える書き方かどうか」 が重要になる

これからも、
新しい機能やフレームワークが現れるたびに、
コードは変わっていく。

そのたびに、洞窟のような暗がりが現れ、
小さな謎がいくつも積み重なるだろう。

そのとき、
旅人はきっと、こう自問する。

「ここは、本当に if で書くべきか?」
「それとも、パターンとして並べたほうが、
仕様が見えるのではないか?」

洞窟の奥で手に入れた
“パターンという灯り” は、
これから先の開発の道のりでも
静かに、しかし確かに、歩みを照らし続けてくれるはずだ。


おわりに:次に試してみてほしいこと

この記事を読み終えたら、
ぜひ自分のコードの中から、次のような場所を探してみてほしい。

  • if が3段以上ネストしているところ
  • ifelse if がずらっと並んでいるところ
  • 「状態」と「その結果」が散らばっているところ

そこに、この記事で見たような

  • switch
  • プロパティパターン
  • タプルパターン
  • 関係パターン
  • record / enum を使ったドメインモデル

のどれかを、ひとつでも 当てはめてみる。

それはきっと、
あなた自身の C# クエストにおける
新しい「一歩」になるはずだ。

気に入ったパターンがあれば、コメントに貼ってもらえると他の人の学びにもなる。


補足:C# パターンマッチング機能 ― バージョン対応表(参考)

機能 導入バージョン
型パターン (is Type x) C# 7.0
switch C# 8.0
プロパティパターン C# 8.0
タプルパターン C# 8.0
位置パターン C# 8.0
関係(比較)パターン (<, <=, >= など) C# 9.0
record 型 C# 9.0
論理パターン (and, or, not) C# 9.0

Discussion