rksoftware

Visual Studio とか C# とかが好きです

C# 11 の新機能を確認「 リスト パターン 」

C# 11 の新機能を確認しています。目次は次の記事です。
rksoftware.hatenablog.com

今回は 「 リスト パターン 」。公式 Learn の記事は次です。

learn.microsoft.com

これは、公式 Learn をさらっと見るだけで特に気になる点は深掘りはしなくても十分把握できる気がします。
リスト ( や配列など ) の要素でパターンマッチできます。

■ 確認

次のようなコードが書けます。

IList<int> data = new[] { 1, 2, 3 };

var result = data switch
{
    [1, 2, 3] => 1,
    _ => 2
};
Console.WriteLine(result);  // 1

ちなみに前者のコードは .NET 6 を指定した場合は次のエラーになります。

エラー CS8936 機能 'リスト パターン' は C# 10.0 では使用できません。言語バージョン 11.0 以上を使用してください。

■ 使える型

// IList<T> 、 IReadOnlyList<T> ではできる
IList<int> dataA = new[] { 1, 2, 3 };
IReadOnlyList<int> dataB = new[] { 1, 2, 3 };
Console.WriteLine(dataA is [1, 2, 3]);
Console.WriteLine(dataB is [1, 2, 3]);

// これはできない
ICollection<int> dataXA = new[] { 1, 2, 3 };
IReadOnlyCollection<int> dataXB = new[] { 1, 2, 3 };
IEnumerable<int> dataXC = new[] { 1, 2, 3 };
Console.WriteLine(dataXA is [1, 2, 3]);
Console.WriteLine(dataXB is [1, 2, 3]);
Console.WriteLine(dataXC is [1, 2, 3]);
// エラー CS0021 角かっこ [] 付きインデックスを 'IReadOnlyCollection<int>' 型の式に適用することはできません
// エラー CS0021 角かっこ [] 付きインデックスを 'IEnumerable<int>' 型の式に適用することはできません
// エラー CS0021 角かっこ [] 付きインデックスを 'ICollection<int>' 型の式に適用することはできません
// エラー CS8985 リスト パターンは、型 'IEnumerable<int>' の値には使用できません。適切な 'Length' または 'Count' プロパティが見つかりませんでした。
エラー CS0021 角かっこ [] 付きインデックスを 'IReadOnlyCollection<int>' 型の式に適用することはできません
エラー CS0021 角かっこ [] 付きインデックスを 'IEnumerable<int>' 型の式に適用することはできません
エラー CS0021 角かっこ [] 付きインデックスを 'ICollection<int>' 型の式に適用することはできません
エラー CS8985 リスト パターンは、型 'IEnumerable<int>' の値には使用できません。適切な 'Length' または 'Count' プロパティが見つかりませんでした。

■ 対応する型を自分で作る

int 型のインデクサーと int Count プロパティがあれば使えそうです。次のコードは実行できます。

Sample data = new();
Console.WriteLine(data is [1, 2, 3]);

class Sample
{
    public int this[int index]{ get => 0; }
    public int Count => 0;
}

■ 破棄と ..

値がなんでもいい場所では _ を設定します。

var dataA = new[] { 1, 2, 3 };
var dataB = new[] { 1, 4, 3 };
Console.WriteLine(dataA is [1, _, 3]);  // True
Console.WriteLine(dataB is [1, _, 3]);  // True

値の個数もなんでもいい場所では .. を設定します。

var dataA = new[] { 1, 3 };
var dataB = new[] { 1, 4, 5, 3 };
Console.WriteLine(dataA is [1, .., 3]);  // True
Console.WriteLine(dataB is [1, .., 3]);  // True

.. は先頭末尾としても使えます。

var dataA = new[] { 1 };
Console.WriteLine(dataA is [.., 1]);  // True
Console.WriteLine(dataA is [.., 4]);  // False
Console.WriteLine(dataA is [1, ..]);  // True
Console.WriteLine(dataA is [4, ..]);  // False

.. は一つのパターンの中で一回だけ使えます。

Sample data = new();
Console.WriteLine(data is [.., 0, ..]);   // エラー CS8980 スライス パターンは、1 回限り、リスト パターン内で直接使用される可能性があります。

破棄や .. でマッチを評価しているとき、どのようにインデクサが呼ばれるかというとこんな感じでした。

Sample data = new();
Console.WriteLine(data is [1, _, 0, ..]);   // Count index0 False
Console.WriteLine(data is [0, _, 0, ..]);   // Count index0 index2 True
Console.WriteLine(data is [.., 5]);         // Count index4 False
Console.WriteLine(data is [0, .., 5]);      // Count index0 index4 False

class Sample
{
    public int this[int index] { get { Console.Write($"index{index} "); return 0; } }
    public int Count { get { Console.Write("Count "); return 5; } }
}

先頭から必要なところだけを見ていって、マッチしないことが分かったらそこで止めています。いいですね。
switch 式でも毎回インデクサにアクセスするのではなく必要なだけ。いいですね。

Sample data = new();
var result = data switch
{
    [1, 2, ..] => 1,
    [1, 1, ..] => 2,
    [0, 1, ..] => 3,
    [0, 0, ..] => 4,
    _ => 5
};
Console.WriteLine(result);  // Count index0 index1 4

class Sample
{
    public int this[int index] { get { Console.Write($"index{index} "); return 0; } }
    public int Count { get { Console.Write("Count "); return 5; } }
}

■ パターンマッチのいろいろ

パターンマッチの一環なのでいろいろはパターンマッチの条件が使えます。

var dataA = new[] { 1, 2, 3 };
Console.WriteLine(dataA is [1 or 2, <= 2, >= 3]);   // True

■ 定数 変数

条件に定数は使えますが変数は使えません。

const int valueA = 1;
int valueB = 1;
var dataA = new[] { 1, 2, 3 };
Console.WriteLine(dataA is [valueA, ..]);   // True
Console.WriteLine(dataA is [valueB, ..]);   // エラー CS0150 定数値が必要です

■ 要素をマッチ後に使う

要素の一部をマッチ後に使う場合はこんな感じに var を使います。

var dataA = new[] { 1, 2, 3 };
Console.WriteLine((dataA is [var value1, var value2, ..]) ? (value1, value2) : (0, 0)); // (1, 2)

この時、 when も使えます。

var data = new[] { 1, 2, 3 };
var result = data switch
{
    [var value1, var value2, ..] when value1 == 0 => $"1 ({value1}, {value2})",
    [var value1, var value2, ..] when value1 == 1 => $"2 ({value1}, {value2})",
    _ => "3"
};
Console.WriteLine(result);  // 2 (1, 2)

■ リスト パターン自体の and or

リスト パターン自体を andor で組み合わせることもできます。

var data = new[] { 1, 2 };
Console.WriteLine(data is [1, ..] and [..,2]);  // True
Console.WriteLine(data is [1, ..] and [.., 3]); // False
Console.WriteLine(data is [0, ..] or [1,..]);   // True
Console.WriteLine(data is [0, ..] or [2, ..]);  // False

■ 必要な時、自然に書くと思います

この新機能は使う機会のない人はあまり使わないと思います。使うべき時に忘れていないようしっかり覚えておきましょう。でも細かいことは特に覚えておかなくても普通に書けると思います。大丈夫です。安心してください。