てくメモ

trivial な notes

【C#】コレクション式 : 独自型でコレクション式を使ってみる (CollectionBuilder 属性)

C# 12 で導入されたコレクション式は、CollectionBuilderAttributeを用いることで、独自型にも導入できる。

公式ドキュメント
コレクション式 (コレクション リテラル) - C# | Microsoft Learn
CollectionBuilderAttribute クラス (System.Runtime.CompilerServices) | Microsoft Learn


まずは普通に使ってみる。

PathCollection という(float, float)のコレクションを考える。
(※ IEnumerable<T>継承は必須ではないという指摘をいただき、修正しました 1

// ↓ 必要:CollectionBuilder 属性を付け、ビルダーの型とメソッド名を指定
[CollectionBuilder(typeof(PathCollectionBuilder), nameof(PathCollectionBuilder.Create))]
public class PathCollection
    : IEnumerable<(float, float)> // <- IEnumerable<T>の継承は必須ではないよう
{
    private readonly (float, float)[] paths;

    public PathCollection(ReadOnlySpan<(float, float)> paths)
    {
        this.paths = new (float, float)[paths.Length];
        paths.CopyTo(this.paths);
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    public IEnumerator<(float, float)> GetEnumerator() // <- 必要:列挙可能
    {
        foreach (var path in paths) yield return path;
    }
}

public static class PathCollectionBuilder
{
    // ↓ 必要:ビルダーのメソッドは ReadOnlySpan を受け、static
    public static PathCollection Create(ReadOnlySpan<(float, float)> span) => new(span);
}

これでコレクション式が導入できた。

PathCollection paths = [(0f, 0f), (0.1f, 0.1f), (1f, 1f)];


なお、ビルダーメソッドはstaticである必要があるがビルダー型がstaticな必要はなく、また、ビルダー型が対象の型と別である必要はない。

他に、コレクション式を使うだけならGetEnumeratorは not implemented でも問題ない。

それらをふまえ以下に、コレクションではない矩形を表す構造体で糖衣的にコレクション式を使ってみる、といった例2

[CollectionBuilder(typeof(Rect), nameof(Rect.Create))]
public struct Rect
{
    public float X;
    public float Y;
    public float Width;
    public float Height;

    public IEnumerator<float> GetEnumerator() => throw new NotImplementedException();

    public static Rect Create(ReadOnlySpan<float> span) => new() { X = span[0], Y = span[1], Width = span[2], Height = span[3] };
}
private string PrintRectCore(Rect rect) => $"({rect.X}, {rect.Y}) {rect.Width}x{rect.Height}";
public void PrintRect()
{
    Console.WriteLine(PrintRectCore([0f, 0f, 24f, 12f]));
}

導入自体は手軽。とはいえ、本来の用途外に使うのは色々と厳しそう。


なお、要素型はジェネリックでも可。
例:Source Browser(リンク先:ImmutableList<T>

一方で、複数の要素型を指定することはできなかった。

[CollectionBuilder(typeof(RGB), nameof(RGB.Create))]
public struct RGB : IEnumerable<float>, IEnumerable<byte>
{
    public float R;
    public float G;
    public float B;

    IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
    public IEnumerator<float> GetEnumerator() => throw new NotImplementedException();

    // 「明示的なインターフェースの実装」で要素型を複数用意できるけれど――
    IEnumerator<byte> IEnumerable<byte>.GetEnumerator() => throw new NotImplementedException();

    public static RGB Create(ReadOnlySpan<float> span) => new() { R = span[0], G = span[1], B = span[2] };

    // コレクション式で呼び分けてはくれず、こちらは呼ばれない
    public static RGB Create(ReadOnlySpan<byte> span) => new() { R = span[0] / 255f, G = span[1] / 255f, B = span[2] / 255f };
}
RGB rByte = [(byte)64, (byte)64, (byte)64];

ReadOnlySpan<byte> bytes = [64, 64, 64];
RGB rByte2 = RGB.Create(bytes);

Console.WriteLine($"({rByte.R}, {rByte.G}, {rByte.B})");
Console.WriteLine($"({rByte2.R}, {rByte2.G}, {rByte2.B})");
// (64, 64, 64)
// (0.2509804, 0.2509804, 0.2509804)
//  ↑ コレクション式で byte を渡しているが、暗黙変換で Create(ReadOnlySpan<float>) が呼ばれている(上段)。下段が byte のオーバーロードの出力


基本的に、通常想定された使い方での利用となりそう。(当たり前)



  1. ドキュメントの "The type parameter of the implemented System.Collections.Generic.IEnumerable<T> interface indicates the element type." という記述は……
  2. 前述の修正に合わせ、例示コードからIEnumerable<T>を外す修正をしました。