はじめに
Collections系の記事を書こうとしていたら、System.Collections.Immutable
名前空間の型をまったく使えていなかったことに気づき、反省も兼ねて記事にしてみました。
仕様は調べて書いているつもりですが、 使いこなせていない人(=私)の記事ですので、実戦で使いこなしている方からの温かいツッコミを期待しています!
Immutable(イミュータブル/変化しない)
まずは_Immutable_なデータ型の代表的な特徴は以下の通り。
- 内容に変更がない場合の複製のコストが低い
- スレッドセーフ
これを意識したクラスを自分で作ったことがあるかどうかはさておき、使ったことがない人はいないと思います。
なぜならば、C#では string
型はImmutable だからです。
「Immutableってなんだったっけ?」と思ったら、string
型を思い出すと良いでしょう。
クラスライブラリの中身
System.Collections.Immutable
名前空間には、一通りのコレクション系クラスが揃っています。
型 | 種別 | 整列 |
---|---|---|
ImmutableArray<T> |
配列 | 〇 |
ImmutableList<T> |
List | 〇 |
ImmutableHashSet<T> |
Set | × |
ImmutableSortedSet<T> |
Set | 〇 |
ImmutableQueue<T> |
Queue | 〇 |
ImmutableStack<T> |
Stack | 〇 |
ImmutableDictionary<TKey,TValue> |
Dictionary | × |
ImmutableSortedDictionary<TKey,TValue> |
Dictionary | 〇 |
インスタンスの生成
これらのクラスはコンストラクタ持ちませんが、C#12 以降では、コレクション式で生成できます。
C#11 以前の環境では、対応する静的クラスのファクトリメソッドCreate<T>()
からインスタンス生成をしましょう。
var list = ImmutableList.Create<int>(2, 3, 5, 7);
var list = new List<int>() { 2, 3, 5, 7 };
// 「コレクション式」構文により、
// 統一感のある書き方ができるようになった。
ImmutableList<int> ilist = [2, 3, 5, 7];
List<int> list = [2, 3, 5, 7];
int[] array = [2, 3, 5, 7];
ReadOnlySpan<int> span = [2, 3, 5, 7];
コレクションの操作
同じようなメソッド名のコレクション操作を持ちますが、操作後は別のインスタンスが生成されることに注意します。
// ImmutableなListは、項目が追加された別のインスタンスを生成する
var addedList = list.Add(11);
// 以下の書き方でも末尾に項目追加可能。
// パフォーマンスは不明
list = [.. list, 11];
// MutableなListは、自身の中身を書き換える
list.Add(11);
いざ使うときに左辺を書き忘れそうで怖い……
Builder
不変なデータ型を扱うときに欠かせない、BuilderがそれぞれのImmutableコレクション型に存在しており、ToBuilder()
メソッドでBuilderに、ToImmutable()
メソッドで元のImmutableコレクションに、それぞれ移行できます。
ちょうど、String
型に対する StringBuilder
型のようなものだと思えば、しっくりくると思います。
// var originalList = ImmutableList.Create<int>(2, 3, 5, 7); (C# 11 以前)
ImmutableList<int> originalList = [2, 3, 5, 7];
// 大量の変更が必要な場合、Builderに。
var builder = originalList.ToBuilder();
builder.Add(11);
builder.Add(13);
builder.Remove(2);
// 変更後、ToImmutable()で、Immutableに。
var newList = builder.ToImmutable();
var originalString = "ABCDEFG";
// 大量の変更が必要な場合、Builderに。
var builder = new StringBuilder(originalString);
builder.Append("H");
builder.Append("I");
builder.Remove(0, 1);
// Immutableに。
var newStr = builder.ToString();
初めからBuilderを生成するためのファクトリメソッドもあります。
var builder = ImmutableList.CreateBuilder<int>();
どんな時に使う?
"マルチスレッドな操作で" "コレクションそのものを扱うとき" Immutableなコレクションが生きてくると思います。同様にマルチスレッド操作で登場するConcurrentなコレクションとは役割が違うので、それぞれを適した場面で使うことになります。
また、ReadOnlyなインタフェースとも似ているようですが、出番が全く違います。
型 | 性質 |
---|---|
System.Collections.Immutable. ImmutableHogehoge
|
不変だが、コレクション操作は行う コレクション全体をスレッドセーフに扱いたい |
System.Collections.Concurrent. ConcurrentHogehoge
|
可変 コレクション操作をスレッドセーフに扱いたい |
System.Collections.Generic. IReadOnlyHogehoge
|
コレクション操作を行わない スレッドセーフかは実装依存(スレッドセーフとはいえない) |
まとめ
System.Collections.Immutable
名前空間がマルチスレッドの全てを解決するわけではないですが、この存在を認識していれば 必ず活躍する場がある と思います。実際に使用する際にはImmutableInterlocked
クラスなども合わせて、うまく組み立てる必要がありそうですので、試行錯誤しつつ使っていきたいと思います。