C#はバージョンアップを重ねるごとに、データ構造をより直感的に扱うための機能を拡充してきました。
その中でも、C#11で導入された「リストパターン」は、配列やリストといったコレクションの要素を驚くほど簡潔に検証・抽出できる強力な機能です。
これまではループ処理やインデックス指定を駆使して記述していた複雑な条件分岐も、リストパターンを使えば宣言的なコードで美しく記述することが可能になります。
本記事では、リストパターンの基礎から応用、そして実戦で役立つテクニックまでを詳しく解説します。
リストパターンとは何か
リストパターンは、switch式やis式の中で、配列やリストの「形」を直接指定してマッチングを行う機能です。
特定の要素が含まれているか、特定の順番で並んでいるか、あるいは特定の長さであるかといった条件を、直感的な構文で表現できます。

リストパターンが登場する前は、配列の要素を確認するために「長さが3以上か?」「0番目の要素は何か?」「2番目の要素は何か?」といった条件を論理演算子で繋いで記述する必要がありました。
リストパターンを使えば、これらの条件を「データの見た目そのまま」に記述できるため、コードの可読性が飛躍的に向上します。
基本的な構文とマッチングの仕組み
リストパターンの基本は、角括弧[]の中に要素を記述することです。
もっともシンプルな例として、完全一致のパターンを見てみましょう。
using System;
int[] numbers = { 1, 2, 3 };
// リストパターンによる完全一致の判定
if (numbers is [1, 2, 3])
{
Console.WriteLine("配列は [1, 2, 3] と完全に一致しました。");
}
else
{
Console.WriteLine("一致しませんでした。");
}
配列は [1, 2, 3] と完全に一致しました。
この例では、numbersが3つの要素を持ち、かつそれらが順番に1, 2, 3である場合にのみtrueとなります。
もし要素数が異なったり、順番が違ったりすればマッチしません。
これがリストパターンの最も基本的な形です。
リストパターンで利用できる主要な要素
リストパターンを真に使いこなすためには、単なる完全一致だけでなく、「ワイルドカード」や「範囲」を指定する特殊なパターンを理解する必要があります。
任意の1要素にマッチする「破棄パターン」
特定のインデックスにある値は何でも良いが、その場所に要素が存在することだけを確認したい場合は、アンダースコア_を使用する「破棄パターン」を利用します。

using System;
int[] scores = { 100, 85, 90 };
// 2番目の要素は何でも良く、1番目が100、3番目が90であることを確認
if (scores is [100, _, 90])
{
Console.WriteLine("条件にマッチしました。");
}
条件にマッチしました。
この場合、scores[1]がどんな値であっても、要素数が3で最初と最後が一致していれば条件を満たします。
0個以上の要素にマッチする「スライスパターン」
リストパターンの目玉機能の一つが、ドット2つ..で表される「スライスパターン」です。
これは「0個以上の任意の要素」にマッチします。
これを使うことで、配列の長さを固定せずに柔軟なマッチングが可能になります。

using System;
void CheckPattern(int[] vals)
{
string result = vals switch
{
[1, ..] => "1から始まるリスト",
[.., 99] => "99で終わるリスト",
[1, .., 99] => "1で始まり99で終わるリスト",
[] => "空のリスト",
_ => "その他のリスト"
};
Console.WriteLine($"{string.Join(",", vals)} : {result}");
}
CheckPattern(new[] { 1, 2, 3 });
CheckPattern(new[] { 5, 99 });
CheckPattern(new[] { 1, 50, 99 });
CheckPattern(new int[] { });
1,2,3 : 1から始まるリスト
5,99 : 99で終わるリスト
1,50,99 : 1で始まり99で終わるリスト
: 空のリスト
スライスパターンは1つのリストパターンの中で一度だけ使用できます。
これにより、「最初と最後だけ決まっていて、間は何個あっても良い」というパターンを極めてシンプルに表現できます。
変数へのキャプチャと再帰的なパターン
リストパターンは、単に値を検証するだけでなく、マッチした要素を変数として取り出す(キャプチャする)ことも可能です。
これにより、データの検証と抽出を一気に行えます。
varパターンによる要素の抽出
varを使用することで、特定の場所にある要素を変数に代入できます。
using System;
string[] path = { "home", "user", "documents", "data.txt" };
if (path is ["home", var userName, .. var rest])
{
Console.WriteLine($"ユーザー名: {userName}");
Console.WriteLine($"残りのパスの階層数: {rest.Length}");
}
ユーザー名: user
残りのパスの階層数: 2
注目すべきは、.. var rest という記述です。
スライスパターンに対しても変数を割り当てることができ、この場合、マッチした部分範囲が新しいコレクションとしてrestに格納されます。
リスト内での論理パターンの活用
リストパターンの各要素の位置には、定数だけでなく、比較演算子や論理演算子(and, or, not)を組み合わせた「パターン」を記述できます。

