目次

キーワード

概要

前項で説明した通り、C# 7.0で、is演算子とswtichステートメントが拡張されて、is/caseの後ろにパターンを書けるようになりました。 パターンには以下のようなものがあります。

パターン バージョン 概要
型パターン C# 7.0 型の判定 int istring s
定数パターン C# 7.0 定数との比較 null1
var パターン C# 7.0 何にでもマッチ・変数で受け取り var x
破棄パターン C# 8.0 何にでもマッチ・無視 _
位置パターン C# 8.0 分解と同じ要領で、再帰的にマッチングする (1, var i, _)
プロパティ パターン C# 8.0 プロパティに対して再帰的にマッチングする { A: 1, B: var i }
パターンの組み合わせ C# 9.0 andor などでパターンの組み合わせができる int x and (x is 0 or 1)
関係演算パターン C# 9.0 <> などで数値の範囲を指定してマッチングする <= 0 and < 10
リスト パターン C# 11.0 配列やリストなどにマッチ [], [_, ..]

サンプル コード: https://github.com/ufcpp/UfcppSample/tree/master/Chapters/Data/Patterns

非再帰パターン

Ver. 7.0

C# の文法上の区別する意味はないんですが、 パターンのうち、C# 7.0 で入ったものと 8.0 で入ったものの一番の差は再帰があるかどうかです。 C# 7.0 からあるパターンは1層限り、8.0 で追加されたパターンは再帰的に何層もマッチできます。 (再帰がある方が難しいので後からの追加になりました。)

ここではまず、文法が簡単な再帰のないパターンから説明していきます。

型パターン (宣言パターン)

C# 6.0以前から元々あった is 演算子の自然な拡張になっているのが型パターン(type pattern)です。 以下のように、型の後ろに続けて、マッチした結果を変数で受け取れます。

static void M(object x)
{
    if (x is int i) Console.WriteLine("int " + i);
    else if (x is string s) Console.WriteLine("string " + s);
}

