LoginSignup
31
25

More than 5 years have passed since last update.

CompositeDisposableにおけるClearとDisposeの挙動

Last updated at Posted at 2017-10-10

脳死でDisposeで解放してたら思わぬところで躓いたのでメモ

CompositeDisposable

CompositeDisposableは簡単に言うとまとめてDisposeするためのクラスです
Rxライブラリより提供されています
IDisposableCollectionであり、自身がIDisposableであるところが特徴で、自身をDisposeすることで登録されている要素をまとめてDisposeします、便利ですね
もちろんRemoveなどすると要素ごとの削除に紐づいてその要素をDisposeしてくれます

DisposeとClear

まず2つのソースを見比べます


            /// <summary>
            /// Disposes all disposables in the group and removes them from the group.
            /// </summary>
            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();
                }
            }
    


            /// <summary>
            /// Removes and disposes all disposables from the CompositeDisposable, but does not dispose the CompositeDisposable.
            /// </summary>
            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();
            }
    

    違いとしてはDisposeの場合は内部でDisposeされたかどうかを記憶しているところだけです(個人的には変数でこうやって管理しているだけってところに驚きました)
    disposeされた状態ではAddやRemoveを通らなくなります
    Summaryでも書いてある通り、自身をDisposeするかどうか、の差です

    差異を招くケース

    CompositeDisposableTest.cs
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UniRx;
    
    public class CompositeDisposableTest : MonoBehaviour {
    
        private CompositeDisposable _compositeDisposable1 = new CompositeDisposable();
    
        private CompositeDisposable _compositeDisposable2 = new CompositeDisposable();
    
        private int _dispose = 0;
        private int _clear = 0;
    
        private IDisposable _test1, _test2, _test3, _test4;
    
        private void TestDispose()
        {
            _dispose++;
    
            //Disposeしてみる
            _compositeDisposable1.Dispose();
    
            _test1 = Observable.Interval(TimeSpan.FromSeconds(1))
                .Subscribe(_ => Debug.Log("Dispose1:" + _dispose))
                .AddTo(_compositeDisposable1);
    
            _test2 = Observable.Interval(TimeSpan.FromSeconds(1))
               .Subscribe(_ => Debug.Log("Dispose2:" + _dispose))
               .AddTo(_compositeDisposable1);
        }
    
        private void TestClear()
        {
            _clear++;
    
            // Clearしてみる
            _compositeDisposable2.Clear();
    
            _test3 = Observable.Interval(TimeSpan.FromSeconds(1))
                .Subscribe(_ => Debug.Log("Clear1:" + _clear))
                .AddTo(_compositeDisposable2);
    
            _test4 = Observable.Interval(TimeSpan.FromSeconds(1))
               .Subscribe(_ => Debug.Log("Clear2:" + _clear))
               .AddTo(_compositeDisposable2);
        }
    
        // Use this for initialization
        void Start () {
    
            //テスト用IDisposable
            _test1 = Observable.Interval(TimeSpan.FromSeconds(1))
                .Subscribe(_ => Debug.Log("Dispose1"))
                .AddTo(_compositeDisposable1);
    
            _test2 = Observable.Interval(TimeSpan.FromSeconds(1))
               .Subscribe(_ => Debug.Log("Dispose2"))
               .AddTo(_compositeDisposable1);
    
            _test3 = Observable.Interval(TimeSpan.FromSeconds(1))
                .Subscribe(_ => Debug.Log("Clear1"))
                .AddTo(_compositeDisposable2);
    
            _test4 = Observable.Interval(TimeSpan.FromSeconds(1))
               .Subscribe(_ => Debug.Log("Clear2"))
               .AddTo(_compositeDisposable2);
    
            //実験用
            Observable.Interval(TimeSpan.FromSeconds(1.5f))
                .Subscribe(_ => {
                    TestDispose();
                    TestClear();
                });
        }
    
    }
    
    

    2つのCompositeDisposableを用意し片方を定期的にDispose、もう片方を定期的にClearして回数をカウントしつつ挙動を見ようというコードです
    結果はこちら
    キャプチャ.PNG

    Clearだけがしっかりと反映されており、Disposeに関しては一回呼んだ後は動作していません
    これは1回目のDisposeで既に_compositeDisposale1自体がDisposeされてしまっているため、即座に解放されてしまっているというからくりです

    AddToの内部
    using System;
    using System.Collections.Generic;
    
    namespace UniRx
    {
        public static partial class DisposableExtensions
        {
            /// <summary>Add disposable(self) to CompositeDisposable(or other ICollection). Return value is self disposable.</summary>
            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;
            }
        }
    }
    

    CompositeDisposableを引数としたAddTo自体がそもそも引数としてコンテナを指定し、Addをするという挙動なので上で書いた通りDispose済みであるため結果としてAddをすり抜けます
    えっ、すり抜けるなら解放されないのでは…?とは行かないのです

    CompositeDisposableのAdd
            /// <summary>
            /// Adds a disposable to the CompositeDisposable or disposes the disposable if the CompositeDisposable is disposed.
            /// </summary>
            /// <param name="item">Disposable to add.</param>
            /// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
            public void Add(IDisposable item)
            {
                if (item == null)
                    throw new ArgumentNullException("item");
    
                var shouldDispose = false;
                lock (_gate)
                {
                    shouldDispose = _disposed;
                    if (!_disposed)
                    {
                        _disposables.Add(item);
                        _count++;
                    }
                }
                if (shouldDispose)
                    item.Dispose();
            }
    

    ちゃんとAddのなかではDispose判定をし、shouldDispose、つまり解放済みのものにAddされた場合はすり抜けてDisposeを行います
    なのでAddToが実質即座にDisposeを行ってしまっているので結果的にTest1とTest2が動作しないということですね

    結論

    自分はなんとなくClearのほうがいいとかは聞いたことがある程度で内部理解までには至ってませんでした、すごく単純なんですけど意外とハマってしまいました
    1回きりならまだいいんですけど、基本的にはDisposeせずにClearしましょう

31
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
25