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
正如我們期望的那樣,與過濾條件不符的信息都被剔除了,只輸出滿足過濾條件的流信息。
更多內容,歡迎訪問