UniRx入门系列(二)

原文链接: https://qiita.com/toRisouP/items/851087b4c990d87641e6

上节回顾

在上一节中,解释了IObsrver的接口定义如下:

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

上一节中,只讲述了OnNext,下面我们将讨论一下在流执行过程中的OnError、OnCompleted和Dispose。

UniRX中的OnNext、OnError、OnCompleted

在UniRX中的对任意流的操作,最终都会转化为这三类中的任何一类,其具体用途如下:

  • OnNext 在事件发生时,发出通知,观察者会执行对应的订阅操作
  • OnError 处理流的过程中发出异常时发出通知
  • OnCompleted 当前流结束时,发出通知

OnNext是UniRX中最常用的操作,通常代表事件通知;即,当事件发生时,发出事件已经发出的通知

例子1: 整数通知(发布)

var subject = new Subject<int>();

subject.Subscribe(x => Debug.Log(x));

subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
subject.OnCompleted();

输出如下:

1
2
3

本例中,只是一个简单的整数通知,然后在订阅端通过Debug.Log()打印。

例子2:一个没有意义的值的通知(发布)

var subject = new Subject<Unit> ();
		subject.Subscribe (
			onNext:x => Debug.Log (x),
			onCompleted:()=>{Debug.Log("OnComplete");});
subject.OnNext(Unit.Default);

输出结果如下:

()

例子二中使用了一个Unit类型的特殊类型,这种类型表示当前信息内容是没有意义的。这对于事件的发布时机来说是很重要的,OnNext()中的内容在任何情况下都可以使用。比如,例子三中,可以利用在场景初始化完成时或者Player玩家死亡时。

例子三:以Unit作为传递值以通知(发布)场景初始化完成

public class UniRxUint : MonoBehaviour {
    private Subject<Unit> initialedSubject = new Subject<Unit> ();
	public IObservable<Unit> OnInitializedAsync => initialedSubject;

	void Start () {
		StartCoroutine(GameInitialitializeCoroutine());
		OnInitializedAsync.Subscribe(_=>{
			Debug.Log("场景初始化完成");
		});
	}
	IEnumerator GameInitialitializeCoroutine () {
		/*
		一些耗时的初始化处理,自行脑补
		 */
		yield return null;
		initialedSubject.OnNext (Unit.Default);
		initialedSubject.OnCompleted ();
	}
}

这种情况下,我们只需要发出场景初始化完成的通知,并不需要发布值,便可使用Unit来表示。

OnError

OnError,如名字一样,当在处理流的过程中发生异常时发出异常的通知。OnError可以在流中进行Catch处理(异常捕获),或者直接到达Subscribe方法,再进行处理。如果OnErro消息到达Subscribe,那么,流的订阅将会被终止并且销毁。

例子4:在Subscribe接收过程中发生错误

    var stringSubject = new Subject<string> ();
		stringSubject
			.Select (str => int.Parse (str))
			.Subscribe (
				onNext: v => { Debug.Log ("转换成功:" + v); },
				onError : ex => { Debug.Log ("转换失败: " + ex); }
			);
	stringSubject.OnNext ("1");
	stringSubject.OnNext ("2");
	stringSubject.OnNext ("100");
	stringSubject.OnNext ("Hello");
	stringSubject.OnCompleted ();

输出结果如下:

成功:1
成功:2
成功:100
转换失败:System.FormatException: Input string was not in a correct format.

在示例4中,OnNext发出的字符串被Select(选择或者转换)操作符解析并打印出Int类型的流;通过OnError,就可以在处理流的过程中,发生异常时,便可知道得到异常的细节。如果流收到异常之后没有被处理,那么当前流就会被终止。

例子5:处理流过程中发生异常,则重新订阅(发布)

var stringSubject = new Subject<string> ();
		stringSubject
			.Select (str => int.Parse (str))
			.OnErrorRetry ((FormatException ex) => {
				Debug.Log ("本次转换失败 :" + ex);
			})
			.Subscribe (
				onNext: v => { Debug.Log ("转换成功:" + v); },
				onError : ex => { Debug.Log ("转化失败: " + ex); }
			);
		stringSubject.OnNext ("1");
		stringSubject.OnNext ("2");
		stringSubject.OnNext ("100");
		stringSubject.OnNext ("Hello");
		stringSubject.OnNext ("250");
		stringSubject.OnNext ("300");
		stringSubject.OnNext ("550");
		stringSubject.OnCompleted ();

