はじめに:「Immutable変換」は“絶対に書き換えられないコレクション”を手に入れる技
業務システムを書いていると、こういう悩みが出てきます。
「このコレクション、誰かにうっかり書き換えられてバグになったら嫌だ」
「マルチスレッドで読まれるデータは、安全に扱いたい」
そこで出てくるのが Immutable(イミュータブル)コレクション です。
「一度作ったら中身を変更できないコレクション」で、C# では System.Collections.Immutable 名前空間にまとまっています。
ここでの「Immutable変換」とは、
LINQ で作った結果や List<T> などを、ImmutableList<T> や ImmutableDictionary<TKey, TValue> などに変換して、
「絶対に書き換えられない形」にして扱う
というユーティリティのことです。
ここから、初心者向けに順番にかみ砕いていきます。
Immutableコレクションとは何か
「Add できるけど、中身は変わらない」という不思議な性質
Immutable コレクションは、「変更メソッドがない」のではなく、「変更しても“新しいコレクション”を返す」のが特徴です。
using System.Collections.Immutable;
var list = ImmutableList<int>.Empty;
var list2 = list.Add(1);
var list3 = list2.Add(2);
C#list は空のまま、list2 は [1]、list3 は [1, 2] という別物になります。
「元を壊さずに、新しいバージョンを作る」という動きです。
ここでの重要ポイントは、「Immutable=“変更禁止”ではなく、“変更は常に新インスタンスとして返す”」ということです。
だからこそ、共有しても安全です。
まず準備:Immutableコレクションを使えるようにする
NuGet パッケージと using
Immutable コレクションは標準ライブラリですが、プロジェクトによっては NuGet パッケージの追加が必要です。
System.Collections.Immutable パッケージを追加し、コード側では次を記述します。
using System.Collections.Immutable;
C#これで ImmutableList<T> や ImmutableDictionary<TKey, TValue> などが使えるようになります。
LINQ 結果を ImmutableList に変換する
ToImmutableList の基本
LINQ の結果は IEnumerable<T> なので、ToList() と同じノリで ToImmutableList() が使えます。
using System.Linq;
using System.Collections.Immutable;
var numbers = Enumerable.Range(1, 5); // 1,2,3,4,5
var immutable = numbers
.Where(x => x % 2 == 0)
.ToImmutableList();
C#この immutable は ImmutableList<int> です。Add メソッドはありますが、元のインスタンスは変わりません。
var immutable2 = immutable.Add(100); // immutable はそのまま、immutable2 は [2,4,100]
C#ここでの重要ポイントは、「LINQ の最後を ToImmutableList にするだけで、“結果を絶対に書き換えられない形”にできる」ということです。
ImmutableDictionary への変換
ToImmutableDictionary で「安全なマップ」を作る
業務では「コード → 名前」のようなマスタを Dictionary で持つことが多いですが、
「誰かに書き換えられたら困る」典型例でもあります。
public class Status
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
}
var statuses = new[]
{
new Status { Code = "A", Name = "有効" },
new Status { Code = "I", Name = "無効" },
};
C#これを ImmutableDictionary に変換します。
var statusMap = statuses
.ToImmutableDictionary(x => x.Code, x => x.Name);
C#statusMap["A"] で "有効" が取れますが、Add や Remove をしても「新しいマップ」が返るだけで、元は変わりません。
ここでの重要ポイントは、「マスタや設定値を ImmutableDictionary にしておくと、“どこからも書き換えられないマップ”になる」ということです。
ReadOnlyCollection との違いをはっきりさせる
ReadOnlyCollection は「ラッパー」、Immutable は「性質そのもの」
ReadOnlyCollection<T> は、「内部に List を持っていて、それを読み取り専用の顔で包んでいる」だけです。
内部の List を持っている側が変更すれば、中身は変わります。
var list = new List<int> { 1, 2, 3 };
var readOnly = list.AsReadOnly();
list.Add(4); // readOnly からも 4 が見えるようになる
C#一方、ImmutableList は「中身を変える操作は常に新インスタンスを返す」ので、
誰かが勝手に中身を書き換える、ということが起こりません。
ここでの重要ポイントは、「“本当に不変にしたい”なら Immutable、“外からの変更だけ防ぎたい”なら ReadOnlyCollection」という使い分けです。
実務での使いどころ 1:アプリ全体で共有するマスタ・設定値
起動時に読み込んで Immutable にしてしまう
例えば、ステータス一覧を DB から読み込んで、アプリ全体で使い回したいケース。
public class StatusService
{
public ImmutableList<Status> Statuses { get; }
public ImmutableDictionary<string, Status> StatusByCode { get; }
public StatusService(IStatusRepository repo)
{
var all = repo.GetAll(); // IEnumerable<Status>
Statuses = all
.OrderBy(x => x.Code)
.ToImmutableList();
StatusByCode = Statuses
.ToImmutableDictionary(x => x.Code);
}
}
C#これで、どこから参照しても「絶対に中身が変わらない」マスタになります。
もし変更が必要なら、「新しい StatusService を作る」か、「新しい Immutable を作って差し替える」ことになります。
ここでの重要ポイントは、「“いつの時点のマスタか”をバージョンとして扱える」ということです。
Immutable にしておくと、「途中で誰かが書き換えたせいで整合性が崩れた」という事故を防げます。
実務での使いどころ 2:マルチスレッドで共有するデータ
ロック地獄を避けるために「そもそも書き換えない」
複数スレッドから参照されるコレクションを List<T> で持っていると、
ロックをどうするか、いつ書き換えられるか、などを常に気にしないといけません。
Immutable コレクションなら、「読み取りだけならロック不要」です。
書き換えたいときは、新しい Immutable を作って、参照を差し替えます。
private ImmutableList<User> _users = ImmutableList<User>.Empty;
public ImmutableList<User> Users => _users;
public void ReloadUsers(IEnumerable<User> newUsers)
{
_users = newUsers.ToImmutableList();
}
C#読み手は Users をそのまま参照するだけで安全です。
書き換えは「新しいインスタンスを丸ごと入れ替える」ので、
途中で中身が半端な状態になることもありません。
ここでの重要ポイントは、「“書き換えない設計”にすることで、マルチスレッドの難しさを減らせる」ということです。
Immutable変換ユーティリティを用意する
ToImmutableXXX を「意図を持って使う」
System.Collections.Immutable には、すでに便利な拡張メソッドが揃っています。
ToImmutableList()ToImmutableArray()ToImmutableDictionary()ToImmutableHashSet()
LINQ の最後をこれらにするだけで、「結果を不変にする」ことができます。
例えば、ユーティリティクラスの中で「不変で返したい」ことを明示するために、
戻り値の型を最初から Immutable にしてしまうのも有効です。
public ImmutableList<User> GetActiveUsers()
{
return _users
.Where(x => x.IsActive)
.OrderBy(x => x.Id)
.ToImmutableList();
}
C#ここでの重要ポイントは、「戻り値の型に Immutable を使うことで、“呼び出し側は絶対に書き換えないでね”をコンパイルレベルで保証できる」ということです。
まとめ:「Immutable変換」は“壊されたくないコレクションに、絶対防御を張る”技
Immutable変換の本質は、
LINQ や List で作った結果を、ToImmutableList や ToImmutableDictionary で不変にしてしまい、
「誰にも書き換えられない前提」で安心して共有できるようにする
ことです。
押さえておきたいポイントを整理すると、次の通りです。
Immutable コレクションは「変更すると新インスタンスを返す」ので、元は壊れない
LINQ の最後を ToImmutableList / ToImmutableDictionary にするだけで不変化できる
ReadOnlyCollection は「ラッパー」、Immutable は「性質そのもの」
マスタ・設定値・マルチスレッド共有データなど、「壊されたくないもの」と相性が良い
戻り値の型を Immutable にしておくと、「書き換え禁止」が設計として明示できる
ここまで理解できていれば、
「とりあえず List を返す」段階から一歩進んで、
“壊されたくないコレクションには Immutable で鍵をかける”という設計判断ができるようになります。
