C# Tips | コレクション・LINQ:Immutable変換

C# C#
スポンサーリンク

はじめに:「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#

この immutableImmutableList<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"]"有効" が取れますが、AddRemove をしても「新しいマップ」が返るだけで、元は変わりません。

ここでの重要ポイントは、「マスタや設定値を 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 で作った結果を、
ToImmutableListToImmutableDictionary で不変にしてしまい、
「誰にも書き換えられない前提」で安心して共有できるようにする

ことです。

押さえておきたいポイントを整理すると、次の通りです。

Immutable コレクションは「変更すると新インスタンスを返す」ので、元は壊れない
LINQ の最後を ToImmutableList / ToImmutableDictionary にするだけで不変化できる
ReadOnlyCollection は「ラッパー」、Immutable は「性質そのもの」
マスタ・設定値・マルチスレッド共有データなど、「壊されたくないもの」と相性が良い
戻り値の型を Immutable にしておくと、「書き換え禁止」が設計として明示できる

ここまで理解できていれば、
「とりあえず List を返す」段階から一歩進んで、
“壊されたくないコレクションには Immutable で鍵をかける”という設計判断ができるようになります。

タイトルとURLをコピーしました