パターンマッチング

C#7.0から、switch文switch式is演算子パターンマッチングという柔軟な条件一致の方法が可能となりました。
パターンは複数あり、これらは組み合わせて使用可能です。

C#7.0では型パターンや宣言パターンなどの基本的な機能が提供され、後のバージョンでさらに機能が追加されています。
バージョン情報が明示されていない機能についてはC#7.0で使用可能です。

定数パターン

定数パターンは、式の結果(評価値)と定数との一致を判定します。
switch文では従来通りの機能ですが、パターンマッチングの機能の一部になったので他のパターンマッチングとの組み合わせが可能になります。
is演算子では従来のswitch文と同等の判定方法が可能になります。


int n = 1;
object o = null;

if (n is 1) { }
if (o is null) { }

型パターン

型パターンは、式のデータ型と特定のデータ型との一致を判定します。
従来のis演算子の機能がswitch文(switch式)でも使用可能になります。


object obj = 1;

switch (obj)
{
    case 0: //通常の定数値との判定を混在可能
        Console.WriteLine(obj);
        break;
    case int:
        Console.WriteLine("int型");
        break;
    case string:
        Console.WriteLine("string型");
        break;
    default:
        Console.WriteLine("それ以外の型");
        break;
}
int型

なお、switch文でパターンマッチングを使用する場合、判定は上から順に行われます。
定数との一致を判定する従来のswitch文の場合、それぞれのcase句で条件が重複することはないため、case句の順序を入れ替えても結果は同じです。
パターンマッチングでは条件が重複する場合があるので、判定の順序によって結果が変わります。
上のコードは、まず「0」との一致判定を行うため、object型の中身がint型であっても中身が0の場合はcase 0:の処理が実行されます。

ちなみにswitch文の一致式にはどのような値(式、変数など)でも指定できますが、コードの文脈上そのデータ型以外はありえない場合はcase句にそのデータ型以外を書くとコンパイルエラーになります。


int n = 0;
switch (n) //int型で確定
{
    case int:
        Console.WriteLine("int型");
        break;
    case string: //コンパイルエラー
        Console.WriteLine("それ以外の型");
        break;
}

nullとの一致

型パターンは式の結果のデータ型との一致を判定するため、参照型変数の中身がnullの場合はどのデータ型とも一致しません。
(→値型と参照型)
例えばstring型変数の判定では、中身がnullの場合はstring型とは一致せずにnullと一致します。


string str = null;
switch (str)
{
    case string:
        Console.WriteLine("string型");
        break;
    case null:
        Console.WriteLine("null");
        break;
}
null

宣言パターン

データ型が一致した場合、その値を使用するにはキャストするという方法もありますが、判定と同時にキャスト済みの変数を作ることもできます。
これを宣言パターンといいます。


object obj = "abc";

switch (obj)
{
    case int n: //int型変数nにキャスト
        Console.WriteLine(n);
        break;
    case string s: //string型変数sにキャスト
        Console.WriteLine(s.Length);
        break;
    default:
        Console.WriteLine("それ以外の型");
        break;
}
3

上記コードは、object型の中身がint型の場合はn、string型の場合はsという変数がそれぞれcase句内で使用できます。
これは後述するwhen句でも使用できます。

when句

switch文、switch式では、宣言パターンによる変数はwhen句でその値からさらに細かく条件を追加できます。


object obj = 5;

switch (obj)
{
    case int n when n == 0: //int型、かつ0の場合
        Console.WriteLine("int型: 0");
        break;
    case int n: //int型、かつ「n == 0」が偽の場合
        Console.WriteLine("int型: " + n);
        break;
}
int型: 5

varパターン

特定の型ではなく、すべての型に一致するパターンとしてvarパターンがあります。
これは主にswitch文の最後に「全てのcase句に一致しなかった場合」のパターンとして使用します。


object obj = 1.0;

switch (obj)
{
    case int n:
        Console.WriteLine("int型: " + n);
        break;
    case string s:
        Console.WriteLine("string型: " + s);
        break;
    case var unknown:
        Console.WriteLine("不明な型: " + unknown.ToString());
        break;
}
不明な型: 1

varパターンは全ての型に一致するため、それ以降にcase句を書くことはできません。
(コンパイルエラー)
default句を書くことはできますが、これも一致することはなく、警告がでます。

