なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

【UniRx】オペレータのSwitchについて

始めに

UniRxのオペレータでSwitchを見かけたのですが、コードの挙動を追うことが出来ていなかったので勉強してまとめてみようと思います。



Switchとは?

Switchは簡単に言いますとIObservable<T>を切り替えるオペレータです。

IObservable<IObservable<T>>の拡張メソッドで、ストリームからIObservable<T>を受け取った際にすでにIObservable<T>を購読していた場合は破棄して新しく受け取ったIObservable<T>を購読します。

f:id:vxd-naoshi-19961205-maro:20210829201718p:plain



Switchを使ったカウントスタートボタン

言葉のみの説明では全くわからなかったので、簡単なサンプルを作成してみました。

ボタンを押したら、1秒おきにカウントを流すIObservableを生成してSwitchで新しいストリームに切り替えるプログラムです。

using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class CountStartButton : MonoBehaviour
{
    [SerializeField] private Button _button;
    [SerializeField] private Text _text;
    
    void Start()
    {
        _button.OnClickAsObservable()
            .Select(_ => 
                Observable.Timer(dueTime:
                    TimeSpan.FromSeconds(0), period:
                    TimeSpan.FromSeconds(1f))
                )
            .Switch()
            .Subscribe(count => _text.text = count.ToString())
            .AddTo(this);
    }
}


実行結果は次のようになりました。 ボタンを押すとカウントがスタートします。

ボタンを押すごとに、前のストリームが破棄されて新しいものに切り替わるため、カウントが0からスタートし直すことがわかります。

f:id:vxd-naoshi-19961205-maro:20210822174816g:plain



画像読み込みサンプル

次に、SelectMany, Switchを使用して画像読み込みサンプルを作成してみました。

内容はInputFieldにPathを入力して、それをもとにSpriteを非同期でロードするものです。また、ロードの際は1秒ほど遅延を入れています。

画像は次の3つを用意して入力しやすいようファイル名をそれぞれA, B, Cとしました。

f:id:vxd-naoshi-19961205-maro:20210822162551p:plain


SelectManyを使ったスクリプト

始めにSelectManyを使ったスクリプトを作成します。

InputFieldの文字が変わるたびに、スプライトをロードします。

using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class LoadWithSelectMany : MonoBehaviour
{
    [SerializeField] private Image _image;
    [SerializeField] private float _delayTime;
    [SerializeField] private InputField _inputField; 
    
    void Start()
    {
        _inputField.OnValueChangedAsObservable()
            .SelectMany(spritePath => LoadSprite(spritePath).ToObservable())
            .Subscribe(sprite => _image.sprite = sprite)
            .AddTo(this);
    }

    private async UniTask<Sprite> LoadSprite(string spritePath)
    {
        var sprite = await Resources.LoadAsync<Sprite>(spritePath);
        await UniTask.Delay(TimeSpan.FromSeconds(_delayTime));
        return (sprite as Sprite);
    }
}



結果は次のようになりました。

"A"を入力した後すぐに消して"B"を入力した場合、一度Aの画像が表示されてデフォルトのImageになり、最後にBの画像が表示されました。

f:id:vxd-naoshi-19961205-maro:20210822163525g:plain



Switchに書き換える

次にSelectManyの部分をSwtichに置き換えたプログラムを用意しました。

    void Start()
    {
        _inputField.OnValueChangedAsObservable()
            .Select(spritePath => LoadSprite(spritePath).ToObservable())
            .Switch()
            .Subscribe(sprite => _image.sprite = sprite)
            .AddTo(this);
    }


結果は次のようになりました。

"A"を入力した後にすぐ消してに"B"を入力した場合、Aの画像やデフォルトのImageは表示されず"B"が表示されました。

f:id:vxd-naoshi-19961205-maro:20210822170407g:plain


ただ今回の画像読み込みのサンプルはToObservable()を使っているため、UniTask自体は裏で動いたままなので本当はCancellationTokenでキャンセルした方が良いでしょう。(参考: UniTaskをCancellationTokenを指定しながらToObservableするメモ - Qiita

 

2つを比較して

両方で共通している個所はInputFieldに変更があった場合に新しいIObservableを生成しているところですが、SelectManyは生成されたIObservableをすべて並列で実行しています。

反対に、Switchでは画像読み込み中でも新しいIObservableが来た場合は新しいものを購読するので前のものは無視されます。 



参考

reactivex.io

light11.hatenadiary.com

neue.cc