UniRx入門系列(一)

UniRX簡介(一)


之前我曾今零星的分享過一些關於UniRX的文章,但是這些文章並沒有形成一個系統可以學習的形式,所以我決定寫一系列的文章,分幾個章節,這樣就可以系統性的學習UniRX.

什麼是UniRX


UniRX 是neuecc爲Unity開發的一個Reactive Extensions程序庫

什麼是Reactive Extensions,概要如下:

  • MicrosoftResearch開發的面向C#的異步處理庫
  • 基於設計模式中的Observer(觀察者)模式設計的
  • 用於描述與時間、執行定時等相關的操作
  • 完成度較高,已被移植到各大主流的編程語言中(Java、JavaScript和Swift中)

UniRX這個基於ReactiveExtension移植到Unity的庫,和.NET 版本的RX相比,有如下區別:

  • UniRX基於Unity的編程思想進行過優化
  • 基於Unity的開發習慣增加了一些方便的功能和操作符
  • 增加了ReactiveProperty等響應式屬性
  • 相比原來的.NET RX 內存性能更好

C#中的event和UniRX


我們在寫C#程序時,會經常使用的一個功能——event,Event用於在某個時刻發出消息並讓其在另外一個地方處理的功能。

using UnityEngine;
using UniRx;
using System.Collections;
public class TimeCounter : MonoBehaviour
{
    public delegate void TimerEventHandler(float time);
    public event TimerEventHandler OnTimeChanged;
    void Start()
    {
        StartCoroutine(TimerCoroutine());
    }
    IEnumerator TimerCoroutine()
    {
        var time = 100.0f;
        while (time > 0)
        {
            time -= Time.deltaTime;
            OnTimeChanged(time);
            yield return null;
        }
    }
}

事件訂閱方,執行對應的事件操作

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class TimeView : MonoBehaviour
{
    public TimeCounter timeCounter;
    public TextMeshProUGUI textMeshProUGUI;
    void Start()
    {
        timeCounter.OnTimeChanged += (time) =>
        {
            textMeshProUGUI.text = time.ToString("F3");
        };
    }
}

以上代碼是一個倒計時的操作,事件發出方,將當前剩餘時間對外廣播,訂閱了這個事件的接收方,接收這個事件,並在UGUI文本中顯示當前剩餘時間。

UniRX


之前之所以提到C#中的event,是因爲UniRX與event是完全兼容的,且相比event來說,UniRX更加靈活。我們利用UniRX重寫剛纔的代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using System;
public class TimeCounterUniRX : MonoBehaviour
{
    private Subject<float> timerSubject = new Subject<float>();
    public IObservable<float> OnTimeChanged => timerSubject;
    void Start()
    {
        StartCoroutine(TimerCoroutine());
    }
    IEnumerator TimerCoroutine()
    {
        var time = 108.0f;
        while (time > 0.0f)
        {
            time -= Time.deltaTime;
            timerSubject.OnNext(time);
            yield return null;
        }
        timerSubject.OnCompleted();
    }
}

事件訂閱方

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using TMPro;
public class TimeViewUniRX : MonoBehaviour
{
    public TimeCounterUniRX timeCounterUniRX;
    public TextMeshProUGUI textMeshProUGUI;
    void Start()
    {
        timeCounterUniRX.OnTimeChanged
        .Subscribe(time =>
        {
            textMeshProUGUI.text = time.ToString("F3");
        });
    }
}

上面的代碼用UniRX替換了之前在event中實現的代碼。

在註冊事件處理程序時出現了一個名爲Subject的類,而不是event;所以,我們可以看出Subject是UniRX的核心,通過Subject和訂閱者連接起來。接下來,我們將詳細介紹《OnNext和Subscribe》。

UniRX中的《OnNext和SubScribe》

OnNext和Subscribe都是在Subject中實現的方法,它們具有如下行爲。

  • Subscribe 在接收到消息時執行註冊的函數
  • OnNext 將收到的消息傳遞給Subscribe註冊的函數並執行

看一下下方的示例代碼:

using System.Text;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
public class TimeTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Subject<string> subject=new Subject<string>();

        subject.Subscribe(msg=>Debug.Log("Subscribe1:"+msg+"\n"));
        subject.Subscribe(msg=>Debug.Log("Subscribe2:"+msg+"\n"));
        subject.Subscribe(msg=>Debug.Log("Subscribe3:"+msg+"\n"));

        subject.OnNext("hello 001");
        subject.OnNext("hello 002");
        subject.OnNext("hello 003");
    }
}

