- はじめに:「ReadOnlyCollection化」は“触ってほしくないコレクションに鍵をかける”技
- なぜ ReadOnlyCollection が必要なのか
- LINQ 結果を ReadOnlyCollection にする基本
- 拡張メソッド ToReadOnlyCollection を作る
- 実務での使いどころ 1:設定値・マスタの公開
- 実務での使いどころ 2:ドメインオブジェクトの内部コレクションを守る
- 実務での使いどころ 3:LINQ 結果を“確定させて”渡す
- ReadOnlyCollection化で意識しておきたいこと
- まとめ:「ReadOnlyCollection化ユーティリティ」は“コレクションに鍵をかける小さな設計ツール”
はじめに:「ReadOnlyCollection化」は“触ってほしくないコレクションに鍵をかける”技
業務コードを書いていると、こういう気持ちになる場面がよくあります。
「このコレクション、外から中身をいじられたくないんだけど…」
「呼び出し側には“見るだけ”にしてほしい」
そんなときに出てくるのが ReadOnlyCollection化 です。
つまり、
「中身は List などで持っておきつつ、
外には“読み取り専用の顔”だけ見せる」
という設計にするためのテクニックです。
ここでは、初心者向けに
なぜ ReadOnlyCollection が必要なのか
LINQ 結果を ReadOnlyCollection にする基本パターン
拡張メソッドとして ToReadOnlyCollection を用意する
業務での使いどころ(不変データ・設定値・マスタなど)
を、例題付きでかみ砕いて説明していきます。
なぜ ReadOnlyCollection が必要なのか
List をそのまま公開すると何が困るか
例えば、クラスの中でこういうフィールドを持っているとします。
public class UserGroup
{
private readonly List<string> _users = new();
public List<string> Users => _users;
}
C#一見便利そうですが、呼び出し側からこう書けてしまいます。
var group = new UserGroup();
group.Users.Add("Alice");
group.Users.Clear(); // 全削除もできてしまう
C#クラスの外から、内部の List を好き放題いじれてしまうわけです。
「本当はこのクラスだけが中身を管理したい」のに、
外からも Add や Remove、Clear ができてしまうのは、バグの温床になります。
ここでの重要ポイントは、
「List をそのまま公開する=内部実装を丸出しにしている」
ということです。
“カプセル化”の観点からも、あまり良くありません。
「見るだけ OK、変更は NG」という顔を作る
そこで登場するのが ReadOnlyCollection<T> や IReadOnlyList<T> です。
これらは、
要素の列挙(foreach)はできる
インデックスアクセス([0])もできる
でも Add / Remove / Clear はできない
という、「見るだけ専用のコレクションの顔」です。
using System.Collections.ObjectModel;
public class UserGroup
{
private readonly List<string> _users = new();
public ReadOnlyCollection<string> Users => _users.AsReadOnly();
}
C#こうしておけば、呼び出し側は
foreach (var u in group.Users) { ... }
var first = group.Users[0];
C#まではできますが、
group.Users.Add("Alice"); // コンパイルエラー
group.Users.Clear(); // コンパイルエラー
C#となり、「見るだけ」に制限できます。
ここでの重要ポイントは、
「“中身を守りたいコレクション”には、読み取り専用の顔をかぶせる」
という設計の考え方です。
LINQ 結果を ReadOnlyCollection にする基本
AsReadOnly と ToList の組み合わせ
ReadOnlyCollection<T> は、List<T> に対して AsReadOnly() を呼ぶことで作れます。
using System.Collections.ObjectModel;
using System.Linq;
var numbers = Enumerable.Range(1, 5); // IEnumerable<int>
var list = numbers.ToList(); // List<int> にする
var readOnly = list.AsReadOnly(); // ReadOnlyCollection<int> にする
C#LINQ の結果は IEnumerable<T> なので、
一度 ToList() してから AsReadOnly() する、という流れになります。
ここでの重要ポイントは、
「LINQ の結果 → ToList() → AsReadOnly() という 2 ステップが基本パターン」
ということです。
拡張メソッド ToReadOnlyCollection を作る
毎回 ToList().AsReadOnly() と書くのは長い
LINQ のチェーンの最後に、毎回こう書くのは少し冗長です。
var readOnly = users
.Where(x => 条件)
.ToList()
.AsReadOnly();
C#そこで、「ToList と同じノリで ToReadOnlyCollection が欲しい」という気持ちになります。
拡張メソッドを 1 本用意しておきましょう。
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
public static class ReadOnlyCollectionExtensions
{
public static ReadOnlyCollection<T> ToReadOnlyCollection<T>(
this IEnumerable<T> source)
{
return source.ToList().AsReadOnly();
}
}
C#使い方はこうです。
var readOnly = users
.Where(x => x.IsActive)
.OrderBy(x => x.Id)
.ToReadOnlyCollection();
C#ここでの重要ポイントは、
「ToReadOnlyCollection という名前だけで、“この結果は外から変更させないつもりなんだな”と伝わる」
ということです。
意図がコードに乗るので、チーム開発でも読みやすくなります。
実務での使いどころ 1:設定値・マスタの公開
「アプリ起動時に読み込んで、その後は変えない」データ
例えば、アプリ起動時に「ステータス一覧」を読み込んで、
あとは参照専用にしたいケース。
public class Status
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
}
C#public class StatusService
{
private readonly ReadOnlyCollection<Status> _statuses;
public ReadOnlyCollection<Status> Statuses => _statuses;
public StatusService(IStatusRepository repo)
{
_statuses = repo.GetAll() // IEnumerable<Status>
.OrderBy(x => x.Code)
.ToReadOnlyCollection();
}
}
C#呼び出し側は Statuses を列挙したり参照したりはできますが、Add や Remove はできません。
ここでの重要ポイントは、
「“アプリ全体で共有するマスタ・設定値”は ReadOnlyCollection で公開する」
というパターンです。
「勝手に書き換えられない」という保証が、設計として効いてきます。
実務での使いどころ 2:ドメインオブジェクトの内部コレクションを守る
集約の中のコレクションを外からいじらせない
DDD っぽい設計をしていると、
「エンティティの中にコレクションを持つ」ことがよくあります。
public class Order
{
private readonly List<OrderLine> _lines = new();
public ReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
public void AddLine(string itemCode, int quantity)
{
_lines.Add(new OrderLine(itemCode, quantity));
}
}
C#呼び出し側はこうなります。
var order = new Order();
order.AddLine("A", 1);
order.AddLine("B", 2);
foreach (var line in order.Lines)
{
Console.WriteLine($"{line.ItemCode} x {line.Quantity}");
}
// order.Lines.Add(...) はできない
C#ここでの重要ポイントは、
「“コレクションの変更はメソッド経由に限定し、
外からは読み取り専用の顔だけ見せる”」
という設計です。
これにより、「不正な状態にされる」リスクを大きく減らせます。
実務での使いどころ 3:LINQ 結果を“確定させて”渡す
IEnumerable のままだと「あとから中身が変わる」ことがある
LINQ の結果は IEnumerable<T> なので、
「列挙したタイミングで初めて中身が評価される(遅延実行)」ことが多いです。
IEnumerable<User> GetActiveUsers()
{
return _users.Where(x => x.IsActive);
}
C#このまま返すと、
呼び出し側が列挙するタイミング
そのときの _users の中身
によって、結果が変わってしまう可能性があります。
「この時点の結果を“固定”して渡したい」という場合、ToReadOnlyCollection で「確定+読み取り専用」にしてしまうのが有効です。
ReadOnlyCollection<User> GetActiveUsers()
{
return _users
.Where(x => x.IsActive)
.ToReadOnlyCollection();
}
C#ここでの重要ポイントは、
「ReadOnlyCollection化は、“結果を確定させる”+“外から変更させない”の二つを同時に満たせる」
ということです。
LINQ の遅延実行の影響を避けたいときにも、よく使うパターンです。
ReadOnlyCollection化で意識しておきたいこと
「本当に変更禁止にしたいか」を考える
なんでもかんでも ReadOnlyCollection にしてしまうと、
「テストで差し替えたい」「一部だけ入れ替えたい」といったときに、逆に扱いづらくなることもあります。
このコレクションは、誰が責任を持って変更するのか
外から変更されると困るのか
結果を“スナップショット”として固定したいのか
こういった観点で、「ReadOnlyCollection にするかどうか」を判断するとよいです。
ここでの重要ポイントは、
「ReadOnlyCollection化は“安全側に倒すための設計”であって、万能ではない」
ということです。
「守りたいところだけ鍵をかける」という感覚で使うのがちょうどいいです。
まとめ:「ReadOnlyCollection化ユーティリティ」は“コレクションに鍵をかける小さな設計ツール”
ReadOnlyCollection化の本質は、
中身は List などで柔軟に管理しつつ、
外には「読み取り専用の顔」だけ見せることで、
意図しない変更やバグを防ぐ
ことです。
押さえておきたいポイントを整理すると、
List をそのまま公開すると、外から好き放題いじられてしまうAsReadOnly() で ReadOnlyCollection<T> を作れる(元は List が必要)
LINQ 結果には ToList().AsReadOnly()、拡張メソッド ToReadOnlyCollection() を用意すると便利
マスタ・設定値・ドメイン内部コレクション・LINQ 結果の“スナップショット”など、業務での出番は多い
「誰が変更権限を持つか」を意識して、守りたいところだけ ReadOnlyCollection化する
ここまで腹落ちしていれば、
「とりあえず List を public に出す」段階から卒業して、
“コレクションの公開の仕方も設計できる C# エンジニア”に一歩近づけます。
