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

正如我们期望的那样,与过滤条件不符的信息都被剔除了,只输出满足过滤条件的流信息。

更多内容,欢迎访问


在这里插入图片描述

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