はじめに
「クラス定義の上のほうにインスタンス変数をたくさん書いてOnDestroy()
内ですべて開放する処理を書く」みたいなことをしていると、どうしてもリソースの破棄のし忘れが発生してしまいます。
この記事ではIDisposable
インタフェースに関連するクラスや仕組みとその使い方についてまとめ、忘れずに破棄を行うにはどうしたらいいかについて述べます。
UnityのデフォルトのC#で使えるusing
ステートメントだけでなく、UniRxのCompositeDisposable
クラスやAddTo(GameObject gameObject)
メソッドについてもご紹介するので、UniRxを使っていない方はぜひ導入してみてください。
IDisposable
インターフェース
MSDNの説明によると、
アンマネージ リソースを解放するためのメカニズムを提供します。
となっています。実装すべきインターフェース自体はvoid Dispose()
のみといたってシンプルです。
System
名前空間にあるのでusing System;
を忘れずに。
解説の準備
解説のためにインスタンス生成時とDispose()
が呼ばれたときにログを吐くIDisposable
インターフェース実装クラスを作ります。
using System;
using UnityEngine;
public class DisposeLog : IDisposable
{
public string id { get; private set; }
public DisposeLog (string id)
{
this.id = id;
Debug.Log(string.Format("{0}:constructed", this.id));
}
public void Dispose()
{
Debug.Log(string.Format("{0}:called Dispose()", this.id));
}
}
便利な道具たち
using
ステートメント
usingステートメントを使うと、ブロックを抜けたときに()
に与えたオブジェクトのDispose()
が**必ず**呼ばれます。
using System;
using System.Collections;
using UnityEngine;
public class DisposableSample1 : MonoBehaviour
{
void Start()
{
try {
NormalCase();
ReturnCase();
ErrorCase();
} catch {
}
StartCoroutine(CoroutineCase1());
StartCoroutine(CoroutineCase2());
StartCoroutine(CoroutineCase3());
}
private void NormalCase()
{
using (var disposable = new DisposeLog("1")) {
// 特に何もしない
}
}
private void ReturnCase()
{
using (var disposable = new DisposeLog("2")) {
return;
}
}
private void ErrorCase()
{
using (var disposable = new DisposeLog("3")) {
throw new Exception("error");
}
}
private IEnumerator CoroutineCase1()
{
using (var disposable = new DisposeLog("4")) {
// 特に何もしない
}
yield return null;
}
private IEnumerator CoroutineCase2()
{
using (var disposable = new DisposeLog("5")) {
yield break;
}
}
private IEnumerator CoroutineCase3()
{
using (var disposable = new DisposeLog("6")) {
throw new Exception("error in coroutine");
}
yield return null;
}
}
実行結果
1:constructed
1:called Dispose()
2:constructed
2:called Dispose()
3:constructed
3:called Dispose()
4:constructed
4:called Dispose()
5:constructed
5:called Dispose()
6:constructed
6:called Dispose()
Exception: error in coroutine
上記のサンプルコードではyield break
で途中で処理を抜けたりusing
ブロックの内部でエラーを発生させていますが、ちゃんとDispose()
が呼ばれていることが確認できます。
これはコンパイラがコードを内部的にtry~finally
に変換してくれるからです(MSDNの解説)。
ちなみに()
で与えたオブジェクトはusing
ブロック内では読み取り専用になります。
CompositeDisposable
クラス
UniRxにはCompositeDisposable
というクラスが含まれています。このクラスはICollection<IDisposable>
を実装していて、IDisposable
をまとめて扱うのに便利です。
using System;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
public class DisposableSample2 : MonoBehaviour
{
private List<IDisposable> _disposableList = new List<IDisposable>();
private CompositeDisposable _compositeDisposable = new CompositeDisposable();
void Start()
{
// List
var disposable1 = new DisposeLog("1");
_disposableList.Add(disposable1);
_disposableList.Add(new DisposeLog("2"));
// CompositeDisposable
var disposable3 = new DisposeLog("3");
_compositeDisposable.Add(disposable3);
_compositeDisposable.Add(new DisposeLog("4"));
}
void OnDestroy()
{
// List<IDisposable>の破棄
foreach (var disposable in _disposableList) {
disposable.Dispose();
}
// CompositeDisposableの破棄、Clear()とDispose()は挙動が異なる
_compositeDisposable.Clear();
}
}
サンプルコードでは比較のためにList<IDisposable>
を使った実装もしてあります。CompositeDisposable
を使ったほうがOnDestroy()
内の破棄でforeachを使っておらず、若干スッキリしているのがわかると思います。
CompositeDisposable
はAddされたIDisposable
をRemoveするときにDispose()
を呼びます。Clear()
メソッドはすべてをRemoveしてDispose()
してくれます。
AddしたIDisposable
を一度にDispose()
するメソッドはClear()
とDispose()
があるのですが、Dispose()
を呼び出すとそれ以降はAddしたIDisposable
を即Dispose()
するようになるので、CompositeDisposable
のインスタンス自体を使いまわしたい場合はClear()
を使うほうがいいと思います。
AddTo
メソッド
UniRxを利用することでIDisposable
インターフェースにAddTo
というメソッドが追加されます。オーバーロードがいくつかありますが、以下のような挙動となっています。
いずれも戻り値はIDisposable
です。
シグネチャ | 挙動 |
---|---|
AddTo(GameObject gameObject) |
GameObjectのOnDestroy() のタイミングでレシーバのDispose() が呼ばれる |
AddTo(Component gameObjectComponent) |
component.gameObject のOnDestroy() のタイミングでレシーバのDispose() が呼ばれる |
AddTo(ICollection<IDisposable> container) |
container.Dispose() のタイミングでレシーバのDispose() が呼ばれる(内部的にcontainer.Add(レシーバ) される) |
AddTo(ICollection<IDisposable> container, GameObject gameObject) |
container.Dispose() とGameObjectのOnDestroy() のタイミングでレシーバのDispose() が呼ばれる(内部的にはAddTo(container).AddTo(gameObject) している) |
上記の表をみてもピンと来にくいのでサンプルコードを載せておきます。
using UnityEngine;
using UniRx;
public class DisposableSample3 : MonoBehaviour
{
CompositeDisposable _compositeDisposable = new CompositeDisposable();
void Start()
{
// OnDestroy()での破棄
new DisposeLog("1").AddTo(this);
new DisposeLog("2").AddTo(this.gameObject);
// OnClick()での破棄
new DisposeLog("3").AddTo(_compositeDisposable);
new DisposeLog("4").AddTo(_compositeDisposable, this.gameObject);
}
public void OnClick()
{
_compositeDisposable.Clear();
}
}
簡単に言うと、「AddToに渡した引数の破棄タイミングで破棄をするように予約を入れる」感じです。リストに加えるときとは主語と述語が反対になっており、メソッドチェーンで書けてスッキリしました。
1,2は自分に紐づくGameObject
のOnDestroy()
のタイミングでの破棄の予約をしています。実際は自動でAddComponent
されるObservableDestroyTrigger
というコンポーネントのOnDestroy()
内でDispose()
が呼ばれます。
3,4はOnClick()
が呼ばれるタイミングで破棄されるようにしてみました。このように破棄タイミングを自分でコントロールしたい場合はCompositeDisposable
を使いましょう。
4についてはOnDestroy()
とOnClick()
の両方のタイミングで破棄されます。
AddTo(this)
とAddTo(this.gameObject)
UniRx関係のコードでAddTo(this)
とAddTo(this.gameObject)
の両方のコードを見かけますが、細かい挙動の違いはあれどAddTo(this.gameObject)
よりはAddTo(this)
のほうが短く書けていいと思います。
Disposable.Create
UniRxのDisposable.Create(Action disposeAction)
を使うと簡単にIDisposable
インターフェースを実装したインスタンスを作成できます。シグネチャを見ればだいたい察しがつく通り、Dispose()
が呼ばれると引数で渡したAction disposeAction
が実行されます。
using UnityEngine;
using UniRx;
public class DisposableSample4 : MonoBehaviour
{
void Start()
{
var notDisposable = new NotDisposable(1, "hoge");
// 破棄処理の登録
Disposable.Create(() => {
notDisposable.id = 0;
notDisposable.name = "";
}).AddTo(this);
var disposable = Disposable.Create(() => notDisposable.name = "disposed");
using (disposable) {
// 何らかの処理
}
Debug.Log(notDisposable.name); // -> "disposed"
}
public class NotDisposable
{
public int id{ get; set; }
public string name{ get; set; }
public NotDisposable (int id, string name)
{
this.id = id;
this.name = name;
}
}
}
「using
ブロックやCompositeDisposable
などを使いたいが、いちいちIDisposable
を実装したクラスを作るほどでもない」というときに役立ちます。
一番有用な使い方はサンプルコードのようにIDisposable
を実装していないリソースについて確保と破棄を同じ場所に書いておけることだと思います。
Disposable.Empty
UniRxのDisposable.Empty
はDispose()
が呼ばれても何もしないIDisposable
を返します。
「引数でIDisposable
が必要だけど特になにもする必要がない」という場合に使えます。
忘れずに破棄を行うにはどうしたらいいか
そもそも破棄をし忘れるのは「リソースの確保と破棄の処理が離れている」ことが問題であることがほとんどです。
なので、
- 破棄処理を忘れてもいいようにしておく
- リソースの確保と破棄をセットで書く
のが肝心です。
具体的には以下をなるべく守るようにしましょう。
- なるべくすべてのクラスで
IDisposable
を実装する - できない場合は
Disposable.Create
をうまく使う - インスタンスを使うスコープが限定されているなら
using
ステートメントを使う - リソースの確保処理と同時に破棄を登録する処理を書く
-
OnDestroy()
で破棄を行うならAddTo(this)
- タイミングを細かく制御したいなら
CompositeDisposable
を使う
-
番外編:using
ブロックとコルーチン
(後ほど)