UniRx(1)——简介

目录

  • 前言
    • 响应式编程
  • 可观察序列
  • 总结

前言

UniRx,顾名思义就是Unity上的ReactiveX(Rx)。Unity就很好理解了,可是这个Rx又是哪路神仙?找到Rx的官方简介,大概意思是:ReactiveX是一个通过可观察序列来综合异步编程事件编程

这句话回答了很多问题:首先它是一个库,其次它的作用是简化异步编程和事件编程,使用的手段是可观察序列。

于是,我们现在已经对Rx有了一个比较模糊的认识,现在我们再来看看UniRx的官方定义:UniRx(Reactive Extensions for Unity)是.Net Reactive Extensions(Rx.Net)的一个重新实现,因为Rx.Net不兼容Unity和iOS。这个库修复了Rx.Net的一些Bug,增加了一些针对Unity的功能,并且支持更多的平台。

综上所述,我们可以这么理解:UniRx是在Unity上实现的增强版Rx。

鉴于面向对象是当下的主流,在开始之前,给你一点忠告:
请暂时忘掉面向对象的思维模式。
请暂时忘掉面向对象的思维模式。
请暂时忘掉面向对象的思维模式。

响应式编程

在具体介绍UniRx之前,需要先引入一个新的编程范式——响应式编程(RP),一种和面向对象有异曲同工之妙的思维模式。在这里,所有东西都是数据流,数据流之间可以通过相互观察来建立依赖关系,从而相互协作,完成工作。这里的观察的语义和观察者模式里的观察相同,RP本身甚至都可以视为观察者模式的一种高阶形态。

乍一看RP和OOP很相似,好像只是换了一套术语而已。可是,当我们细品这些术语时,就会发现:RP怎么这么反人类???关系全都是反的。在OOP中,A依赖B的含义是:A调用B;而在RP中,当A观察B时,反而是B调用A。在OOP中我们讲究依赖倒置,上层模块不应该依赖下层模块而应该依赖抽象;而在RP中根本就没有“抽象”这个概念,具体的数据流之间明显存在着依赖关系。

以上这些事实让我们意识到一个严重的问题:RP和OOP虽然表面上很像,但内在的区别可远不止换了一套术语这么简单,非常容易混淆。所以我说在学习RP之前一定要摒弃OOP,有意识地将这两种思维模式隔离开来,否则你真的会走火入魔的。

OOP和RP的根本区别在于看问题的方式不同。OOP中的对象更像一个领导,大多数对象不会去接触底层工作,只是想着谁能完成工作我就去找谁。所以对象看问题的方式是:我找谁。而RP中的数据流大多本身就有业务逻辑,所以数据流更关心如何推销自己,谁需要我的功能我就去告诉他可以帮他。所以数据流看问题的方式是:谁找我

这两种方式其实并没有优劣之分,它们的目标是相同的:通过让软件的行为更符合人类的思维来降低软件开发的难度。在现实中,根据情境不同,确实有时候需要侧重我找谁,有时候需要侧重谁找我。你口渴了总不能等着水自己飞到你嘴里去吧,你要么自己去找水(依赖查找),要么让别人给你拿水来(依赖注入)。无论如何,你想的都是我找谁。但是在求职的时候就不同了,你会向企业推销自己,告诉他们你有能力胜任工作,当他们缺人手的时候就来找你。在这种情况下,你想的就是谁找我

可观察序列

说了这么多,也该回到正题了。根据Rx的定义,我们不难发现它就是一个响应式编程环境。上面也说了,有些问题对于OOP来说非常棘手,而RP存在的意义,正是弥补OOP在这些场景中缺陷。

我以一个简单的小练习开头:实现一个函数,用户每次双击左键就在控制台打印一个1。

这是传统的OOP(或者说POP)实现:

public class DoubleClick : MonoBehaviour
{
	private float time; // 距离上一次点击的时间
	private float threshold = 0.5f; // 触发双击的时间阈值
	private int buffer; // 已点击次数
	private void Update()
	{
		if (Input.GetMouseButtonUp(0))
		{
			if (buffer == 0)
			{
				time = 0;
				buffer = 1;
			} else if (buffer == 1)
			{
				Debug.Log(1);
				buffer = 0;
			}
		}
		if (time < threshold)
		{
			time += Time.deltaTime;
		} else if (buffer == 1)
		{
			buffer = 0;
		}
	}
}

一个简单的双击居然写了这么一大坨代码,我吐了。下面再来看看RP的实现:

public class DoubleClickRx : MonoBehaviour
{
	private void Awake()
	{
		Observable.EveryUpdate() // 每帧发送数据
				  .Where(x => Input.GetMouseButtonDown(0)) // 筛选出鼠标点击了左键的帧
				  .Buffer(TimeSpan.FromSeconds(0.5f), 2) // 缓冲,超过0.5秒或点击两次发送才发送数据
				  .Where(list => list.Count == 2) // 筛选出点击两次的缓冲
				  .Subscribe(x => Debug.Log(1)) // 打印
				  .AddTo(this); // 绑定生命周期
	}
}

???????
???????
???????