在示例5中,在处理流的过程中,出现了异常,使用OnErrorRetry对流进行重建并继续订阅。当前流并没有被终止,而是继续想传递。OnErrorRetry是一个异常处理操作符,当OnError是一个特定的异常时,当前流从Subscribe重新开始,即Subject重新注册IObserver.

一些可用的异常处理操作符

  • Retry 当OnError被触发时,Retry会再次重试。
  • Catch 捕获错误,进行错误处理,将其替换为另外一个流
  • CatchIgnore 捕获错误并处理,将OnErroe转换为OnCompleted
  • 捕获错误处理后,重新Subscribe

OnCompleted

OnCompleted 当流完成时发出通知,并且之后不再发出通知。如果OnCompleted消息到达Subscribe,和OnErrod一样,该流的订阅将会被终止和销毁。因此,可以向流发出OnCompleted来终止流的订阅,同样,也可以用此方法来清理流。

例子6:检测OnCompleted

 Subject<string> stringSubject = new Subject<string>();

        stringSubject.Subscribe(
            onNext: x => Debug.Log(x),
            onCompleted: () =>
            {
                Debug.Log("OnCompleted");
            });

输出如下:

1
2
OnCompleted

在Subscribe的重载方法中定义OnNext、OnCompleted

Subscribe的重载方法

我们之前介绍的Subscribe实际上有多个重载方法,你可以根据你的事件流选着满足你要求的重载方法,如下:

  • Subscribe(IObserver observer) 最基本的订阅,参数代表观察者对象
  • Subscribe() 不对信息做任何处理
  • Subscribe(Action onNext, Action onError) 传递流,并处理异常
  • Subscribe(Action onNext, Action onCompleted)传递流,流被终止时发布消息
  • Subscribe(Action onNext, Action onError, Action onCompleted)传递流,并处理操作流过程中的各种信息

终止流,结束订阅

接下来,我们解释一下IObservable “IDisposable”

public interface IObservable<T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

IDisposable是C#中的一个接口,有一个"Dispose"方法,用于对资源的释放。

namespace System
{
    public interface IDisposable
    {
        void Dispose();
    }
}

如果Subscribe的返回值是IDisposable,那么就可以终止流的订阅,并释放流。

例子7:使用Dispose结束流的订阅

void Start()
    {
        var subject = new Subject<int>();
        var disposable = subject.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted"));

        subject.OnNext(1);
        subject.OnNext(2);

        disposable.Dispose();

        subject.OnNext(100);

        subject.OnNext(10);

        subject.OnCompleted();
    }

输出如下:

1
2

如上,可以通过调用Dispose来终止订阅。这里需要注意一点,如果使用Dispose来终止流的订阅,那么OnCompleted将不会被出发。所以,如果你在OnCompleted中写了停止流时的一些触发处理,那么使用Dispose释放流之后,是不会运行的。

例子8:只终止(释放)特定的流

void Start(){
   var subject = new Subject();
        var disposable1 = subject.Subscribe(
            onNext: x => Debug.Log("Disposable 1:" + x),
            onCompleted: () => Debug.Log("OnCompleted: 1"));
        var disposable2 = subject.Subscribe(
            onNext: x => Debug.Log("Diaposable 2:" + x),
            onCompleted: () => Debug.Log("OnCompleted: 2"));

        subject.OnNext(1);
        subject.OnNext(2);
        //释放第一个流
        disposable1.Dispose();
        //第二个流未被释放,继续传递
        subject.OnNext(3);
        subject.OnCompleted();
}

流的生命周期和Subscribe终止时间

在使用UniRX的过程中,时刻注意流的生命周期是非常必要的。频繁创建和删除对象会导致应用性能下降。

是什么控制着流的流动和传递

在对流进行生命周期管理时,你需要意识到,是什么在控制着流的传递,是什么控制着流。事实上,流的实体是Subject,如果这个Subject被销毁,那么,当前流也会被销毁和终止。之前说过,Subscribe是指在Subject上注册的响应订阅的处理函数。也就是说,在在Subject的内部保留着调用函数的列表(以及与该函数相连的方法链)。这也说明了,Subject是流的管理对象。一旦Subject被全部销毁或者终止,那么流也会被销毁和终止。反过来说,只要Subject继续存在,流就还会继续运转。如果你在流开始传递前丢弃流中需要引用的对象,那么流可能会继续往下传递,从而导致应用性能下降,引起内存泄漏,或者应用直接抛出空异常。所以,在使用流的时候,需要特别细心,一定养成不使用的流,及时Dispose或者OnCompleted;

