脳死で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するかどうか、の差です
#差異を招くケース
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して回数をカウントしつつ挙動を見ようというコードです
結果はこちら
Clearだけがしっかりと反映されており、Disposeに関しては一回呼んだ後は動作していません
これは1回目のDisposeで既に_compositeDisposale1自体がDisposeされてしまっているため、即座に解放されてしまっているというからくりです
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をすり抜けます
えっ、すり抜けるなら解放されないのでは…?とは行かないのです
/// <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しましょう