using System;
int[] data = { 10, 150, 0 };
if (data is [> 0, > 100, <= 0])
{
Console.WriteLine("「正の数、100超、0以下」の条件に一致しました。");
}
「正の数、100超、0以下」の条件に一致しました。
このように、「配列の構造」と「各要素の条件」を1つの式で完結させられるのが、C#11のリストパターンの真骨頂です。
実戦的な活用シーン
リストパターンは、実際のアプリケーション開発においてどのような場面で役立つのでしょうか。
代表的な2つの例を紹介します。
1. コマンドライン引数の解析
コマンドライン引数は文字列の配列として渡されますが、その解析は条件分岐が複雑になりがちです。
リストパターンを使えば、コマンドの構造をそのまま表現できます。
using System;
void ProcessCommand(string[] args)
{
switch (args)
{
case ["start", var target]:
Console.WriteLine($"{target} を起動します。");
break;
case ["stop", var target, "--force" or "-f"]:
Console.WriteLine($"{target} を強制停止します。");
break;
case ["copy", var source, var dest]:
Console.WriteLine($"{source} から {dest} へコピーします。");
break;
default:
Console.WriteLine("不明なコマンドです。");
break;
}
}
ProcessCommand(new[] { "start", "WebServer" });
ProcessCommand(new[] { "stop", "DBServer", "-f" });
WebServer を起動します。
DBServer を強制停止します。
--force or -fのように、特定の要素に複数の候補を持たせる記述もリストパターン内で行えるため、非常にスマートです。
2. CSVや構造化テキストのパース
カンマ区切りなどで構成されたデータをSplitメソッドで分解した後、その内容を検証する際にも威力を発揮します。

using System;
void ImportUser(string csvLine)
{
string[] parts = csvLine.Split(',');
if (parts is [var idStr, var name, "Admin" or "User"])
{
if (int.TryParse(idStr, out int id))
{
Console.WriteLine($"インポート成功: ID={id}, 名前={name}, 権限={parts[2]}");
}
}
else
{
Console.WriteLine("無効なフォーマットです。");
}
}
ImportUser("101,Tanaka,Admin");
ImportUser("invalid,Data");
インポート成功: ID=101, 名前=Tanaka, 権限=Admin
無効なフォーマットです。
パフォーマンスと注意点
リストパターンは非常に便利ですが、内部的な動作を理解しておくことも重要です。
パフォーマンスへの影響
リストパターンは、基本的にはコンパイラによって高効率なif文やプロパティ参照(Lengthやインデクサ)に展開されます。
そのため、手動で同じチェックを書く場合と比べて、実行時のオーバーヘッドはほとんどありません。
ただし、スライスパターンを使用して変数にキャプチャする場合(例:.. var rest)、マッチした部分のコピーが作成される可能性がある点には注意が必要です。
| 特徴 | 詳細 |
|---|---|
| 評価順序 | 左の要素から順番に評価されます。 |
| 型の要件 | Length または Count プロパティを持ち、整数インデクサを持つ型であれば利用可能です。 |
| Spanのサポート | Span<T> や ReadOnlySpan<T> にも適用でき、この場合はスライス時にヒープ割り当てが発生しません。 |
リストパターンが使えないケース
リストパターンは、あくまで「順序のあるコレクション」を対象としています。
そのため、IEnumerable<T>を直接ターゲットにすることはできません(列挙しないと長さが確定しないため)。
リストパターンを適用したい場合は、ToArray() や ToList() で実体化しておく必要があります。
まとめ
C#11で導入されたリストパターンは、コレクション操作におけるコードの抽象度を一段階引き上げる画期的な機能です。
これまでは命令的なコード(どのようにチェックするか)を記述していましたが、リストパターンによって宣言的なコード(どのような形であるべきか)を記述できるようになりました。
特に..によるスライスや、varによる抽出、論理パターンの組み合わせは、データ解析や複雑な条件分岐が伴うロジックを劇的にスリム化してくれます。
配列やリストを扱う際は、まず「この処理はリストパターンで書けないか?」と検討してみてください。
きっと、より保守性が高く読みやすいコードへと進化させることができるはずです。
最新のC#の機能を積極的に取り入れ、モダンな開発スタイルを身につけていきましょう。
