UniRxを紐解く「Take(1)とFirst()の違い」

UniRxとは

大変便利なReactiveExtensionsのUnity版ライブラリ。

neue cc

neuecc (@neuecc) | Twitter

 

最近、業務でも業務外でもこちらを触ることが多く、色々な地雷を踏み倒してきました。

備忘録の意味も兼ねて今回は

  • 「Take(1)とFirst()」の違い

をまとめようと思います。

入門者向きではないです、ご了承ください。

あとかなり長いです。

 

ソースコードなどはこちらの

qiita.com

を参考にさせていただいています。

TakeとかFirstってなんですか?という方は、こちらをまず読まれると良いと思います。

その他にも様々な分かりやすいサンプルがあり、入門に最適です。 

 

 Take(n)

nに指定した分だけ、OnNextを発行するオペレータです。

using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;

public class TakeSample : Base {

    // Use this for initialization
    void Start () {
        gameObject.transform.position = new Vector2(0, 0.5f);

        //Takeで最初の100回以外はカット
        this.UpdateAsObservable()
            .Take(100)
            .Subscribe(l => Move(0.01f, 0));
    }

}

 このソースコード上で書かれたObservableを分解すると

 

  • this.UpdateAsObservable()

毎フレーム、次のオペレータ[Take(100)]に対してOnNextを発行する

  • .Take(100)

OnNextを100回だけ次のオペレータに発行する

  • Subscribe(l => Move(0.01f,0));

OnNextが来たら、Move(0.01f,0)を叩く

 

という記述がされています。

 

ここでさらに、記述されていない中身を付け足したソースコードがこちらです

using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;

public class TakeSample : Base {

    // Use this for initialization
    void Start () {
        gameObject.transform.position = new Vector2(0, 0.5f);

        //Takeで最初の100回以外はカット
        this.UpdateAsObservable()
            .Take(100)
            .Subscribe(l => Move(0.01f, 0),
() => OnCompleted());
} }

 

() => OnCompleted()がSubscribeに追加されています。

これは、Subscribeまでのストリームで、OnCompletedが発行された際に実行されます。

 

そして、今回のObservableでOnCompletedを発行するのは

Take(100)が100回OnNextを通した時です。

 

つまるところ、100回Move()したら、OnCompletedが呼ばれるということです。

 

ちなみに、OnCompletedはだいたいどのオペレータでも

  • try { observer.OnCompleted() }  finally { Dispose(); } ;

という処理になっていますので、OnCompletedを発行してエラーを吐いてもDisposeされるようになっています。

 

まとめ

Take(n)はnに指定した回数だけOnNextを通して、終了時にOnCompletedを発行する

 

First(Func),First()

OnNextが発行された最初の一度だけOnNextを発行します

using UnityEngine;
using System.Collections;
using UniRx;
using UniRx.Triggers;

public class First : Base
{

    // Use this for initialization
    void Start()
    {
        gameObject.transform.position = new Vector2(0, 1f);

        //クリックされたら右に1.5動かす(但し1回だけ)
        this.UpdateAsObservable()
            .First(l => Input.GetMouseButton(0))
            .Subscribe(l => Move(1.5f, 0));
        //こっちもやってること同じ
this.UpdateAsObservable()
.Where(_ => Input.GetMouseButton(0))
.First()
.Subscribe(l => Move(1.5f, 0));
} }

 このソースコード上で書かれたObservableをまた分解していきます

 

  • this.UpdateAsObservable()

毎フレーム、次のオペレータ[First(Func)]に対してOnNextを発行する

  • .First(Func) .First()

OnNextをFuncがtrueの時に、一度発行する

FuncがないFirst()は、OnNextを無条件で発行する

  • Subscribe(l => Move(0.01f,0));

OnNextが来たら、Move(0.01f,0)を叩く

 

という記述がされています。

 

First()は一度OnNextを発行すると、OnCompletedを発行します

単体では、Take(1)と同じ挙動をします

 

まとめ

First(),First(Func)は一回OnNextを発行し、その後OnCompletedを発行する

 

Take(1)とFirst()の違い

 ここまで読んで

「Take()は回数指定できる、First()は条件指定できるのね、へ〜」

 と思ったことでしょう。

 

それだけではないです、というかここからが重要です

端的に言うと

OnCompletedをオペレータ内で通す際の挙動に明確な違いがあります

 

TakeとFirstのOnCompletedメソッドをごらんください

  • Take
public class Take : OperatorObserverbase<T,T>{

public override void OnCompleted(){
try {observer.OnCompleted();} finally { Dispose(); }
}

}
  • First
public class First : OperatorObserverbase<T,T>{

public override void OnCompleted(){
if(notPublished){
try { observer.OnError(new InvalidOperationException("Sequence is empty"));}
finally { Dispose(); }
}
else{
try {observer.OnCompleted();} finally { Dispose(); }
}
}
}

 

こうなってます、全然違いますね。

 

一目見てなんだこれとなるのは、First()の方にあるnotPublishedというboolだと思います。

これはFirst()でOnNextが発行された時にfalseになるbool値です。

OnNextメソッドでもこれを使用していて、notPublishedの状態の時のみOnNextが次のオペレータに発行されるようになっています。

一度だけしか発行しない処理の中身はこうなっていました。

 

つまり

OnNextが一度も通っていないFirstオペレータはOnCompletedを通しません

こういう明確な差があるわけですね。

 

例として

public class First : Base
{

    // Use this for initialization
    void Start()
    {
        gameObject.transform.position = new Vector2(0, 1f);

        //クリックされたら右に1.5動かす(但し1回だけ)
        this.UpdateAsObservable()
.TakeWhile(_ => isEnabled)
.Where(_ => Input.GetMouseButton(0)) .First() .Subscribe(l => Move(1.5f, 0));
} }

このコードで

一度もGetMouseButton(0)がtrueにならないまま、isEnabledがfalseになり

OnCompletedが発行されると、FirstくんはErrorを吐きます。

 

 

まとめ

OnNextが発行されていなくてもOnCompletedを次のオペレータに通知できるのが

Take(1)

OnNextが発行されていないと、OnCompletedを次のオペレータに通知できないのが

First()

です。

 

もしOnCompletedを発行できるオペレータが複数あるストリームを組むときは

Take(1)を使用することをオヌヌメします。

 

終わり。

間違いなどがあったら@YutaDevelopまでお願いします。

 

unirxすげー