例子9:使用UniRX中的事件通知重写Player的坐标

假设有一个动作游戏,游戏思路如下:

  • 有一个Player可以操控
  • 给定一个计时器(倒计时)
  • 当倒计时结束时,将Player的坐标重置为起点坐标
  • Player如果超出给定范围,就会被杀死

倒计时:

public class TimeCounterUniRX : MonoBehaviour
{
    private Subject<int> timerSubject = new Subject<int>();
    public IObservable<int> OnTimeChanged => timerSubject;
    void Start()
    {
        StartCoroutine(TimerCoroutine());
        timerSubject.Subscribe(x => Debug.Log(x));
    }
    IEnumerator TimerCoroutine()
    {
        var time = 10;
        while (time >= 0)
        {
            timerSubject.OnNext(time--);
            yield return new WaitForSeconds(1);
        }
        timerSubject.OnCompleted();
    }
}

Player玩家:

public class Player : MonoBehaviour
{
    public TimeCounterUniRX timeCounterUniRX;
    public float moveSpeed = 10.0f;

    void Start()
    {
        timeCounterUniRX.OnTimeChanged
         .Where(x => x == 0)
         .Subscribe(_ =>
         {
             transform.localPosition = Vector3.zero;
         });
    }
    void Update()
    {
        var xzValue = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        if (xzValue.magnitude > 0.1f)
        {
            transform.localPosition += xzValue * moveSpeed * Time.deltaTime;
        }
        if (transform.localPosition.x > 10)
        {
            Debug.Log("Game Over");
            Destroy(this.gameObject);
        }
    }
}

当计时器到达0时,如果,Player未被销毁。Player坐标正确的覆盖到初始位置。如果在计时器未到达0时,Player被销毁(使其position.x在10秒内>10);当计时器器到达0时,会抛出:MissingReferenceException: The object of type ‘Player’ has been destroyed but you are still trying to access it.,也就是说,当计时器到达0时,我们才发现,Player已经不存在了。

原因何在呢?

看下面这段代码:

 void Start()
    {
        timeCounterUniRX.OnTimeChanged
         .Where(x => x == 0)
         .Subscribe(_ =>
         {
             transform.localPosition = Vector3.zero;
         });
    }

正如我们之前说过的那样,流的管理者是Subject,由Subject维持着流的传递和运转,就算Player被Destroy销毁,流依然由TimeCounterUniRX中的Subject维持,所以流依然继续保持,当流满足限定条件时,会访问订阅者的对象,但是,订阅者的对象已经被销毁了,所以才会引发空引用异常。结论就是,如果流的生命周期和对象的生命周期不一致,就会导致对象行为出现异常。

如何处理这种情况

处理的方法很简单,那就是当Player对象被销毁时,终止订阅流程就可以了。UniRX提供了多种终止并释放流的方式,下面的例子展示一个最简单的AddTo的使用方式。

void Start()
    {
        timeCounterUniRX.OnTimeChanged
         .Where(x => x == 0)
         .Subscribe(_ =>
         {
             transform.localPosition = Vector3.zero;
         }).AddTo(gameObject);
    }

使用了AddTo方法指定当前流的生命周期和this.gameobject的生命周期一致,这样一来,便不会出现之前销毁Player对象之后,任然抛出MissReferenceException的问题了。即当Player被销毁之后,当前流的订阅也会被停止。

总结

在流的执行过程中,有三种类型的信息传递:

  • OnNext 在事件触发时发出通知消息
  • OnError 在处理流的过程中抛出异常
  • OnCompleted 流结束时发布消息(通知)

停止订阅流的方式:

  • Subscribe的OnCompleted,检测OnCompleted是否被触发
  • Subscribe的OnError,执行流过程中,触发异常
  • Subscribe返回的IDisposable 的Dispose方法

流的生命周期和对象生命周期之间的关系:

  • 流的管理者是Subject,流的运转依赖于Subject
  • 使用流的过程中,养成自动Dispose或者OnCompleted的习惯去释放或者终止流,否者会造成内存泄漏或者抛出其它异常信息。

更多内容,欢迎访问:


UniRx入门系列(二)_第1张图片

你可能感兴趣的:(UniRx)