輸出結果如下

Subscribe1:hello 001
Subscribe2:hello 001
Subscribe3:hello 001

Subscribe1:hello 002
Subscribe2:hello 002
Subscribe3:hello 002

Subscribe1:hello 003
Subscribe2:hello 003
Subscribe3:hello 003

如上所述,你可能會發現,Subscribe是接收到消息時執行相對應的操作,OnNext是發出消息後,按順序調用Subscribe註冊的操作。


在這裏插入圖片描述

上圖描述了Subject的的行爲,解釋了UniRX的基本動作機制以及核心思想;下一部分中,我們將要討論一個更加抽象的話題,IObserver接口和IObservable接口。

UniRX中的IObserver和IObservable

在Subject中實現了OnNext和Subscribe兩個方法;這樣說其實不併太準確,更確切地說,Subject實現了IObserver接口和IObservable兩個接口

UniRX中的IObserver接口


IObserver是一個接口,定義了RX中可以發佈事件消息的行爲,其定義如下:

using System;
namespace UniRx
{
    public interface IObserver<T>
    {
        void OnCompleted();
        void OnError(Exception error);
        void OnNext(T value);
    }
}

如你所見,IObserver非常簡單,只簡單的定義了三個方法,即OnNext、OnError、OnCompleted。之前使用的OnNext方法實際上就是IObserver中定義的方法。

OnError則是當發生錯誤時,發出發生錯誤的通知,OnCompleted在消息發佈完成之後觸發。

UniRX中IObservable接口

IObservable接口定義了可以訂閱事件消息的行爲

using System;
namespace UniRx
{
    public interface IObservable<T>
    {
        IDisposable Subscribe(IObserver<T> observer);
    }
}

IObservable只簡單的定義了一個Subscribe方法。

現在我們看一下剛纔用到的Subject類的定義。

namespace UniRx
{
    public sealed class Subject<T> : ISubject<T>, IDisposable, IOptimizedObservable<T> {/*省略具體實現*/}
}

Subject實現了ISubject接口,ISubject接口定義如下:

namespace UniRx
{
    public interface ISubject<TSource, TResult> : IObserver<TSource>, IObservable<TResult>
    {
    }
    public interface ISubject<T> : ISubject<T, T>, IObserver<T>, IObservable<T>
    {
    }
}

可以看到Subject實現了IObservable和IObserver兩個接口,也就是說Subject具有發佈值和可以訂閱值兩個功能。
如下圖:


在這裏插入圖片描述

事件的過濾、篩選

假設有如下實現:

void Start()
    {
        Subject<string> subjectStr = new Subject<string>();
        subjectStr.Subscribe(x => Debug.Log(string.Format("Hello :{0} ", x)));

        subjectStr.OnNext("Enemy");
        subjectStr.OnNext("Wall");
        subjectStr.OnNext("Wall");
        subjectStr.OnNext("Enemy");
        subjectStr.OnNext("Weapon");
    }

輸出如下:

Hello :Enemy 
Hello :Wall 
Hello :Wall 
Hello :Enemy 
Hello :Weapon 

現在,我們的需求是,我們只想要輸出Enemy,其它的消息需要被過濾掉(不輸出);使用傳統的編程思路,我們就得老老實實的在Subscribe的回調函數中寫if條件判斷語句,但是這樣就失去了使用UniRX的意義。

是否可以在Subject和Subscribe之間加上過濾操作呢?

如之前所說,處理髮出值的對象被定義爲IObserver(觀察者),訂閱值的處理被定義爲IObservable(被觀察者),這意味着我們可以將兩個實現類夾在Subject和Subscribe之間,然後在Subscribe中實現具體的細節。現在,我們嘗試使用Where操作符過濾以下信息。


在這裏插入圖片描述

 void Start()
    {
        Subject<string> subject = new Subject<string>();

        subject
        .Where(x => x == "Enemy")
        .Subscribe(x => Debug.Log(string.Format("Hello : {0}", x)));

        subject.OnNext("Wall");
        subject.OnNext("Wall");
        subject.OnNext("Enemy");
        subject.OnNext("Enemy");
    }

輸出如下所示:

Hello :Enemy
Hello : Enemy

通過Where操作符,就可以在IObservable和IObserver之間,對信息進行過濾。

UniRX中的操作符

在UniRX中除了Where還有許多的操作符,這裏介紹其中的一部分:

  • Where 過濾操作符
  • Select 轉換操作符
  • Distinct 排除重複元素操作符
  • Buffer 等待緩存一定的數量
  • ThrottleFirst 在給定條件內,只使用最前面那個
    上述操作符只是衆多操作員中的一小部分,UniRX提供了許多處理流的操作符,之後會出一個彙總的表格。

