パターンマッチング関連の新機能
C# 11に至るまでに、パターンマッチングについてさまざまな修正が入りました。本節では、この主なものについて順番に紹介していきます。
パターンマッチングの基本
C# 6までは、switch文やis演算子では、それぞれ定数と型の判定のみが可能でした。これは、C言語をはじめとする多くのプログラミング言語と共通の振る舞いです。C# 7.0以降では、一致対象がパターンとして拡張され、定数や型はもちろんのこと、マッチングする対象を組み合わせて指定できるほか、パターンからパターンを参照する、すなわち再帰的なパターンも指定できるようになっています。これにより、従来ならif文を使って多重分岐で記述していたような処理を、switch文(とswitch式)、is演算子のみで記述できる範囲が広がりました。
パターンマッチングは、C# 7.0以降で以下のように拡張されています。
バージョン | 種類 | 概要 |
---|---|---|
7 | 型(宣言)パターン | 型の判定(int i、string sなど) |
定数パターン | 定数値との比較(100、nullなど) | |
varパターン | 全ての値にマッチして変数にセット(var xなど) | |
8 | 破棄パターン | 全ての値にマッチして破棄(_) |
位置指定パターン | 特定の位置にマッチ(半角パーレン( … )) | |
プロパティパターン | プロパティにマッチ(半角中かっこ{ … }) | |
9 | 論理パターン | 複数のパターンをandやorで結合 |
リレーショナルパターン | 数値の範囲を「<」や「>」で指定 | |
11 | リストパターン | 配列やリストへのマッチング |
このうち型パターンは宣言パターンとも呼ばれ、is演算子とともに使われることが多いパターンです。使い方は、以下のように従来どおりis演算子のオペランドにパターンを指定します。
int ivalue = 100; // 「is int」と書けるが宣言パターンでは「int is_i」のis_iを参照可能になる if (ivalue is int is_i) { Console.WriteLine($"Type Pattern: ivalue is integer {is_i}"); } // 実行結果:Type Pattern: ivalue is integer 100
この他、全てのパターンの例は紹介できませんが、いくつかのパターンについては、次項のswitch文とswitch式の例を通じて紹介していきます。最後のリストパターンはC# 11で実装された機能なので、以降で項を設けて紹介します。
switch文とswitch式について
switch文は、C言語におけるそれとほとんど同一の働きを持った制御構文です。指定された変数の値が、caseラベルに一致すればそこに記述されている文が実行されます。処理を分ける基準が単なるリテラルとの一致といった場合には、if文による多重分岐より記述がシンプルになるというメリットがあります。
ただし、caseラベルによって対応する値を取得すればよいだけとかいう場合には、switch文は必要な処理以外に記述しなければならないもの(caseやbreakなど)が多く、記述が冗長になるというデメリットも生じます。
そのため、C# 8.0ではswitch式が導入されました。「式」とあるように、switch式は値を返します。副作用としての文を実行することはできますが、基本的に値を返すのがswitch式の役割です。switch式にはcaseラベルはありません。caseラベルに相当する値の対応はアームと呼ばれます。他言語では、match式とも呼ばれることもあります。
以下は、switch式でリレーショナルパターンを論理パターンで結合して、点数から評価を求める例です。最後の「_」はdefaultラベルに相当するものですが、ここでは破棄パターンとなっており、何にでもマッチするパターンとなります。
var point = 70; var rank = point switch { >= 90 => "S", (< 90) and (>= 80) => "A", (< 80) and (>= 70) => "B", (< 70) and (>= 60) => "C", _ => "F" }; Console.WriteLine($"Your point is {point}, rank is {rank}"); // 実行結果:Your point is 70, rank is B
Span<T>構造体とReadOnlySpan<T>構造体
Span<T>構造体については、第2回で簡単に紹介しました。Span<T>構造体はC# 7.0で実装された構造体で、メモリ上の連続した領域を表現するために利用されます。特に、パフォーマンスが要求されるような局面で、配列や文字列の一部を取得したり、書き換えたりする目的で用いられます。ReadOnlySpan<T>構造体は、その名の通りSpan<T>構造体の読み取り専用版です。
次項で、Span<char>に対する文字列定数でのパターンマッチングについて紹介しますが、これはSpan<T>が文字列操作に使われることが多くなってきたことからサポートされた機能です。例えば、文字列から部分文字列を取得する場合にはSubstring関数を使いますが、このときに部分文字列のコピーが作成されるため、部分文字列を参照するだけといった場合にはやや非効率なものになってしまいます。このときにSpan<char>を使うと、コピーを伴わない部分文字列の参照が可能です。
以下は、文字列の部分文字列をSubstring関数とReadOnlySpan<char>の双方で取得し、表示する例です。
string s = "Hello, world!!"; Console.WriteLine($"Substring of s: {s.Substring(7)}"); ReadOnlySpan<char> ros = s.AsSpan().Slice(7); Console.WriteLine($"Span of s: {ros}");
ReadOnlySpan<char>のインスタンス生成では、文字列sに対してAsSpanメソッドを実行して文字列をSpanとして取得し、そのSliceメソッドで部分的な領域を取得しています。このとき文字列のコピーは発生しないので、効率のよい参照となってます。
Span<char>が文字列定数でパターンマッチング可能に(Pattern match Span<char> on a constant string)
C# 11では、Span
前述の通り、Span<char>(ReadOnlySpan<char>も含む)はchar型の連続した領域を表した型であるので、それに対してパターンを指定するにはchar型の領域を記述する必要がありました。
例えば以下は、コンソールからの入力をstringではなくReadOnlySpan<char>で受けて、それに対してパターンマッチングを試みる例です(.NET 6(C# 10)で動作するコードです)。パターンマッチングしたい文字列定数をいったんAsSpanメソッドでSpan<char>に変換し、その上でSequenceEqualメソッドで一致の判定を行っており、かなり冗長です。
Console.Write("Place: "); ReadOnlySpan<char> str = Console.ReadLine(); var year = str switch { var tokyo when tokyo.SequenceEqual("Tokyo".AsSpan()) => 2020, var rio when rio.SequenceEqual("Rio".AsSpan()) => 2016, var london when london.SequenceEqual("London".AsSpan()) => 2012, var beijing when beijing.SequenceEqual("Beijing".AsSpan()) => 2008, _ => 0 }; if (year != 0) { Console.WriteLine($"{str} Olympic held on {year}"); }
C# 11では、Span<char>に対してのパターンに文字列定数を直接記述できるので、シンプルになります。文字列をSpan<char>(ReadOnlySpan<char>)で処理することが増えてきたことから、特別扱いのような形で実装されたようです。
以下は、上記の例を文字列による定数パターンで書き直したものです。
Console.Write("Place: "); ReadOnlySpan<char> str = Console.ReadLine(); var year = str switch { "Tokyo" => 2020, "Rio" => 2016, "London" => 2012, "Beijing" => 2008, _ => 0 }; if (year != 0) { Console.WriteLine($"{str} Olympic held on {year}"); }
実行結果は下記です。
Place: Tokyo Tokyo Olympic held on 2020
リストパターン(List patterns)
C# 11では、[]を使ったリストによるパターンの指定が可能になりました。
リストパターンは、配列やリスト(List<T>など)に対してのパターンマッチングを可能にする機能です。[]の中に、マッチさせたい要素(パターン)を列挙することで、例えば配列がそのような要素を持つのか、判定することができます。
以下は、リストパターンの例です。
var a = new[] {1, 2, 3, 4, 5}; Console.WriteLine(a is [1, 2, 3, 4, 5]); // (1)True Console.WriteLine(a is [1, 2, _, _, _]); // (2)True Console.WriteLine(a is []); // (3)False Console.WriteLine(a is [1, 2, 3]); // (4)False Console.WriteLine(a is [1, 2, 3, ..]); // (5)True Console.WriteLine(a is [1, .., 5]); // (6)True
[]の中に記述できるのはパターンであるので、「_」を記述した場合には任意の値となります。また、(1)でマッチして、(3)(4)でマッチしないことからわかりますが、この場合は配列の要素数とパターン数が一致しないとマッチしたことになりません。このため、(5)のようなスライスパターン(..)が用意されています。スライスパターンを使うと、その部分には要素があってもなくてもよい、というリストパターンを指定できます。スライスパターンは、(5)のように末尾、(6)のように途中に記述できるほか、先頭にも記述できます。