再帰パターン

Ver. 8.0

C# 7.0 の範囲で使えるものは、「パターン」と呼ぶのが仰々しいくらい単純なものでした。 C# 8.0 で、再帰的に使えるパターンが追加されて、ようやくパターン マッチングが完成しました。

例えば以下のような感じです。

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public Point(int x = 0, int y = 0) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
 
class Program
{
    static int M(object obj)
        => obj switch
    {
        0 => 1,
        int i => 2,
        Point (1, _) => 4, // new!
        Point { X: 2, Y: var y } => y, // new!
        _ => 0
    };
}

位置パターン

位置パターン (positional pattern)は、 分解と同じ要領で再帰的なマッチングを行うパターンです。

分解と同様、Deconstructメソッドを呼んでメンバーを取り出した上で、 それぞれのメンバーの値に対してマッチングを行います。 例えば、先ほど例として使ったPointクラスを引き続き使うとして、以下のように書けます。

static int M(Point p)
    => p switch
{
    (1, 2) => 0,
    (var x, _) when x > 0 => x,
    _ => -1
};

このコードは概ね以下のような意味になります。

p.Deconstruct(out var x, out var y);
if (x is 1 && y is 2) return 0;
if (x > 0) return x;
return -1;

サブパターンの順序に意味があるため「位置」パターンという呼び名になっています。

上記の例では元々型がPointだとわかっているので型名を省略していますが、 型の明示もできます。

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

また、後述しますが、プロパティ パターンとの混在や、 型パターンのように変数を付け足すこともできます。

obj switch
{
    Point (var x, _) { Y: var y } p => x * y
};

位置パターンとか言いつつ、名前付き引数のノリで、名前付きなパターン マッチングもできます。

static int NamedPattern(Point p)
    => p switch
{
    (x: 1, y: 2) => 0,
    (x: var x, y: _) when x > 0 => x,
    _ => -1
};

補足: コンストラクター呼び出しの逆

位置パターンは、コンストラクター呼び出し(new)の逆に当たる構文です。 書き方も、コンストラクターと対になっています。

// 位置指定で構築できるんなら、位置指定でマッチングできるべき
var p1 = new Point(1, 2);
var r1 = p1 is Point (1, 2);
 
// 名前指定で構築できるんなら、名前指定でマッチングできるべき
var p2 = new Point(x: 1, y: 2);
var r2 = p2 is Point (x: 1, y: 2);
 
// 型推論が効く場合に new の後ろの型名は省略可能(になる予定)なら
// 型が既知なら型名を省略してマッチングできるべき
Point p3 = new (1, 2);
var r3 = p3 is (1, 2);
 
// 階層的に new できるんなら、階層的にマッチングできるべき
var line = new Line(new Point(1, 2), new Point(3, 4));
var r4 = line is ((1, 2), (3, 4));

分解方法

