📦

.Net8 で追加されたコレクション式で初期化できるファーストクラスコレクションを実装する

2024/09/03に公開

C# で配列を初期化するとき、どうやっていますか?
C# 12.0 (.NET 8.0) からはコレクション式が利用可能になりました。
下記のコード例からわかるように、配列の種類を問わず同じ様に、そして直感的に初期化できます。

int[] array = [1, 2, 3, 4, 5];
List<int> list = [1, 2, 3, 4, 5];
ImmutableArray<int> immutableArray = [1, 2, 3, 4, 5];

コレクション式のより詳しい説明やメリットなどはこちらがわかりやすいです。

https://ufcpp.net/study/csharp/cheatsheet/ap_ver12/

今回は自作のファーストクラスコレクションでも、このコレクション式を利用できるようにする方法を紹介します。

TL;DR

  • クラスに CollectionBuilder(typeof(MyCollection), nameof(Create)) 属性を付与
  • static な MyCollection Create(ReadOnlySpan) メソッドを実装

補足: ファーストクラスコレクションとは

ファーストクラスコレクションとは、大雑把に説明すればコレクションをラップしたクラスのことです。
アプリケーション内のコレクションに関する操作をそのクラスに集約し、コレクションの振る舞いをカプセル化・制限することができます。

詳しい使い方やメリットについては、以下の書籍がわかりやすいです。

https://gihyo.jp/book/2022/978-4-297-12783-1

ファーストクラスコレクションの実装

まずはコレクション式なし、つまり C# 12.0 より前のやり方でファーストクラスコレクションを実装します。

public class ImmutableIntCollection : IEnumerable<int>
{
    private readonly ImmutableArray<int> _items;

    public ImmutableIntCollection(IEnumerable<int> items)
    {
        _items = items.ToImmutableArray();
    }

    public int Count => _items.Length;

    public bool Contains(int item) => _items.Contains(item);

    public int Sum() => _items.Sum();

    public double Average() => _items.Average();

    public ImmutableIntCollection Add(int item)
    {
        return new ImmutableIntCollection(_items.Add(item));
    }

    public ImmutableIntCollection Remove(int item)
    {
        return new ImmutableIntCollection(_items.Remove(item));
    }

    public IEnumerator<int> GetEnumerator() => _items.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public override string ToString() => $"[{string.Join(", ", _items)}]";
}

この例では、IEnumerable<int> を実装した不変なコレクション ImmutableIntCollection クラスを作成しました。
このようなクラスを作成した場合、利用方法は下記のようになるでしょう。

var collection = new ImmutableIntCollection(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(collection); // 出力: [1, 2, 3, 4, 5]
Console.WriteLine($"Sum: {collection.Sum()}"); // 出力: Sum: 15
Console.WriteLine($"Average: {collection.Average()}"); // 出力: Average: 3

var newCollection = collection.Add(6);
Console.WriteLine(collection); // 出力: [1, 2, 3, 4, 5](元のコレクションは変更されない)
Console.WriteLine(newCollection); // 出力: [1, 2, 3, 4, 5, 6]

コレクションの操作自体はシンプルでわかりやすいですが、配列の初期化をするために元データの配列を作るという処理が必要でやや冗長です。

コレクション式を利用したファーストクラスコレクションの実装

コレクション式を利用することで、ファーストクラスコレクションをより簡潔に利用できます。
下記のコード例では、ImmutableIntCollection クラスを拡張し、コレクション式を利用できるようにしています。

+ [CollectionBuilder(typeof(ImmutableIntCollection), nameof(Create))]
public class ImmutableIntCollection : IEnumerable<int>
{
    private readonly ImmutableArray<int> _items;

    public ImmutableIntCollection(IEnumerable<int> items)
    {
        _items = items.ToImmutableArray();
    }

+     public static ImmutableIntCollection Create(ReadOnlySpan<int> items)
+     {
+         return new ImmutableIntCollection(items.ToArray());
+     }

    // 残りは同じなので省略
}

主な変更は以下の2つです。

  1. CollectionBuilder 属性の追加

    [CollectionBuilder(typeof(ImmutableIntCollection), nameof(Create))]
    

    これによりコンパイラがコレクション式を解釈する際に、ImmutableIntCollection.Create メソッドを呼び出すようになります。

  2. static な Create メソッドの追加

    public static ImmutableIntCollection Create(ReadOnlySpan<int> items)
    {
        return new ImmutableIntCollection(items.ToArray());
    }
    

    コレクション式で渡される ReadOnlySpan<int>ImmutableIntCollection に変換して返却するためのメソッドです。

これらの変更により、以下のようにコレクション式を使用してImmutableIntCollectionを初期化できるようになります。
下記の利用例を見ていただければ、今までの初期化処理と比べて、コードが簡潔になったことがわかります。

ImmutableIntCollection collection = [1, 2, 3, 4, 5];
Console.WriteLine(collection); // 出力: [1, 2, 3, 4, 5]
Console.WriteLine($"Sum: {collection.Sum()}"); // 出力: Sum: 15
Console.WriteLine($"Average: {collection.Average()}"); // 出力: Average: 3

var newCollection = collection.Add(6);
Console.WriteLine(collection); // 出力: [1, 2, 3, 4, 5](元のコレクションは変更されない)
Console.WriteLine(newCollection); // 出力: [1, 2, 3, 4, 5, 6]

まとめ

コレクション式の利用はコードの書きやすさと可読性を向上させ、開発効率も向上します。
また公式ドキュメントを読み解くと、コレクション式はパフォーマンスの最適化も図っているとのことです。

コンパイラは、スタティック分析を使用して、コレクション式で宣言されたコレクションを作成する最もパフォーマンスの高い方法を判断します。
引用元: https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/collection-expressions

これからの C# プロジェクトでは、コレクション式を積極的に利用していくことが望ましいでしょう。
今回紹介したように、自作のファーストクラスコレクションでもコレクション式を利用することも可能です。
ぜひ活用してみてください。

GitHubで編集を提案

Discussion