導入
C#には通知可能なコレクションとしてObservableCollection<T>が用意されています。
内部実装の話
ソースコードを見てみます。
1 . 現在の実装(CollectionChangedの隠蔽)
public class ReadOnlyObservableCollection<T> : ReadOnlyCollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
public ReadOnlyObservableCollection(ObservableCollection<T> list) : base(list)
{
((INotifyCollectionChanged)Items).CollectionChanged
+= new NotifyCollectionChangedEventHandler(HandleCollectionChanged);
}
void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnCollectionChanged(e);
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
if (CollectionChanged != null)
{
CollectionChanged(this, args);
}
}
event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
{
add { CollectionChanged += value; }
remove { CollectionChanged -= value; }
}
protected virtual event NotifyCollectionChangedEventHandler CollectionChanged;
}
2 . 実装の代案(カプセル化)
public class ReadOnlyObservableCollection<T> : ReadOnlyCollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
public ReadOnlyObservableCollection(ObservableCollection<T> list) : base(list)
{
((INotifyCollectionChanged)Items).CollectionChanged
+= new NotifyCollectionChangedEventHandler(HandleCollectionChanged);
}
void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnCollectionChanged(e);
}
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
if (CollectionChanged != null)
{
CollectionChangedInternal(this, args);
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add { CollectionChangedInternal += value; }
remove { CollectionChangedInternal -= value; }
}
protected virtual event NotifyCollectionChangedEventHandler CollectionChangedInternal;
}
何れも、protected virtual event CollectionChanged(Internal)とprotected virtual void OnCollectionChanged をオーバーライドできます。
event CollectionChangedにアクセスしやすい分、カプセル化の方が便利に思えます。
chatGPTの回答
ChatGPTに訊いてみました。
1. 不変性(ReadOnly)の強調
ReadOnlyObservableCollection は、ObservableCollection の変更を監視するための読み取り専用のラッパーです。このため、開発者が意図せず CollectionChanged に直接アクセスして変更を試みるのを防ぐ意図があると考えられます。
2. 意図的なアクセス制御
通常の ObservableCollection では CollectionChanged が public ですが、ReadOnlyObservableCollection では INotifyCollectionChanged を通じてのみアクセスできるようになっています。これにより、イベントの発火を制御する側(元の ObservableCollection)と、受け取る側(ReadOnlyObservableCollection の利用者)を明確に分けることができます。
3. リスナーの意図を明確にする
ReadOnlyObservableCollection を使用する場合、開発者はコレクションを変更するのではなく変更を監視する目的で使うはずです。
したがって、INotifyCollectionChanged にキャストして CollectionChanged を扱うことで、「このイベントは ReadOnlyObservableCollection ではなく、元の ObservableCollection から伝播されたものだ」という意図を明確にできます。
- 設計の一貫性
.NET のコレクション設計では、イベントの公開レベルを制限することで API の意図を明示することがあります。
例えば:
BindingList も ListChanged イベントを提供しますが、基本的にはデータバインディングを通じて使用することを想定。
ReadOnlyObservableCollection も INotifyCollectionChanged を実装していますが、明示的実装にすることで、意図的に CollectionChanged を扱う必要があることを示しています。
まとめ
ReadOnlyObservableCollection は、あくまで変更通知を受け取る側であり、変更の発火を意図していない。
CollectionChanged を明示的に INotifyCollectionChanged にキャストしないとアクセスできないのは、開発者が意図せず誤用するのを防ぐため。
イベントの利用を INotifyCollectionChanged を通じたアクセスに限定することで、リスナーの意図を明確にする設計になっている。
この仕様は、「変更通知を受け取る目的でのみ ReadOnlyObservableCollection を使ってください」という設計意図の現れですね。
とのこと。
結び
隠蔽、カプセル化ともにコンストラクタ内で
((INotifyCollectionChanged)Items).CollectionChanged
+= new NotifyCollectionChangedEventHandler(HandleCollectionChanged);
とイベントの購読を行っていますが、購読を解除する方法が提供されていません。
例えReadOnlyObservableCollectionの方が短命で、破棄しようにもObservableCollectionに参照を持ち続けられます。
その結果、ReadOnlyObservableCollectionの寿命はラップされるObservableCollectonと同じになることは注意。