System.Collections.ObjectModel.ReadOnlyObservableCollection<T> は、ObservableCollection<T> をラップして読み取り専用にしつつ、変更通知は伝搬する コレクションです。「内部では自由に書き換えたいが、外部からは読み取りだけにしたい」という MVVM の典型的な公開パターンを実現するための型です。
ReadOnlyObservableCollection<T> とは
- 名前空間:
System.Collections.ObjectModel - 基底クラス:
ReadOnlyCollection<T> - コンストラクタは
ObservableCollection<T>を受け取る - ラップしたコレクションへの変更通知(
CollectionChanged/PropertyChanged)を そのまま外部に伝える - 自身は書き換え API を提供しない(呼べばコンパイルエラー or
NotSupportedException)
using System.Collections.ObjectModel;
var inner = new ObservableCollection<string> { "apple", "banana" };
var view = new ReadOnlyObservableCollection<string>(inner);
view.CollectionChanged += (s, e) =>
Console.WriteLine($"View notified: {e.Action}");
inner.Add("cherry");
// View notified: Add
外部に渡すのは view、内部で書き換えるのは inner。読み手は変更を観測できるが、勝手に追加・削除はできません。
サポートするインターフェース
| インターフェース | 役割 |
|---|---|
IReadOnlyList<T> |
インデックスアクセスのみの読み取り専用リスト |
IReadOnlyCollection<T> |
読み取り専用 Count |
IList<T> |
互換のため実装するが書き換え API は例外 |
ICollection<T> |
同上 |
IEnumerable<T> |
foreach での列挙 |
IList / ICollection / IEnumerable |
非ジェネリック互換 |
INotifyCollectionChanged |
コレクション変更通知 |
INotifyPropertyChanged |
プロパティ変更通知 |
IList<T> を「実装はするが書き換え操作は例外を投げる」という構成は基底クラスの ReadOnlyCollection<T> の設計を引き継いでいます。コンパイル時には不変性を強制できない 点に注意してください。型としての契約は IReadOnlyList<T> が表現しています。
各インターフェースの基本については以下を参照してください。
IList<T>/IReadOnlyList<T>: Listの記事 INotifyCollectionChanged/INotifyPropertyChanged: ObservableCollectionの記事 IEnumerable<T>/IEnumerator<T>: IEnumerable と IEnumerator の記事
主な API と計算量
ReadOnlyObservableCollection<T> 自体は薄いラッパーで、内側の ObservableCollection<T> への委譲がほとんどです。
| 操作 | API | 計算量 |
|---|---|---|
| インデックスアクセス | [i] |
O(1) |
Count |
Count |
O(1) |
| 含有判定 | Contains(item) |
O(n) |
| 検索 | IndexOf(item) |
O(n) |
| 列挙 | foreach |
O(n) |
| 書き換え系 | Add 等 |
不可(例外 or コンパイルエラー) |
派生クラスとして「書き込みは内部、公開は読み取り専用」を実現する
典型的な MVVM の公開パターンは次のとおりです。
public class MainViewModel
{
private readonly ObservableCollection<string> _items = new();
public MainViewModel()
{
Items = new ReadOnlyObservableCollection<string>(_items);
}
public ReadOnlyObservableCollection<string> Items { get; }
// 公開メソッドは目的別に絞った API のみ
public void AddItem(string s)
{
// バリデーションなど
_items.Add(s);
}
}
XAML 側はこれまで通り ItemsSource="{Binding Items}" で OK。Items 経由では追加・削除ができないため、外部のコードが意図せずコレクションを書き換えるバグを防げます。
ReadOnlyCollection<T> との比較
| 観点 | ReadOnlyCollection<T> |
ReadOnlyObservableCollection<T> |
|---|---|---|
| ラップ対象 | IList<T>(List<T> 等) |
ObservableCollection<T> |
| 変更通知 | なし | CollectionChanged / PropertyChanged を中継 |
| バインディング自動更新 | × | ◯ |
| 用途 | 不変ビューの公開(非バインディング) | バインディング対象の不変ビュー |
「変更通知が要らない読み取り専用ビュー」なら ReadOnlyCollection<T> か、より厳密な不変性が必要なら ImmutableArray<T> / ImmutableList<T> を選びます。
使いどころ
- MVVM の公開プロパティ:ViewModel から外部(View や他のサービス)にバインディング対象を公開しつつ、書き込みは ViewModel 内部だけに閉じ込めたいとき。
- モデル層から UI 向けに変更追跡コレクションを渡す:変更を反映してほしいが直接書き換えてほしくない API 境界。
- テストでの安全性:呼び出し側が誤って Mock のコレクションに
Addしてしまう事故を防ぐ。
向かないケース
- UI バインディングがない場面 →
ReadOnlyCollection<T>かIReadOnlyList<T>の公開で十分。 - 完全な不変が要件 →
ImmutableArray<T>/ImmutableList<T>のほうが明確。 - 大量データを差し替える → 内部の
ObservableCollection<T>を都度差し替えるか、Reset通知を 1 回だけ飛ばす派生型を使うのが効率的。
注意点
- 完全な不変ではない:内部の
ObservableCollection<T>は呼び出し側からは直接見えないものの、IList<T>経由のキャストで書き込み API の実体に到達できる場合があります(((IList)view).Add(...)はNotSupportedException)。「悪意のある書き換えへの防御」ではなく「うっかりミスの防止」が主目的と理解してください。 - イベントハンドラの解除:購読側が長命だとリークの原因になるため、
-=で確実に解除する。 - UI スレッド制約:通知は内部の
ObservableCollection<T>の規約に従うため、書き込みは UI スレッドで行う/EnableCollectionSynchronizationを使う、といった配慮が必要(ObservableCollectionの記事 を参照)。
まとめ
ReadOnlyObservableCollection<T>はObservableCollection<T>をラップして読み取り専用にしつつ変更通知を中継 するコレクション。IReadOnlyList<T>/IList<T>/INotifyCollectionChanged/INotifyPropertyChanged等を実装。- MVVM の公開プロパティとして使うのが定番。書き込みは ViewModel 内部の
ObservableCollection<T>側で行い、外部には読み取り専用ビューを渡す。 - 通知不要なら [
ReadOnlyCollection<T>]、完全な不変が必要ならImmutableArray<T>/ImmutableList<T>を選ぶ。
これで System.Collections / System.Collections.Specialized / System.Collections.Generic / System.Collections.ObjectModel の主要コレクションを一通り整理しました。実務では各クラスの 性能特性とサポートインターフェース を踏まえて適切に選び分けることが、可読性と性能を両立させる近道になります。