HashSet<T> は便利ですが、通常の HashSet<T> は可変です。つまり、追加や削除をすると、そのインスタンス自身が書き換わります。これは普通のアプリでは自然な振る舞いですが、状態管理や並行処理では「いつの間にか中身が変わっていた」という問題につながることがあります。
そうした場面で役立つのが ImmutableHashSet<T> です。これは一言で言えば、変更できない HashSet です。
ImmutableHashSetとは何か
ImmutableHashSet<T> は、一度作った集合をそのまま固定し、変更が必要なときは新しい集合を返すというスタイルのコレクションです。
using System.Collections.Immutable;
var set = ImmutableHashSet<int>.Empty;
var set2 = set.Add(1);
var set3 = set2.Add(2);
ここで重要なのは、set はずっと空のままだということです。
set // 空
set2 // {1}
set3 // {1, 2}
普通のHashSetとの違い
両者の違いは「変更が破壊的か非破壊的か」にあります。
| 項目 | HashSet | ImmutableHashSet |
|---|---|---|
| 変更 | 破壊的 | 非破壊的 |
| 元のインスタンス | 書き換わる | 変わらない |
| スレッドセーフ | 基本なし | 読み取り共有に強い |
| パフォーマンス | 高速 | やや不利 |
この違いは単なる実装上の差ではなく、設計そのものに影響します。
なぜ「不変」が重要なのか
可変コレクションは便利ですが、「誰かが書き換える可能性がある」という前提を常に背負います。たとえば:
var list = new List<int> { 1, 2 };
var other = list;
list.Add(3);
この場合、other も同じインスタンスを参照しているので、内容が変わります。これは当たり前ですが、参照の共有が広がるほど追跡しにくくなります。
一方、Immutableなコレクションではこうなります。
var set1 = ImmutableHashSet.Create(1, 2);
var set2 = set1.Add(3);
このとき set1 は変わりません。
変更結果は set2 という新しい値になります。
この性質は、バグを減らすうえで非常に強力です。
スレッドセーフという意味
ImmutableHashSet<T> は「どんな操作でも完全にスレッドセーフ」というより、読み取り共有に強いという理解が適切です。中身が変わらないので、複数スレッドから同時に参照しても、「途中で別スレッドが書き換えた」という問題が起きません。
つまり、
- 変更を共有しない
- 変更したいときは新しい値を作る
という設計にすることで、ロックに頼らず安全性を高めやすくなります。
完全コピーではない
「毎回新しいインスタンスを作るなら高コストでは?」と思うのは自然です。 ここで重要なのが 構造共有(Structural Sharing) です。
ImmutableHashSet<T> は、変更のたびにすべてを丸ごとコピーしているわけではありません。変わらない部分は再利用し、必要なところだけ新しく作ります。
イメージとしては、
set1: A → B → C
set2: A → B → C → D
のように、既存の構造を共有しつつ差分だけを持つ感じです。
このため、単純な完全コピーよりはかなり効率的です。
基本的な使い方
追加
var newSet = set.Add(10);
削除
var newSet = set.Remove(10);
複数追加
var newSet = set.Union(new[] { 1, 2, 3 });
ここで重要なのは、どの操作も元のインスタンスを変更しないことです。
よくあるミス
これはかなり大事です。
set.Add(1); // 無意味
HashSet<T> の感覚で書くとついこうしてしまいますが、ImmutableHashSet<T> では戻り値を受け取らないと何も変わりません。
正しくは:
set = set.Add(1);
です。
Immutable 系のコレクションは、「メソッドを呼ぶと変化する」のではなく、「新しい値を受け取る」という感覚に切り替える必要があります。
どんな場面で使うのか
ImmutableHashSet<T> は万能ではありません。毎回更新が発生するようなホットな処理では、通常の HashSet<T> の方が向いていることもあります。
一方で、次のような場面では非常に相性が良いです。
典型的な用途
- 状態管理
- undo / redo
- スナップショットの保持
- 並行処理で共有する読み取りデータ
- 副作用を減らしたい設計
特に、「今の状態を壊さずに次の状態を作る」という発想と相性が良く、Redux的な設計やイベントソーシング的な考え方にもつながります。
注意点
通常のHashSetより遅い
不変性を維持するぶん、通常の HashSet<T> より更新コストは高めです。
頻繁に変更する巨大集合に対して、何でも Immutable を使えばよいというわけではありません。
戻り値を必ず使う
これが最も実務的な注意点です。戻り値を捨てると、変更したつもりで何も変わっていない、というバグになります。
まとめ
ImmutableHashSet<T> は、不変な集合を扱うためのコレクションです。要素を追加・削除しても元のインスタンスは変わらず、新しい集合が返されます。
その価値は、単なる「変更できないコレクション」というよりも、
- 状態の追跡がしやすい
- 共有によるバグが減る
- 読み取り共有に強い
という設計上のメリットにあります。
押さえておきたいポイント
- 非破壊更新である
- 戻り値を受け取る必要がある
- 読み取り共有に向く
- 通常の
HashSet<T>より更新コストは高い
「安全に状態を持ちたい」「共有による事故を減らしたい」ときに、ImmutableHashSet<T> はかなり有効な選択肢になります。