Qemelly(けめる)のプログラム備忘録

Unity / AtCoderについて書きます

UniRxの後処理まとめ

UniRxの後処理についてザックリまとめて整理。ようやく少し分かってきたレベルの人間が書いているので、ミスが含まれている可能性がありますことを先に忠告しておきます

参考: qiita.com

light11.hatenadiary.com

そもそもなんで後処理するの?

メモリリークするから

不要になった購読を解除しないとメモリリーク*1が発生します。リークが目に見えるレベルになるのは稀なので、丁寧に後処理しても恩恵が少ないように感じるかもしれませんが、それでもメモリリークがない方がマシなのは自明でしょう。

バグの原因になるから

不要な購読が残っていると、間違ってその購読処理が行われてしまう可能性があります。

また、誤って購読が重複してしまい、大変なことになることもあります(2敗)。

具体的な後処理方法

AddTo(this)

MonoBehaviourに依存しているクラスがOnDestroy()する際に購読をやめたい場合に使えます。

最も簡単です。

厳密にはReactiveProperty<T>Subject<T>等の発行側も実装しないといけない点に注意です!

using UniRx;
using UnityEngine;

namespace UniRxDisposeExample
{
    public class PlayerPresenter : MonoBehaviour
    {
        private Player _player;
        private void Start()
        {
            _player.Hp.DistinctUntilChanged().Subscribe(x => Debug.Log(x))
                .AddTo(this); // ← OnDestroy() で Dispose() が呼ばれ、購読が破棄される
        }
    }

    public class Player : MonoBehaviour
    {
        public IReadOnlyReactiveProperty<int> Hp => _hp; 
        private ReactiveProperty<int> _hp = new(100); 
        
        private void Start()
        {
            _hp.AddTo(this); // ← ReactiveProperty等もAddToを忘れずに!
        }
    }
}

AddTo(gameObject)

ほとんど一緒ですが、GameObjectそのものの寿命に対応させたい場合はこちらを利用しましょう。

こんな感じで、AddTo()の引数にとれるものは様々あります。

IDisposableの実装

MonoBahaviourを継承したクラスなら大抵AddTo(this)でどうにかなりますが、非Monoで書かれたクラスに対してはそうはいきません。明示的にDispose()を実行してやる必要があります。

そのためにIDisposableを実装し、適切なタイミングで破棄できるように準備しておきます。

namespace UniRxDisposeExample
{
    public class Score : IDisposable // 非MonoでUniRx使うときはほぼ必ず実装!
    {
        public IReadOnlyReactiveProperty<int> Current => _current;
        private readonly ReactiveProperty<int> _current = new();

        private readonly ScoreView _scoreView; // GUIにScoreを表示するクラス

        private CompositeDisposable _disposables; // Disposableをまとめることが出来るクラス

        public Score(ScoreView scoreView)
        {
            _scoreView = scoreView;
            _current.Value = 0;

            Register();
        }

        private void Register()
        {
            _current.DistinctUntilChanged()
                .Subscribe(_ => { _scoreView.SetCurrent(_current.Value); })
                .AddTo(_disposables);
        }

        public void Add(int value)
        {
            _current.Value += value;
        }

        public void Dispose()
        {
            _disposables?.Dispose(); // まとめてDispose()
            _current?.Dispose(); // ReactivePropertyもしっかりDispose()
        }
    }
}

このとき、きっちりDispose()まで実装しても、このDispose()が呼ばれなかったら何の意味もありません。適切なタイミングで呼び出し元がDispose()するように注意しましょう。

AddToは引数に対象のICollection<IDisposable>(CompositeDisposableとか)を入れて紐づけることが出来るので、うまく活用しましょう。

namespace Scripts.EntryPoints
{
    /// <summary>
    /// ゲームのエントリーポイント例
    /// </summary>
    public class GameEntryPoint : MonoBehaviour
    {
        [SerializeField] private ScoreView scoreView;
        
        private CompositeDisposable _disposables;
        
        private void Awake()
        {
            var score = new Score(scoreView);
            score.AddTo(_disposables); // もしくは _disposables.Add(score);
            
            // その他諸々のDI
        }


        private void OnDestroy()
        {
            _disposables.Dispose(); // ←ここまで書いてようやくMonoBehaviourのライフサイクルと紐づく
        }
    }
}

TakeUntilDestroy()

オペレーターの一種です。 Destroy()するまで購読します。つまり、Destroy()されたら購読は解除されます。

このオペレーターを利用すると、購読側がそれぞれ義務付けられていたAddTo()を発行側に組み込むことが出来るので、書き忘れによるミスを減らすことが出来そうです(それでも基本AddTo()を使った方がいいのは確か)。

using UniRx;
using UnityEngine;
using System;

namespace UniRxDisposeExample
{
    public class SubjectEmitter : MonoBehaviour
    {
        public IObservable<Unit> Subject => _subject.TakeUntilDestroy(gameObject);
        private Subject<Unit> _subject = new();
    }

    public class User : MonoBehaviour
    {
        [SerializeField] private SubjectEmitter subject;
        
        private void Start()
        {
            subject.Subject.Subscribe(_ => Debug.Log("SubjectEmitter"))
                .AddTo(this); // ここのAddToは最悪呼ばなくてもメモリリークにはならない
        }
    }
}

*1:使ってないのに割り当てられている無駄なメモリ領域が生じてしまうバグ