複雑なif/else文や型チェックでコードが乱雑になっていませんか。C#にはpattern matchingと呼ばれる強力な機能があり、型やプロパティ、タプル、リストなど様々な条件に対して簡潔に条件分岐を記述できます。可読性を保ちつつ、ミスを減らし、保守性の高いコードを書くためのヒントと具体例を全部まとめました。最新情報を含めて丁寧に解説します。
目次
C# pattern matching 使い方 の基本概念
C#のpattern matchingは「型」「定数」「範囲」「プロパティ」「リスト」のようなパターンを用いて、対象の式がどのような形・値・状態であるかを判断し、そのパターンに一致した場合に処理を行う構文です。可読性や保守性が向上し、キャストやnullチェックなどの冗長なコードを削減できます。なお、最新情報に沿った内容を含めて説明します。
型パターン(type pattern)と宣言パターン(declaration pattern)
型パターンを使用すると、あるオブジェクトが特定の型であるかをチェックし、その型として利用できるように変数に代入できます。宣言パターンでは、is演算子と共に型をチェックしつつ変数宣言を同時に行えます。これによりキャスト不要で型安全な処理が可能になります。
例えばオブジェクトが文字列であれば小文字化して出力する、あるいはインターフェースを実装しているかで処理を切り替えるなどが可能です。nullチェックを含めた判定も含み、参照型やnullable型の扱いが明確になります。
定数パターンと関係パターン
定数パターンでは対象の値が特定の定数と等しいかどうかを判定します。関係パターン(relational pattern)では大小比較(<、>、=)を使って範囲を指定できます。さらに論理パターン(and/or/not)を組み合わせることで、複雑な条件を明瞭に表現することが可能です。
たとえば温度を判定して「氷」「水」「蒸気」などの状態を返す処理、あるいは数値の範囲と型を組み合わせて複雑な分類をする処理などで使われます。コードの分岐が明瞭になり、複数のif文を重ねるよりも読みやすくなります。
switch式・switch文とwhenガード
switch文とswitch式ではpattern matchingを使って対象を複数のパターンに一括で判定できます。switch式は値を返す形式で書けるため簡潔かつ表現力が高いです。さらにwhenキーワードを用いたガード句でパターンに追加の条件を付けられるため、単純なパターンだけでは足りない条件を補えます。
例えば数値が負、ゼロ、正のさらに偶数奇数の判定、あるいは文字列の型パターンに文字列内容の条件を付けるなど、「型+内容」の複合判定が一つのswitch式で行えるようになります。最新のC#ではこうした機能が充実しており、ガードによってコードの意図が明確になります。
応用: プロパティパターン・位置パターン・リストパターンの活用
基本を押さえたら、次はネストしたプロパティ、位置デコンストラクション、可変長リストなどを扱うパターンで応用力を高めます。これらは構造が深い型やコレクションを扱う際に非常に便利で、複雑なデータ条件分岐をスッキリ書けるようにします。
プロパティパターンと拡張プロパティパターン
プロパティパターンは型のプロパティの値に対してパターンを当てるものです。ネストしたオブジェクトのプロパティをチェックすることもできます。最新では拡張プロパティパターンが導入され、子プロパティのネストをドットで簡潔に表現できるようになりました。
例えば人物オブジェクトの住所(Address)オブジェクトの都市(City)がある値かを判定する時、ネスト構造をそのまま `{ Address.City: “Tokyo” }` のように書け、可読性が向上します。冗長なネスト構文を省略できるのが利点です。
位置パターン(positional pattern)とタプル・Deconstructメソッド
位置パターンはタプルやDeconstructメソッドを持つ型で、要素の順序でパターンを分解・判定できる構文です。座標点や複数値からなる型などで使うと、コードが直感的になります。レコード型や構造体でデフォルトで提供される例もあります。
例えば (x, y) タプルで原点かどうか、あるいは両座標が正かどうかなどを switch 式で書くことで、if文より短く明確に書けます。Deconstruct メソッドを定義しておけば、カスタム型でも同様に位置パターンを利用できます。
リストパターンとスライスパターン
C# 11で導入されたリストパターンは配列やコレクションの形をそのままマッチさせることができます。要素数、先頭や末尾の値、途中を無視するスライスパターンなどを使い、動的長のシーケンス条件を扱うことが可能です。
例えば配列の先頭と末尾を取り出したい場合や、長さが3以上であることを前提として特定の位置の要素だけチェックするなどの用途で使われます。スライスパターンを使えば中間の要素を省略でき、パターンの表現力が大幅に向上しました。
パターンマッチングのバージョンごとの進化と制約
C#はバージョンに応じてpattern matchingが徐々に強化されています。使用できるパターンや構文が異なるため、自分のターゲットとする.NETやC#のバージョンを確認することが大切です。また制約もいくつかあり、誤用を避ける知識も含めて理解しましょう。
C# 7, 8, 9, 10, 11での強化点
C# 7.0で型パターン・定数パターン・varパターンなどが導入されました。C# 8.0でスイッチ式・プロパティパターン・位置パターンなどが加わり、入れ子構造のネストパターンが使いやすくなりました。C# 9で関係パターン(relational pattern)と論理パターン(and/or/not)が強化され、複雑な条件を一つのパターンで表せるようになりました。C# 10で拡張プロパティパターンが追加され、ネストの表現が簡潔になりました。C# 11ではリストパターンとスライスパターンが登場し、可変長シーケンスに対するパターンの表現力が飛躍的に向上しました。
制約と注意点
パターンのマッチングでは除外ケース(default や discard パターン `_`)を含めないとコンパイル時に警告が出たり、実行時例外が発生する可能性があります。またリストパターンを使う型には Count または Length プロパティ、インデクサが必要です。スライスパターンは一つしか使えずリストパターンの中のみで使う制約があります。
さらに、whenガードを使う場合ガード式の順序がマッチングのマッチ優先順に影響するため、意図しないパターンが先にマッチしてしまうことがあります。プロパティのネストやnullチェックを含めたパターンを書く場合、可読性を損なわないよう注意が必要です。
具体例で学ぶ pattern matching を実践的に使う方法
ここでは基本から応用まで、実践的なコード例を交えて pattern matching の使い方を見ていきます。実際のユースケースを想定して、どのように書くと保守性と可読性が高まるかを体験してください。
例 1: 型チェックと null チェックの簡略化
以前は型と null チェックを次のように書いていたかもしれません。
if (obj != null && obj is string) { var s = (string)obj; ... }
pattern matching を使うと次のようになります:
if (obj is string s) { ... }
型チェックとキャストが一文で表され、null チェックも含まれています。
さらに null パターンと論理パターンを用いて、null ではない文字列を扱いたい場合は if (obj is not null and string s) { ... } と書くこともでき、ネストやガードで意味を明確にできます。
例 2: switch 式での複合条件分岐
コマンドや列挙値、数値範囲などを基に処理を分岐する場合、switch 式が便利です。
例えば次のようなものがあります:
command switch {
CommandType.Start => StartProcess(),
CommandType.Stop => StopProcess(),
_ => HandleUnknown() };
また when ガードを使って追加条件を付ける例:
value switch {
"負の数",
int n when n % 2 == 0 => "正で偶数",
int n => "正で奇数",
_ => "その他" };
このように、単なる型や値以外に整数の性質や文字列の内容を組み合わせて判定できます。
例 3: 配列やコレクションを使ったリストパターンの応用
例えば配列の先頭要素と最後の要素をチェックしたり、3要素以上の配列かどうかで処理を変えたりする際にリストパターンが役立ちます。中間を無視するスライスパターンを使えば、余計なコードを書かずに意図を明示できます。
例えば arr is [first, .., last] で先頭と末尾を取り出し、それ以外を捨てる。あるいは arr is [ > 0, <= 10, .. var tail ] のような複合パターンで要素の制約も加えられます。
例 4: 複雑なオブジェクトのプロパティ条件分岐
次のようなドメインモデルで住所(Address)オブジェクトに都市(City)プロパティがあり、人物(Person)オブジェクトを扱うとします。住所が東京で氏名が特定のパターンの人物のみ処理をする、などの場面でこう書けます:
if (obj is Person { Address.City: "Tokyo", Age: >= 18 })
{ ... }
ネスト構造と関係パターンを組み合わせて、複雑な条件を1行で判定できます。
可読性が高く保守もしやすいため、大規模なモデルや複雑なドメインで強みを発揮します。
パフォーマンス視点と実践での使いどころ
pattern matching は読みやすさだけでなくパフォーマンスや安全性にも影響します。実際にどこで使うと得か、また逆に使いにくい場面も理解しておくと良いでしょう。
可読性と保守性の向上
統一されたパターン記法を使うことで、分岐が一貫した構造を持ちます。型チェック・nullチェック・値比較を混在させた if 文チェーンを switch 式や pattern matching に置き換えることで、コードが短くなり、意図が明確になります。レビューの際にも条件が見落とされにくくなります。
特にドメインモデルやデータ転送オブジェクトなど、プロパティが多い型を扱うコードで効果的です。ネストプロパティやリスト構造を扱うパターンを上手く使えば、散在する条件式をまとめられます。
実行時オーバーヘッドと型チェックのコスト
パターンマッチングは内部で型チェックやプロパティやインデクサのアクセスが行われ、従来の if 文+キャストに比べてわずかなオーバーヘッドがあることがあります。しかし JIT やランタイムの最適化が進んでおり、明快な条件と簡潔な構造にする方が全体のパフォーマンスに悪影響を与えることは少ないです。
とはいえ、非常に頻繁に呼び出される処理やホットパスではパターンの深さやネスト度に注意が必要です。プロパティにアクセスする部分が多くなると null 値チェックなどの追加コードが入るため、これらも考慮して設計することが望ましいです。
互換性とバージョン依存性
pattern matching の機能は C# のバージョンによって異なります。自分のプロジェクトがターゲットとしている .NET および C# のバージョンを確認し、導入可能なパターンのみを使うこと。古いバージョンでは使えない構文を使うとビルドエラーになります。
またチームでコードを書く場合、どのバージョンまで対応するかを統一し、コードスタイルガイドにパターンマッチングの使用ポリシーを含めると良いでしょう。
C# pattern matching 使い方 を身につけるステップとヒント
初めて pattern matching を使う人、既に使っているが応用力を高めたい人向けに、学習ステップや実践時のヒントを紹介します。少しずつ導入することでコードの品質を高めていけます。
ステップ 1: 基本から始める
まずは `is 型 変数` や switch 式の基本的な定数パターン・型パターンを使ってみます。小さな関数の中で null チェックや列挙型の分岐に置き換えてみることで、効果を実感できるでしょう。
ステップ 2: ネストパターンやプロパティパターンの導入
型の中にオブジェクトをプロパティとして持つ構造を扱うなら、プロパティパターンや拡張プロパティパターンを試します。ネストするプロパティのチェックを簡潔に書くことで可読性がぐっと向上します。
ステップ 3: リストパターンやスライスパターンの応用
配列やコレクションを引数に取る関数などで条件分岐をする場合、リストパターンやスライスパターンを使って「最初の要素」「末尾の要素」「途中を無視して扱う部分」を簡潔に表現します。パフォーマンスを観察しながら導入するのがおすすめです。
ヒント: コードスタイルとガードの順序を意識する
pattern matching を使うときはパターンの優先順や when ガードの順序が実行結果に影響します。意図しないパターンが先にマッチしてしまうことがあるため、値の範囲や型の広さなどを先に限定的なパターンにし、最後に汎用的な discard パターン `_` を配置するようにします。
まとめ
C# pattern matching を使いこなすと、従来の if/else やキャストによる冗長なコードが整理され、型安全性と可読性が高まります。定数・型・関係・論理パターン、プロパティ・位置・リストパターンなど様々なパターンを組み合わせることで、複雑な条件分岐も明確かつ簡潔に表現可能です。
またガード句や拡張プロパティパターン、スライスパターンなど最新の機能を活用すれば、コードの量を減らしつつ意図をもっと正確に伝えられます。まずは基本から、バージョン対応を意識しながら少しずつ導入してみてください。
コメント