目前,我們使用”Subject、Subscribe“和"Subject 組合操作符來表達UniRX的執行過程。這種描述方式其實並不好,所以我們會用形容這個過程。

在UniRX中,用流來表達從發出消息到Subscribe的一系列處理流程;組合操作符、構建流,執行Subscribe,啓動流,傳遞流、發佈OnCompleted、停止流等等。

流是整個UniRX的核心,以後我們會經常使用流這個詞,所以需要熟悉他。

總結

  • UniRX的核心是Subject
  • Subject和Subscribe是UniRX中的核心
  • 實際使用時需要注意IObserver和IObservable兩個接口
  • 利用操作符使事件的發佈和訂閱分離,使得事件的處理更加靈活。
  • 使用操作符連接的一系列事件處理稱之爲流
    在下一個系列中,我將和大家解釋一下OnError、OnCompleted、Dispose等等。

寫一個自己自定義的操作符

之前曾經說過,在有IObserver和IObservable類的Subject和Subscribe之間,通過過濾操作符,寫入過濾條件,即可對流進行過濾。那麼我們可以定義自己的操作符嗎?答案是肯定的,下面我們將自己定義一個具有相同過濾行爲的自定義過濾操作符。

using System;
public class MyFilter<T> : IObservable<T>
{
    private IObservable<T> _source { get; }
    private Func<T, bool> _conditionalFunc { get; }

    public MyFilter(IObservable<T> source, Func<T, bool> conditionalFunc)
    {
        _source = source;
        _conditionalFunc = conditionalFunc;
    }
    public IDisposable Subscribe(IObserver<T> observer)
    {
        return new MyFilterInternal(this, observer).Run();
    }
    private class MyFilterInternal : IObserver<T>
    {
        private MyFilter<T> _parent;
        private IObserver<T> _observer;
        private object lockObject = new object();

        public MyFilterInternal(MyFilter<T> parent, IObserver<T> observer)
        {
            _observer = observer;
            _parent = parent;
        }
        public IDisposable Run()
        {
            return _parent._source.Subscribe(this);
        }

        public void OnCompleted()
        {
            lock (lockObject)
            {
                _observer.OnCompleted();
                _observer = null;
            }
        }

        public void OnError(Exception error)
        {
            lock (lockObject)
            {
                _observer.OnError(error);
                _observer = null;
            }
        }

        public void OnNext(T value)
        {
            lock ((lockObject))
            {
                if (_observer == null) return;
                try
                {
                    if (_parent._conditionalFunc(value))
                    {
                        _observer.OnNext(value);
                    }
                }
                catch (Exception e)
                {
                    _observer.OnError(e);
                    _observer = null;
                }
            }
        }
    }
}

其中,OnNext是比較重要的一個部分。

    if (_parent._conditionalFunc(value))
        {
            _observer.OnNext(value);
        }

這裏判斷你自己定義的條件是否滿足,滿足就繼續向流中傳遞值。

使用擴展方法,增強自定義操作符的可用性

按照現在的流程,我們需要每一次使用自定義操作符時,首先需要進行實例化操作,使用起來並不方便。爲了能夠以流式結構來使用MyFilter自定義過濾操作符,使用如下擴展方法來定義:

using System;
public static class ObservableOperators
{
    public static IObservable<T> FilterOperator<T>(this IObservable<T> source, Func<T, bool> conditionalFunc)
    {
        return new MyFilter<T>(source, conditionalFunc);
    }
}

接下來,讓我們看一下如何使用我們自己定義的這個操作符吧:

 // Start is called before the first frame update
    void Start()
    {
        /// <summary>
        /// 自定義的過濾操作符
        /// </summary>
        /// <typeparam name="string"></typeparam>
        /// <returns></returns>
        Subject<string> subject = new Subject<string>();
        subject.FilterOperator(x => x == "Enemy")
        .Subscribe(x => Debug.Log(string.Format("Hello :{0}", x)));

        subject.OnNext("Wall");
        subject.OnNext("Wall");
        subject.OnNext("Enemy");
        subject.OnNext("Weapon");
        subject.OnNext("Player");
        subject.OnNext("Enemy");
    }

輸出如下:

Hello :Enemy
Hello :Enemy

正如我們期望的那樣,與過濾條件不符的信息都被剔除了,只輸出滿足過濾條件的流信息。

更多內容,歡迎訪問


在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章