UniRx(1)——简介_第1张图片

WDNMD这是什么鬼东西,完全看不懂呀!

上面这一条调用链其实就是对可观察序列(以下称为流)的操作了。首先介绍一下流:流(Stream)又被称为时变变量(Time-Varing Variable),它和普通变量的最大区别就是它会自动改变。

注:可观察性并非流本身的性质,因此这里的流其实指的是响应式流,但为了书写方便还是简称为流。

考虑这样一段代码:

int x = 1;
int y = 2;
int z = x + y;

在这里,xyz都是普通的整形变量,如果x在某个时刻变成了3,z不会发生任何变化。这在大多数情况下是没问题的,但有些时候我们希望z能够跟着x和y一起变,比如z是总攻击力,x和y分别是基础攻击力和额外攻击力,那么当x或y变化时,z理应立即随之改变。为了解决这类问题,流应运而生。当把代码改成这种形式时,z就会随着x和y变化了:

var x = new ReactiveProperty<int>(1);
var y = new ReactiveProperty<int>(2);
var z = x.Value + y.Value;
x.Subscribe(newX => z = newX + y.Value);
y.Subscribe(newY => z = x.Value + newY);

ReactiveProperty就是一种具体的流,它会在自身的值被改变时向外发送数据通知它的订阅者进行更新。这里z同时订阅了x和y,因此在x和y中的任意一个发生改变时,z都会立即修正自己的值。

注:在本专栏中,发送数据、发送消息、发送事件的语义相同,订阅、观察、注册的语义相同。

流的作用远不止于此,UniRx提供了大量的操作符来对流进行变换,并且这些操作符可以进行组合,这直接早就了响应式编程无限的可能性。比如,Where操作符可以从源流中产生出一条新流,比如我们可以对上面的x进行变换:

var x_positive = x.Where(newX => newX > 0);

我们得到一个新的流x_positive,这个流会过滤掉x发送的一些消息。在这里,只有x变为整数的时候,x_positive才会发送消息。因此,如果我们让z订阅x_positive而不是x,就能实现这样的效果:当x变为非正数时,z值不变;当x变为正数时,z才跟着改变。当然,x_positive的存在不会影响x本身,如果z仍然订阅x,也能正常运行。

到这里,我想你应该大致理解流是如何工作的了,让我们回到双击检测的流式实现上:首先,我们调用Observable.EveryUpdate来创建第一个流,这个流会在每次Unity执行LateUpdate之后(敲黑板)发送一条数据,这数据显然有点太多了。于是,我们使用Where操作符进行过滤,只保留点击了左键的帧。之后我们使用一个Buffer操作符来缓存近期的点击事件,这个操作符会在超时或缓冲区满时一次性发送缓冲的所有事件,这里我们设置超时时间为0.5秒,缓冲区大小为2。紧接着又是一个Where操作符,由于Buffer发出的是一个消息的列表,因此我们可以通过列表的长度来判断用户点击的次数:如果缓冲由于超时而发送事件,那么列表的长度小于2;如果缓冲因为缓冲区满而发送事件,那么列表的长度等于2。因此,列表长度等于2的事件即为双击事件。于是我们订阅最终的那条流,设置回调为打印数字。每当双击检测通过,那条流就会发送事件,打印方法就会被调用,问题完美解决。

现在,你是不是觉得响应式编程碉堡了!没毛病,我也这么觉得。但是强归强,我们还是要有一个理性的认识。我见过一些人,他们在学了RP后就各种无脑吹。RP牛逼!RP无敌!RP天下第一不接受反驳!OOP就是个辣鸡!

Emm……我只想说,没有最强,只有最合适,不看业务场景的技术选型全是耍流氓。虽说RP能干净利落地解决复杂的交互问题,但连底层的数据模型跟服务调用都要用RP那就是纯属吃饱了撑的。

总结

响应式编程的本质就是通过操作流来完成功能。流正如它的名字那样,像一条河流,新的数据源源不断地流过来,每当有新数据流过来时,它就会通知订阅者进行更新。操作符就像是河流的分支,它从干流中派生出来,拥有不同的数据、流量等,却不会影响干流的存在。

流本身并不是什么新鲜东西,因为早在上个世纪就有了观察者模式,ReactiveX的标准也仅仅是几个接口而已。响应式编程的最大亮点其实是操作符,操作符能够创建无数的支流,使原本的一条直线变成一张复杂的网络,这就为我们带来了无限的可能。

现在,我们与响应式编程之间最大的隔阂——思维模式,已经消除了。但只有思维模式还远远不够,要想真正驾驭UniRx,还需要将它提供的上百种操作符烂熟于心,这可不是啥轻巧活!从下回开始,就是漫长的背书过程了。

以下是RP妹妹的经典语录:自打我进宫以来呀,就独得皇上恩宠~这后宫佳丽三千,皇上就偏偏宠我一人,于是我就劝皇上一定要雨露均沾,可皇上非是不听呐。皇上啊,就宠我,就宠我,你说这叫为奴的情何以堪呀!

你可能感兴趣的:(#,UniRx研究所)