⚔️ C#クエスト ― パターンマッチングの謎解き 🐉
C# のコードを読んでいると、
ときどき――
まるで 洞窟の奥に仕掛けられた謎 のような分岐に出会う。
if が何層にも重なり、
switch の通路が枝分かれし、
「どこを通れば正解なのか?」を確かめるたびに
何度も戻って読み直すことになる。
一つひとつの条件は単純なのに、
組み合わさると “洞窟の罠”のように先が読めなくなる。
しかし――
この洞窟には、隠された“解き方”がある。
それが パターンマッチング。
型・値・構造をそのまま読み取り、
分岐を“パターン”という 仕掛けの図面 に変えてくれる。
この記事では旅人とともに、
洞窟の奥に待ち受ける謎をひとつずつ解き明かし、
パターンマッチングがどのように
コードの“暗がり”を照らす灯りになるのかを探っていく。
第0章 プロローグ:洞窟の入り口に立つ旅人
旅人は、長い開発の道のりのなかで、
どうしても避けて通れない場所にたどり着いた。
目の前には、ぽっかりと口を開けた 洞窟。
中は暗く、奥まで見通せない。
しかし、そこから吹き出す風には、
どこか懐かしい――そして厄介な気配があった。
洞窟の入口には、
if と switch が雑然と書き込まれた古い石版が並んでいる。
どれも過去に誰かが残した“試行錯誤の跡”のようだ。
分岐が増えるたび、石版は重なり、
一目では意味が分からない。
旅人は、無意識のうちに息を飲んだ。
「この奥には、もっと複雑な“仕掛け”が待っているのだろうか…」
「本当に進んでいいのか?」
何度も振り返りそうになった。
ここから先は、今までのように
条件をただ積み上げるやり方では通用しない気がした。
しかし――
洞窟の奥から、かすかな光が揺れているのが見えた。
それは、ただの光ではなかった。
“謎を解くためのヒント” のように見えた。
旅人はそっと、手を伸ばす。
洞窟の入口を照らすランタンを持ち直し、
深く、ひとつ呼吸をした。
「戻るのは簡単だ。でも、進まなければ何も変わらない。」
次の瞬間、旅人は静かに、しかし力強く
洞窟の暗がりへ向かって一歩を踏み出した。
第1章 switch 式 ― 最初の灯り
洞窟に足を踏み入れると、
最初に現れたのは見慣れた石版だった。
そこには、かつて旅人が何度も書いてきた
おなじみの if と switch が刻まれている。
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 には、次のような良さがある。
- 「入力 → 出力」のペアが上下に揃う
- 不要な
break、returnのノイズが減る - まとめて眺めるだけで 抜けや重複 に気づきやすい
洞窟の壁に貼られた古い地図と、
新しく描かれたシンプルな地図を見比べるようなものだ。
どちらも目的地にはたどり着ける。
しかし、どちらが「一目で分かるか」 は明らかだった。
旅人はランタンを掲げながら、
新たな 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 Typeやint 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段以上ネストしているところ -
ifとelse 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