はじめに
「クラス定義の上のほうにインスタンス変数をたくさん書いて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ブロックとコルーチン
(後ほど)