なお、varパターンは変数の宣言が必須です。
変数が必要ない場合は後述する破棄パターンが使用できます。

破棄パターン(C#8.0)

上記のコードでは、varパターンで作られる変数はあまり使い道がありません。
varパターンでは変数宣言は省略できませんが、変数が必要ない場合はアンダーバー(_)を記述することで値を変数に受けとらないことができます。
これを破棄パターンといいます。
これはC#8.0から使用可能です。


object obj = 1.0;

switch (obj)
{
    case int n:
        Console.WriteLine("int型: " + n);
        break;
    case string s:
        Console.WriteLine("string型: " + s);
        break;
    case var _: //破棄パターン
        Console.WriteLine("不明な型");
        break;
}
不明な型

通常の変数名としての_は有効ですが、破棄パターンとしての_はこの変数は作られず、値は捨てられます。

位置パターン(C#8.0)

位置パターンは、値のリストと式の結果を分解した値との一致を判定します。


public struct Point
{
    public int x;
    public int y;
    public void Deconstruct(out int x, out int y)
        => (x, y) = (this.x, this.y);
}

static void Main(string[] args)
{
    Point p = new Point() { x = 1, y = 1 };
    switch(p)
    {
        case (0, 0):
            Console.WriteLine("0-0");
            break;
        case (0, 1):
            Console.WriteLine("0-1");
            break;
        case (1, 0):
            Console.WriteLine("1-0");
            break;
        case (1, 1):
            Console.WriteLine("1-1");
            break;
    }
}
1-1

位置パターンの書き方はタプルリテラルとほぼ同じです。
ただしパターンに組み込めるのは定数のみで、変数は使用できません。

値の分解もタプルの分解と同じで、Deconstructメソッドが使用されます。

値の判定の前に型の判定を挟むこともできます。


public struct Point { /*省略*/ }

static void Main(string[] args)
{
    object[] objs = new object[]
    {
        new Point() { x = 1, y = 1 },
        (1, 1) //タプル
    };
    foreach(object o in objs)
    {
        //先にPoint型かどうかをチェック
        Console.WriteLine(o is Point(1, 1));
    }
}
True
False

各要素の判定には他のパターンを組み込めます。


public struct Point { /*省略*/ }

static void Main(string[] args)
{
    Point p = new Point() { x = 1, y = 2 };
    Console.WriteLine(p is (1, _));
    //True

    Console.WriteLine(p is (_, var a) && a % 2 == 0);
    //True
    //xは何でもOK(破棄)
    //yは変数aに格納し、2で割った余りが0かを判定
}
True
True

プロパティパターン(C#8.0)

プロパティパターンは、プロパティのリストと式が持つプロパティ(またはフィールド)との一致を判定します。


public struct Point
{
    public int x;
    public int y;
}

static void Main(string[] args)
{
    Point p = new Point() { x = 0, y = 1 };
    Console.WriteLine(p is { x: 0 });
    Console.WriteLine(p is { y: 1 });
    Console.WriteLine(p is { x: 0, y: 1 });
}
True
True
True

位置パターンに似ていますが、プロパティ名で指定されるプロパティ(またはフィールド)との一致を判定します。
必要のないプロパティは除外できますし、値の分解は行いません。

なお、プロパティパターンという名前ですがフィールドにも適用できます。

位置パターンと同じように、値の判定の前に型の判定を挟むことができます。


public struct Point
{
    public int x;
    public int y;
}
public struct Point3D
{
    public int x;
    public int y;
    public int z;
}

static void Main(string[] args)
{
    object p = new Point() { x = 0, y = 0 };
    Console.WriteLine(p is Point{ x: 0 });
    Console.WriteLine(p is Point3D{ x: 0 });
}
True
False

以下はプロパティパターンを位置パターンやその他のパターンと組み合わせる例です。


public class Person
{
    public string firstName;
    public string lastName;
    public int age;

    public string fullName
        => firstName + " " + lastName;

    public void Deconstruct(out string firstName, out string lastName, out int age)
        => (firstName, lastName, age) = (this.firstName, this.lastName, this.age);
}

static void Main(string[] args)
{
    object obj = new Person() {
        firstName = "John", lastName = "Doe", age = 20
    };

    //1、Person型の判定
    //2、age == 20 の判定(その他は破棄)
    //3、fullNameプロパティを変数fullNameに格納
    if (obj is Person(_, _, 20) { fullName: var fullName })
    {
        Console.WriteLine(fullName);
    }
}
John Doe

拡張プロパティパターン(C#10.0)

C#10.0以降、プロパティがさらにプロパティを持つ場合、入れ子のパターンを省略できます。


public struct Person
{
    public string name { get; set; }
}

static void Main(string[] args)
{
    Person p = new Person() { name = "John" };
    Console.WriteLine(p is { name.Length: 4 });

    //C#9.0までの書き方
    Console.WriteLine(p is { name: { Length: 4 } });
}

Person構造体のnameプロパティはstring型なので、Lengthプロパティを持っています。
これをプロパティパターンに組み込むにはC#9.0まではパターンを入れ子にする必要がありましたが、C#10.0からはドット演算時で入れ子のプロパティを直接書けるようになります。
これを拡張プロパティパターンといいます。

リレーショナルパターン(C#9.0)

パターンマッチングでは条件判定に関係演算子を使用できます。
つまり<><=>=で条件判定ができます。
(関係演算子は英語でリレーショナルオペレータ(relational operator)という)


int n = 7;
switch (n)
{
    case < 5:
        Console.WriteLine("5未満");
        break;
    case < 10:
        Console.WriteLine("10未満");
        break;
    default:
        Console.WriteLine("10以上");
        break;
}
10未満

関係演算子の後ろには定数のみ指定できます。
これも上から順に条件判定が行われます。

論理パターン(C#9.0)

複数の条件を同時に判定する場合はandornotが使用できます。
これらはパターン連結子といいます。


int n = 2;
switch (n)
{
    case >= 5 and < 10:
        Console.WriteLine("5以上10未満");
        break;
    case >= 10 or < 0:
        Console.WriteLine("10以上もしくは0未満");
        break;
    case not 1:
        Console.WriteLine("0以上5未満かつ1以外");
        break;
    default:
        Console.WriteLine("その他(1)");
        break;
}
0以上5未満かつ1以外

論理パターンは丸括弧で優先順位を変更できます。


object o = 1.23;

//int型でもstring型でもない場合
if (o is not (int or string)){ }

リストパターン(C#11.0)

リストパターンは、配列やリスト(Listクラスなど)との一致を判定できます。


int[] arr = new int[] { 1, 2, 3 };

switch (arr)
{
    case []: //空の配列
        Console.WriteLine("空");
        break;
    case [1]:
        Console.WriteLine("1");
        break;
    case [1, 2]:
        Console.WriteLine("1, 2");
        break;
    case [1, 2, 3]:
        Console.WriteLine("1, 2, 3");
        break;
}
1, 2, 3

リストの各要素の判定にも他のパターンが使用できます。


int[] ints = { 1, 2, 3 };

Console.WriteLine(ints is [ 1, 2, 3]);
//True

Console.WriteLine(ints is [1, _, 3]);
//True

Console.WriteLine(ints is [ > 0, 1 or 2, int x] && x % 2 == 1);
//True
//array[0] > 0
//array[1] == 1 || array[1] == 2
//array[2] is int && array[2] % 2 == 1
True
True
True

スライスパターン

リストパターンは、パターン内の要素数と判定する値の要素数は同じでないとパターンに一致しません。
任意の要素数に一致させるにはスライスパターンを使用します。


int[] ints = { 1, 2, 1, 2, 3 };

//要素が1,2で始まる場合に一致
Console.WriteLine(ints is [1, 2, ..]);

//要素が1で始まり3で終わる場合に一致
Console.WriteLine(ints is [1, .., 3]);

//要素が3で終わる場合に一致
Console.WriteLine(ints is [.., 3]);
True
True
True

スライスパターンは..というようにドットを二つ連続で記述します。
これは任意の数の要素に一致するという意味で、ここにはいくつ要素があっても一致します。
(0個にも一致する)
言い換えれば、スライスパターンは要素の前方一致、後方一致、またはその両方の判定が可能です。

スライスパターンは一つのリストパターンの中に一つだけ使用できます。
二つ以上書くとエラーになります。


int[] ints = { 1, 2, 1, 2, 3 };

//コンパイルエラー
//1で始まって、途中に2が含まれ、3で終わる
//...みたいなパターンは書けない
Console.WriteLine(ints is [1, .., 2, .., 3]);