位置パターンは 分解と同じ要領でメンバーの値を取り出します。 分解もそうなんですが、タプル(C# のタプル構文を使って作る ValueTuple 構造体の値)の場合とそうでない場合で内部的な挙動が少し変わります。

まず、タプルの場合、コンパイラーの最適化によって、タプルのフィールドを直接参照するようなコードが生成されます。 例えば以下のようなコードを書いた場合、

public bool TupleSyntax((int a, int b) x) => x is (1, 2);

以下のようなコードと同じような挙動をします。

// ValueTuple の場合は直接フィールドを参照する。
public bool TupleSyntax((int a, int b) x)
{
    return x.a == 1 && x.b == 2;
}

そうでない場合、まずはコンパイル時に Deconstruct メソッドを探します。 見つかった場合は、それを使うコードが生成されます。 例として以下のようなクラスを用意します。

using System.Runtime.CompilerServices;

class X : ITuple
{
    public object this[int index] => index;
    public int Length => 2;
    public void Deconstruct(out int a, out int b) => (a, b) = (0, 1);
}

この型に対して以下のようなコードを書いた場合、

public bool Deconstruct(X x) => x is (1, 2);

以下のようなコードと同じような挙動をします。

// コンパイル時に Deconstruct メソッドが見つかる場合はそれを使って分解。
public bool Deconstruct(X x)
{
    x.Deconstruct(out var a, out var b);
    return a == 1 && b == 2;
}

分解代入や分解変数宣言とは違って、位置パターンの場合はコンパイル時に Deconstruct メソッドが見つからない場合があります。 この場合、ITupleインターフェイス(System.Runtime.CompilerServices名前空間)を使って分解を試みます。 例えば以下のようにobjectで値を渡すコードを書いた場合、

public bool Object(object x) => x is (1, 2);

以下のようなコードと同じような挙動をします。

// コンパイル時の解決ができない場合、ITuple を実装しているかどうかを見る。
// Length とインデクサーを使ってマッチング。
public bool Object(object x)
{
    return x is ITuple t
        && t.Length == 2
        && t[0] is int a && a == 1
        && t[1] is int b && b == 1
        ;
}

タプル switch

位置パターンに伴って、switchステートメントの () の中に、複数の値を , 区切りで書けるようになりました。

int Compare(int? a, int? b)
{
    switch (a, b)
    {
        case (null, null): return 0;
        case (int _, null): return -1;
        case (null, int _): return -1;
        case (int a1, int b1): return a1.CompareTo(b1);
    }
}

このコードは、まず (a, b) というタプルを作って、それを switch ステートメントに掛ける挙動になります。case の後ろに書かれているのは位置パターンです。

要するに、意味としては switch ((a, b)) と書くのと同じです。 なので実体としては「複数の値に対するswitch」というより、「タプルに限り、() を一段省略できる」という機能です。

0、1要素の分解

タプル構築や分解代入・分解宣言では0、1要素のもの( ()(x)) は認められていませんが、 位置パターンでは認められるようになりました。 それぞれ、0、1引数のDeconstructメソッドが調べられます。

using System;
 
class X
{
    public void Deconstruct() { }
    public void Deconstruct(out int a) => a = 0;
}
 
class Program
{
    static void Main() => M(new X());
 
    static void M(X x)
    {
        // 0 引数の位置パターン。
        // Deconstruct() を持っていることが使える条件。
        if (x is ()) Console.WriteLine("Deconstruct()");
 
        // 1 引数の位置パターン。
        // Deconstruct(out T) を持っていることが使える条件。
        // ただ、キャストの () との区別が難しいらしく、素直に x is (int a) とは書けない。
        // 前後に余計な var や _ を付ける必要あり。
        if (x is (int a) _) Console.WriteLine($"Deconstruct({a})");
    }
}

0引数のものは単に () で OK です。

一方で、1引数のものは、キャストの () との区別が難しいそうで、 素直に (constant) とか (T variable) とかは書けません。 var (subpattern) とか (subpattern) _ とか、前後に余計なものを付けることでキャストと区別します。

最適化での Deconstruct 削除

位置パターンでは、コンパイラーの最適化によって Deconstruct メソッドの呼び出しが消えることがあります。 以下のように、すべて _ で値を破棄してしまう場合には Deconstruct メソッドを呼び出す必要がなく、 実際、呼び出しが消えてなくなります。

using System;
 
class X
{
    // Deconstruct に副作用を持たせる
    public void Deconstruct() => Console.WriteLine("Deconstruct()");
    public void Deconstruct(out int a)
    {
        Console.WriteLine("Deconstruct(out int a)");
        a = 0;
    }
    public void Deconstruct(out int a, out int b)
    {
        Console.WriteLine("Deconstruct(out int a, out int b)");
        (a, b) = (0, 0);
    }
}
 
class Program
{
    static void Main()
    {
        var x = new X();
 
        // Deconstruct() がないとコンパイル エラーになるけど、
        // Deconstruct() は呼ばれない。
        Console.WriteLine(x is ());
 
        // Deconstruct(out int) がないとコンパイル エラーになるけど、
        // Deconstruct(out int) は呼ばれない。
        Console.WriteLine(x is var (_));
 
        // Deconstruct(out int, out int) がないとコンパイル エラーになるけど、
        // Deconstruct(out int, out int) は呼ばれない。
        Console.WriteLine(x is (_, _));
    }
}

また、引数の数が同じ位置パターンをいくつか並べた際にも、Deconstruct メソッドの呼び出しは1回にまとめられます。

class X
{
    public int Value { get; }
    public X(int value) => Value = value;
    public void Deconstruct(out int value) => value = Value;
}
 
class Program
{
    static int M(X x)
        => x switch
    {
        // 引数の数が同じ位置パターンを3回。
        // この場合、Deconstruct(out int) の呼び出しは1回にまとめられる。
        (0) _ => 1,
        (1) _ => 2,
        (2) _ => 0,
        _ => x.Value
    };
}

ちなみに、仕様上は「必ず消える」という保証もないです(「消えることがある」という仕様)。 なので、Deconstruct メソッドは副作用を起こさないように作ることが推奨されます。

プロパティ パターン

プロパティ パターン(property pattern)は、プロパティに対して最適的なマッチングを行いパターンです。 (プロパティ パターンという名前に反して、フィールドも使えます。)

書き方は、{ PropertyName: SubPattern, ... } というように、 プロパティ名と、そのプロパティに対して掛けたいパターンを : でつなぎます。 複数のプロパティに対して使う場合はそれぞれを , で区切ります。 位置パターンとは違って、名前の省略はできません。

再び Point クラス(int 型の2つのプロパティ XY を持つ)を例に挙げます。 以下のような書き方ができます。

static int M(Point p)
    => p switch
{
    { X: 1, Y: 2 } => 0,
    { X: var x, Y: _ } when x > 0 => x,
    _ => -1
};

このコードは概ね以下のような意味になります。

var x = p.X;
var y = p.Y;
if (x is 1 && y is 2) return 0;
if (x > 0) return x;
return -1;

位置パターンと同様、型の明示もできます。

static int M(object obj)
    => obj switch
{
    int i => i,
    string s => s.Length,
    Point { X: 0, Y: 0 } => 0,
    Point (_, _) => 1,
    _ => -1
};

ちなみに、プロパティ パターンと言いつつ、フィールドも参照できます。

using System;
 
class X
{
    // (外から見て) get-only なプロパティ
    public int GetOnly { get; private set; }
 
    // get/set 可能なプロパティ
    public int GetSet { get; set; }
 
    // フィールド
    public int Field;
 
    // set-only なプロパティ
    public int SetOnly { set => GetOnly = value; }
}
 
class Program
{
    public static void Main()
    {
        // オブジェクト初期化子では、set が public なプロパティか readonly ではないフィールドを指定可能
        var x = new X { GetSet = 1, Field = 2, SetOnly = 3 };
 
        // プロパティ パターンでは、get が public なプロパティかフィールドを指定可能
        Console.WriteLine(x is { GetOnly: 3, GetSet: 1, Field: 2 });
    }
}

オブジェクト初期化子の逆

「位置パターンはコンストラクター呼び出しの逆」という話をしましたが、 同様に、プロパティ パターンはオブジェクト初期化子と対になるものです。

// 初期化子でプロパティ指定できるんなら、プロパティ指定でマッチングできるべき
var p1 = new Point { X = 1, Y = 2 };
var r1 = p1 is { X: 1, Y: 2 };
 
// 混在で構築できるんなら、混在でマッチングできるべき
var p2 = new Point(x: 1) { Y = 2 };
var r2 = p2 is (1, _) { Y: 2 };

ただ、= は代入の意味なのでパターンとしては使えず、代わりに : になっています。 : を使っているのは、位置パターンと構文を共通化できて実装が楽だからだそうです。

位置パターンとプロパティ パターンの順序

位置パターンとプロパティ パターンを混在して使う場合、 Deconstructメソッドとプロパティのアクセサーの呼び出し順序には保証がないそうです。

残念ながら、以下のようなコードには動作保証がないそうです。

using System;
 
enum Type { A, B }
 
class X
{
    public Type Type { get; }
    public X(Type type) => Type = type;
 
    // それぞれ Type が一致しているときだけ値を取り出せ、そうでなければ例外
    public int A => Type == Type.A ? 1 : throw new InvalidOperationException();
    public int B => Type == Type.B ? 2 : throw new InvalidOperationException();
 
    // 分解でタイプ判定
    public void Deconstruct(out Type t) => t = Type;
}
 
class Program
{
    static void Main()
    {
        Console.WriteLine(M(new X(Type.A)));
        Console.WriteLine(M(new X(Type.B)));
    }
 
    // 以下のコードはたまたま動く可能性はあるものの、C# の言語使用としては保証がない。
    // Deconstruct よりも先にプロパティのアクセスがあると例外が出ることがある。
    static int M(X x) => x switch
    {
        (Type.A) { A: var a } => a,
        (Type.B) { B: var b } => b,
        _ => 0
    };
}

非 null マッチング

プロパティ パターンは、暗黙的にnullチェックが挟まって、非 null であることが保証されます。 しかも、x is { } というように、中身が空っぽであっても null チェックだけは挿入されるので、 x is { }を「xはnullではない」の意味で使えます。

C# 7.0 までのパターンだと、null チェックを楽に書く手段がなかったです。

struct LongLongNamedStruct { }
 
void M1(LongLongNamedStruct? x)
{
    // こういう書き方だと null チェックになる。
    if (x is LongLongNamedStruct nonNull)
    {
        // obj が null じゃない時だけここが実行される。
        // でも、x の型が既知なのに、長いクラス名をわざわざ書くのはしんどい…
    }
}
 
void M2(LongLongNamedStruct? x)
{
    // が、var パターンは null にもマッチしちゃう。
    // (var は「何にでもマッチ」。null でも true になっちゃう。)
    if (x is var nullable)
    {
        // obj が null でもここが実行される。
    }
}

単に null チェックだけなら !(x is null) とか x.HasValue だけでいいんですけども、 値を使いたければその後ろで var nonNull = x.GetValueOrDefault(); とかが必要で、何を使うにしても微妙に長くなりがちでした。

そこで先ほどの x is { } を使います。 以下のような書き方で、null 許容型の null チェックをしつつ、値を変数に受け取れます。

void M3(LongLongNamedStruct? x)
{
    // (C# 8.0) プロパティ パターンであれば、null チェックを含む。
    if (x is { } nonNull)
    {
        // obj が null じゃない時だけここが実行される。
    }
}

更新履歴

ブログ