iscase の後ろで変数宣言をしているような形なので、宣言パターン(declaration pattern)とも呼びます。 (というか、C# 8.0以降は宣言パターンの方が正式な呼び方に変わっていそうです。)

型パターンは、旧来からある is 演算子や as 演算子とほぼ同じ挙動です。 上記の例は、概ね以下のコードと同じ動作になります。

if (x is int)
{
    var i = (int)x;
    Console.WriteLine("int " + i);
}
else
{
    string s = x as string;
    if (s != null)
    {
        Console.WriteLine("string " + s);
    }
}

as + != null になっていることからわかる通り、 型パターンは null にはマッチしません。 (以下のように、たとえ変数の型が一致していたとしても、null にはマッチしません。)

static void Main()
{
    M("abc"); // matched abc
    M(null);  // 何も表示されない
}
 
static void M(string x)
{
    if (x is string s) Console.WriteLine("matched " + s);
}

型パターンの簡単化

Ver. 9.0

C# 9.0 で型パターンがちょっとだけシンプルになりました。

型パターンは元々 C# 1.0 からある is 演算子の延長として作られています。 ところが、is の場合は x is T と書けるのに、switch では T _ のように変数宣言か _ (破棄) を伴う必要がありました。 これが C# 9.0 で改善されています。

int Is(object x)
{
    if (x is string)
    {
        return 1;
    }
    return 0;
}
 
int Switch(object x)
{
    switch (x)
    {
        // C# 8.0 までは string _ と書く必要あり
        case string: return 1;
    }
    return 0;
}
 
int SwitchExpr(object x) => x switch
{
    // C# 8.0 までは string _ と書く必要あり
    string => 1,
    _ => 0,
};

C# 9.0 時点でこれが書けたなかったのは次節の定数パターンとの混同を避けるためです。 例えば C# 9.0 では以下のようなコードが書けます。 こんなコードを書くこと自体少ないと思いますが、isの場合とswitchの場合で、型と定数、どちらが優先されるかが違うので注意が必要です。

class X { }
 
class Program1
{
    static int M(object x) => x switch
    {
        X => 1, // これは x の型がクラス X
        _ => 0,
    };
}
 
class Program2
{
    const int X = 1;
 
    static int M1(object x) => x switch
    {
        X => 1, // これは定数 1
        _ => 0,
    };
 
    static bool M2(object x) => x is X; // でもこれはクラス X (C# 8.0 以前との互換性のため)
}

定数パターン

iscaseの後ろには定数も書けます。これを定数パターン(constant pattern)と言います。 単体で見ると普通に == を使えば済むことも多いわけですが、 定数パターンであれば他のパターンとの混在ができます。

switch (x)
{
    // 定数パターン
    case 0: return 0;
    // 型パターン
    case string s: return s.Length;
    default: return -1;
}

名前通り定数しか使えません。 変数との値比較がしたければ、when句を使うなどが必要です。

static int M(object x, int comparand)
{
    switch (x)
    {
        // case comparand: とは書けない。
        // 型パターン + when 句を使う。
        case int i when i == comparand: return 0;
        default: return -1;
    }
}

ちなみに、定数パターンでは、ユーザー定義演算子を見ません。 以下のように、==isで挙動が違う場合があります。

using System;
 
class X
{
    // 全てのインスタンスが等しいという挙動。
    // 当然、x == null も常に true。
    public static bool operator ==(X a, X b) => true;
    public static bool operator !=(X a, X b) => false;
}
 
class Program
{
    static void Main()
    {
        var x = new X();
 
        // なんでも true なので、== null も true
        Console.WriteLine(x == null);
 
        // ユーザー定義の == は見ない。x が本当に null かどうかを見て、false になる
        Console.WriteLine(x is null);
    }
}

ポインターの null 比較

Ver. 8.0

細かい修正ですが、C# 8.0 からポインターに対してもパターン マッチングが使えるようになりました。 といってもプロパティや Deconstruct メソッドを持っているわけではないので、実質的には is null チェック用です。

static unsafe void M(int* p)
{
    // 元々 OK。
    Console.WriteLine(p == null);
 
    // C# 8.0 から OK。
    Console.WriteLine(p is null);
}

ReadOnlySpan に対するパターンマッチ

Ver. 11

C# 11 で、ReadOnlySpan<char> に対して文字列リテラルによる定数パターンが使えるようになりました。

// string を渡せたところには ReadOnlySpan<char> を渡せるように。
ReadOnlySpan<char> s = Console.ReadLine();

// is も
if (s is "a") { }

// switch ステートメントも
switch (s)
{
    case "b":
        break;
}

// switch 式も OK。
var x = s switch
{
    "c" => 1,
    _ => 2,
};

文字列処理に対して ReadOnlySpan<char> を使う機会が多くなってきたので特殊対応したそうです。

(パターンに書かれているのは "" みたいな「定数」ですが、 そこに string から ReadOnlySpan<char> の変換が挟まっていて定数とは言い切れない状態です。 C# チーム自身はそれほど実装に乗り気ではなく、外部からのコントリビューションで実装された機能になります。)

var パターン

型パターンと似ていますが、具体的な型名の代わりに var キーワードを使うと、 任意の型にマッチするパターンになります。 これを var パターン (var pattern)と言います。

switch の最後に書いて「その他全部」な分岐に使ったりします。

static int M(object x)
{
    switch(x)
    {
        case 0: return 0;
        case string s: return s.Length;
        case var other: return other.GetHashCode();
        // あるいは、変数で受け取る必要がないときは _ にしておけば破棄の意味なる
        // case var _:
    }
}

あと、少し悪用気味ではありますが、式中での変数宣言に使えたりします。

while (Console.ReadLine() is var line && !string.IsNullOrEmpty(line))
{
    Console.WriteLine(line);
}

1つ注意が必要な点として、var パターンは型パターンと違って、null にもマッチします。

string s = null;
Console.WriteLine(s is string x); // false
Console.WriteLine(s is var y);    // true

null をはじきたい場合は、var ではなく、後述するプロパティ パターンを使ってx is {} nonNullと書いたりします。

破棄パターン

Ver. 8.0

何にでもマッチして、マッチ結果を受け取る必要がない場合、_ を使って値を破棄できます。これを破棄パターン(discard pattern)と言います。

再帰はしないんですが、switch式の中と、再帰パターン内でしか使えないので C# 8.0 での実装になります。 isやステートメントの方のswitchcaseの後ろではvar _と書く必要がありますが、switch式の場合は_だけで値を破棄します。

static int M(object x)
    => x switch
    {
        0 => 0,
        string s => s.Length,
        _ => -1
    };

余談: 破棄パターンが C# 8.0 からな理由

ちなみに、isswitchステートメント内で _ だけでの値の破棄ができないのは既存コードとの互換性のためです。 普通書かないようなコードですが、一応、以下のようなコードが元々合法なため、意味を変えることができませんでした。

using System;
 
class _Type
{
    class _ { }
 
    static void M(object x)
    {
        Console.WriteLine(x is _); // class _ とのマッチ
    }
}
 
class _Constant
{
    const int _ = 0;
 
    static void M(object x)
    {
        switch (x)
        {
            case _: // 定数 _ とのマッチ
                break;
        }
    }
}

(あまりにも紛らわしいので、このコードを C# 8.0 でコンパイルすると警告が出ます。)

更新履歴

ブログ