はじめに
今回はUniRx
のCompositeDisposable
クラスについて取り上げたいと思います。
UniRx/CompositeDisposable.cs at master · neuecc/UniRx · GitHub
具体的な使い方から,実際のコードを見ながらのTipsについても触れていきたいです。
使い方
まずはCompositeDisposable
を使う前に私が書いていたコードを紹介させてください。
var subject = new Subject<Unit>(); // Step0. IDisposableをまとめるリストを作成する var disposables = new List<IDisposable>(); // Step1. ストリームを購読する subject.Subscribe(_ => Debug.Log("発火しました1")) .AddTo(disposables); subject.Subscribe(_ => Debug.Log("発火しました2")) .AddTo(disposables); // Step2. 最後にまとめてDisposeをして購読解除する foreach (var disposable in disposables) disposable.Dispose();
シンプルにIDisposable
をまとめて,最後にDispose
をしています。
これをCompositeDisposable
を使って書き換えてみましょう。
var subject = new Subject<Unit>(); // Step0. IDisposableをまとめるCompositeDisposableを作成する var compositeDisposable = new CompositeDisposable(); // Step1. ストリームを購読する subject.Subscribe(_ => Debug.Log("発火しました1")) .AddTo(compositeDisposable); subject.Subscribe(_ => Debug.Log("発火しました2")) .AddTo(compositeDisposable); // Step2. 最後にまとめてDisposeをして購読解除する compositeDisposable.Dispose();
ぶっちゃけList<IDisposable>
がCompositeDisposable
に変わったくらいですね。
あんまり変わらないやんけと思うかもしれませんが、CompositeDisposable
の内部もList<IDisposable>
を使っているので当然といえば当然なのかもしれません。
しかしCompositeDisposable
を使うと以下のメリットがあります。
- マルチスレッドに対応している
CompositeDisposable
がDispose
したかどうかのフラグを持っている
2番目についてはTips
にて深く触れたいと思います。
Tips
capacityを指定する
CompositeDisposable
のコンストラクタには以下のものがあります。
public CompositeDisposable(int capacity) { if (capacity < 0) throw new ArgumentOutOfRangeException("capacity"); _disposables = new List<IDisposable>(capacity); }
まあコレクション関連全般(List
も含む)にいえることですが、ある程度要素数の上限が決まっているならCapacity
を指定しておいた方が内部的にコピーが繰り返されることを減らすことができます。(Capacity
を超えると自動でCapacity
が増え、要素がコピーがされるようになっている)
https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.list-1.capacity?view=net-5.0
【C#】ListオブジェクトのCapacityの変化を観察する。 | 創造的プログラミングと粘土細工
どれくらい速度改善につながるといったことは試していないので分かりませんが、CompositeDisposable
でも出来ます。
Disposeをした前後
CompositeDisposable
クラスは_disposed
というフィールドが存在し、CompositeDisposable.Dispose
メソッドを呼ぶとtrue
になります。
private readonly object _gate = new object(); private bool _disposed; private List<IDisposable> _disposables; private int _count; private const int SHRINK_THRESHOLD = 64;
このフラグが立つと,以下のメソッドは実行しても動作しないようになってしまいます。
- Add
- Remove
- Dispose
DisposeとClearの違い
Dispose
とClear
メソッドが存在するのですが,これらはかなり近いメソッドになります。
public void Dispose() { var currentDisposables = default(IDisposable[]); lock (_gate) { if (!_disposed) { _disposed = true; currentDisposables = _disposables.ToArray(); _disposables.Clear(); _count = 0; } } if (currentDisposables != null) { foreach (var d in currentDisposables) if (d != null) d.Dispose(); } } public void Clear() { var currentDisposables = default(IDisposable[]); lock (_gate) { currentDisposables = _disposables.ToArray(); _disposables.Clear(); _count = 0; } foreach (var d in currentDisposables) if (d != null) d.Dispose(); }
違いは_disposed
のフラグをチェックするか否かでですね。Clear
メソッドはチェックを行いません。
基本的に_disposed = ture
であればCompositeDisposable
のメソッドは動作しなくなるので、Dispose
を使うで良いと思います。
そもそもDispose
をした後に色々操作を行うのは、設計自体が間違っている可能性が高いです。
またAddTo
メソッドでIDisaposable
を追加することがほとんどだと思いますが、これも内部ではAdd
メソッドを呼んでいるので,_disposed
フラグによって動作が変わります。
public static T AddTo<T>(this T disposable, ICollection<IDisposable> container) where T : IDisposable { if (disposable == null) throw new ArgumentNullException("disposable"); if (container == null) throw new ArgumentNullException("container"); container.Add(disposable); return disposable; }
補足
購読解除をするにあたって,AddTo
メソッドを呼ぶ以外にTake〇〇
メソッドを使う方法などもあります。
必ずしも購読解除=AddTo
ではないということに注意してください。