Programming w/ C# ~ ちょっとおもしろいパターンマッチ記法の発見

発見

ちょっとおもしろいパターンマッチ記法を発見をした。

ネスト構造の任意データ型の文字列化

ネスト構造の任意データ型の要素1つ1つに再帰である処理を掛けてして文字列化したく、こんな処理を書いてみた。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable<object?> seq => $"({seq.ToStringRecursive().Join(", ")})",
      _                        => x?.ToString() ?? "NULL", // 本当はココにもう少し複雑な処理が入る
    } );

要素が単体ならば _ に落ちて x?.ToString() ?? "NULL" で処理する、要素がさらにシーケンスになっているならば再帰して1つの文字列にしたものを () で囲む、というつもり。

ところが、int 配列 new[] { 1, 2, 3 } をこれに掛けて処理しようとすると、IEnumerable<object?> にパターンマッチさせる意図に反して _ に落ち、int 配列型デフォルトのメソッド Type.ToString() が掛かって System.Int32[] という出力になってしまう。

Chat GPT 先生に聞いて調べてみると、IEnumerable<object?> の型引数 object? は何でも受けられる基底かと思いきや、なんと、ジェネリックの場合は参照型にしか効かず共変性が失われるのだとか。

そこで、値型・参照型無差別な IEnumerable にパターンマッチさせるにはジェネリックにしなければよいのだ、とこのように改善すると int 配列はうまく通る。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable seq => $"({seq.Cast<object?>().ToStringRecursive().Join(", ")})",
      _               => x?.ToString() ?? "NULL",
    } );

new[] { 1, 2, 3 }.ToStringRecursive();

// Result 
// (1, 2, 3)

階層構造データのテスト

階層構造データもうまくいくぞ。よしよし。

new object[] { 1, 2, 3, new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, 3, (4, 5, 6))

文字列を含めたテスト

文字列も要素に加えてみるか。あれ!? なんで X が一段階よけいに階層化されるんだ ... あっ!! そうか ...

new object[] { 1, 2, "X", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, (X), (4, 5, 6))

文字列 string 型は char[] とも解釈されうるから IEnumerable にひっかかってしまうのか ... 2文字以上で試してみるとやはり。

new object[] { 1, 2, "Test", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, (T, e, s, t), (4, 5, 6))

おもしろい記法

ではこう書かなければ。string 型のときは除くなんて、こんな形のパターンマッチ記法は初めて見た。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable seq when x is not string => $"({seq.Cast<object?>().ToStringRecursive().Join(", ")})",
      _                                    => x?.ToString() ?? "NULL",
    } );

new object[] { 1, 2, "Test", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, Test, (4, 5, 6))