はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【UniRx】IDisposableをまとめてDisposeするためのCompositeDisposableクラスを利用する

はじめに

今回はUniRxCompositeDisposableクラスについて取り上げたいと思います。

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を使うと以下のメリットがあります。

  • マルチスレッドに対応している
  • CompositeDisposableDisposeしたかどうかのフラグを持っている

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の違い

DisposeClearメソッドが存在するのですが,これらはかなり近いメソッドになります。

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〇〇メソッドを使う方法などもあります。

light11.hatenadiary.com

必ずしも購読解除=AddToではないということに注意してください。