一、简介
如今,函数响应式编程成为越来越受Swift开发商欢迎的编程方法。原因很简单,它能使复杂的异步代码容易地编写和理解。
在这篇文章中,我要比较GitHub上提供的两个***的函数响应式编程库——RxSwift与ReactiveCocoa。
首先,我们将简要回顾什么是函数响应式编程,然后详细比较这两个框架。本文结束时,你会决定哪一个框架更适合你!
二、什么是函数响应式编程(FRP)
甚至在苹果公司宣布Swift语言之前,函数响应式编程在近几年已人气剧增,从而与面向对象编程形成鲜明对比。从Haskell到Javascript,你都能够发现其中包含着受函数响应式编程启发而存在的支持。这是为什么?函数响应式编程提供了什么特别的支持?也许最重要的是,你该怎样在Swift编程中使用这一技术?
FRP是一种由Conal Elliott创建的编程范式。他定义了有关FRP的非常具体的语义(请参考https://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming)。为了实现更简洁的定义,FRP组合了另外两个功能概念:
l 响应式编程(Reactive Programming):侧重于异步数据流,你可以监听这种数据流并迅速做出相应的反应。要了解与之相关的更多信息,请查阅https://gist.github.com/staltz/868e7e9bc2a7b8c1f754。
l 函数式编程(Functional Programming):强调通过数学语言风格的函数、不变性和表现力来实现计算,并最小化变量和状态的使用。请参阅百度百科(http://baike.baidu.com/link?url=oTiEJsaX5ibGvK3R-BK6J55dGOXf3-Zzn5e1uBpvWDx4AjCV8ykQtRWz3RmuUefjD6IzL_QZcyedkvMB8sEyhK)进一步了解这种编程范式。
(一)一个简单的例子
当然,理解上述概念的最容易的方式是通过一个简单的实例加以说明。现在,我们来构建一个小程序,它将实现用户位置的定位功能,并当该用户靠近一家咖啡馆时及时地通知他。
如果你想使用函数响应式编程来开发上述程序,你需要:
1. 创建一个能够发出你能够进行响应的位置事件的信息流。
2. 然后,你要对位置事件进行过滤,以便确定哪些位置事件对应于靠近咖啡馆这种信息,然后发送相应的警告。
使用ReactiveCocoa编程实现上述功能的代码大致如下所示:
- locationProducer // 1
- .filter(ifLocationNearCoffeeShops) // 2
- .startWithNext {[weak self] location in // 3
- self?.alertUser(location)
- }
下面作简要分析。
1. locationProducer负责每当位置改变时发出一个事件。注意:这在ReactiveCocoa编程中称作“信号”(signal),而在RxSwift中称作“顺序”(sequence)。
2. 然后,使用函数编程技术来响应位置更新。filter方法执行与数组上过滤操作完全相同的功能,负责把每个值传递给函数ifLocationNearCoffeeShops。如果该函数返回true,则该事件被允许进行下一步处理。
3. ***,startWithNext形成对过滤后信号的订阅。于是每当事件到达,闭包中的表达式都被执行。
上面的代码看上去与转换值数组的代码极其相似。但是这里有一点特别“聪明”——这段代码是异步执行的;随着位置事件的发生filter函数和闭包表达式被相应地调用。
当然,你可能会对其所用语法感到有点奇怪,但希望这段代码的基本意图你应该清楚。这就是函数式编程的美丽所在:它是声明性语言。它向你展示发生了什么,而不是如何能做的细节实现。
(二)事件转换
在上面的定位示例中,你才刚刚学会如何观察(observe)流,除了过滤出咖啡馆附近位置信息外,你其实并没有对这些事件做更多的事情。
FRP的另一个基本点是把这些事件组合一起并把它们转换为有意义的内容。为此,你要使用(但不限于)高阶函数。
不出所料,你会在Swift函数式编程中经常遇到如下内容:map、filter、reduce、combine和zip。
让我们修改一下上面的定位示例,以便跳过重复的位置信息而把传入的位置信息(对应于CLLocation结构)转换成更富人性化的消息。
- locationProducer
- .skipRepeats() // 1
- .filter(ifLocationNearCoffeeShops)
- .map(toHumanReadableLocation) // 2
- .startWithNext {[weak self] readableLocation in
- self?.alertUser(readableLocation)
- }
下面让我们来分析一下上面新添加的两行代码:
1. 首先对通过locationProducer发出的信号应用skipRepeats操作。注意,这种操作并没有模拟数组的意思,而是ReactiveCocoa特有的。其执行的函数功能是很容易理解的:过滤掉重复的事件。
2. 在执行过滤函数后,调用map来把事件数据从一种类型转换成另一种类型,有可能从CLLocation转换成String。
至此,你应该了解了FRP编程的优点:
l 简单有力;
l 基于声明式表达方式便代码更易于理解;
l 复杂的流程变得更易于管理和描述。
三、ReactiveCocoa与RxSwift框架简介
现在,你已经更好地了解了FRP是什么以及它如何可以帮助你使复杂的异步流更容易管理。接下来,让我们考察两个当前***的FRP框架——ReactiveCocoa和RxSwift,并进一步了解为什么你可能选择其中之一。
在正式讨论有关细节之前,让我们简要介绍一下每个框架的历史。
(一)ReactiveCocoa框架
ReactiveCocoa框架(https://github.com/ReactiveCocoa/ReactiveCocoa)发布在GitHub网站上。在GitHub Mac客户端上工作时,开发人员发现自己疲于应对其应用程序的数据流。他们从微软的ReactiveExtensions(一个针对C#的FRP框架)框架中找到灵感,然后开发出他们自己的Objective-C实现版本。
当开发团队正在他们Objective-C3.0版本上进行研发时Swift正式宣布发行。他们意识到,Swift的函数天性正好弥补了ReactiveCocoa,于是他们马上着手实现Swift上的3.0版本。该3.0版本充分地利用了柯里化(curring)和pipe-forward运算符技术。
Swift 2.0引入了面向协议编程思想,从而导致ReactiveCocoa API发展历程中的另一个重大变化,随着版本4.0发布的临近,pipe-forward运算符支持协议扩展。
在本文写作之时,ReactiveCocoa已经成为一个GitHub网站上点赞超过13,000星的大热库。
(二)RxSwift框架
微软的ReactiveExtensions启发了大量的框架把FRP概念加入到JavaScript、Java、Scala和其他众多语言中。这最终导致ReactiveX的形成。ReactiveX其实是一个开发小组,它能够创建一批针对FRP实现的通用API;这将允许不同的框架开发人员协同工作。其结局是,熟悉Scala的RxScala开发人员能够相对容易过渡到Java的等效实现——RxJava。
RxSwift是最近才加入到ReactiveX中的,因此目前还没有像ReactiveCocoa(在本文写作之时,在GitHub已经获得大约4,000个星的点赞)那样普及。然而,RxSwift是ReactiveX的一部分的事实无疑将有助于它的流行和长久化。
有趣的是,RxSwift和ReactiveCocoa分享着一个共同的祖代实现——ReactiveExtensions!
四、ReactiveCocoa与RxSwift框架比较
承接前面所提到的,现在我们来关注细节内容。ReactiveCocoa与RxSwift框架在FRP支持方面拥有许多的不同之处。在此仅讨论几个关键部分。
(一)热信号与冷信号
想象一下,你需要发出网络请求、解析响应并向用户显示有关信息,例如类似于下面的代码:
- let requestFlow = networkRequest.flatMap(parseResponse)
- requestFlow.startWithNext {[weak self] result in
- self?.showResult(result)
- }
当订阅信号(使用startWithNext)时,将启动网络请求。这些信号被称为“冷”信号,因为它们处于“冻结”状态——直到你实际上订阅这些信号。
另一方面是“热”信号。当订阅其中之一时,它可能已经启动;因此,你正在观察的可能是第三或第四个相应的事件。一种典型的例子是敲打键盘。所谓“开始”敲打其实并无多大意义,对于服务器请求也是如此。
让我们回顾一下:
l 冷信号是:当你订阅它时你才启动。每个新的订阅服务器启动这项工作。订阅requestFlow三次意味着发出三次网络请求。
l 热信号是:已经可以发送事件。新的订阅服务器不去启动它。通常,UI交互就是热信号。
ReactiveCocoa针对热信号和冷信号提供了对应的类型支持:Signal<T, E>和SignalProducer<T, E>。然而,RxSwift使用了一种适用于上述两种类型的结构Observable<T>。
提供不同的类型来表示热和冷信号有必要吗?
就个人而言,我发现了解信号的语义是很重要的,因为它更好地描述了如何在特定的语境中使用它。当处理复杂的系统时,这可能有很大的区别。
且不说是否提供不同类型,仅了解热和冷信号而言就非常重要。
假设你正在处理一个热信号,但经证明它原来是一个冷信号,针对每个新的订阅服务器你将会以副作用方式启动,这对你的应用程序可能产生巨大的影响。一个常见的例子就是,在你的应用程序中存在三个或四个地方想观察网络请求,而针对每一个新的订阅将开始一个不同的请求。
(二) 错误处理
让我们谈到错误处理之前,不妨扼要地重述一下在RxSwift和ReactiveCocoa中调度的事件性质。在这两个框架中,都提供了三个主要事件:
1. Next<T>:每当有新值(类型T)推入到事件流时,将发送此事件。在上面的定位器示例中,T是CLLocation。
2. Completed:指示事件流已经结束。此事件发生后,不再发送Next <T>或Error <E>。
3. Error:指示一个错误。在上面的服务器请求示例中,如果你有一个服务器错误,会发送此事件。E描述了符合ErrorType协议的数据类型。此事件发生后,不会再发送Next或者Completed。
你可能已经注意到在前面讨论热和冷信号内容时ReactiveCocoa中的Signal<T,E>和SignalProducer<T,E>都使用了两个参数化的类型,而RxSwift的Observable <T>仅提供了一个。其中,第二种类型(E)是指符合ErrorType协议的类型。在RxSwift中,该类型被忽略,而在内部被视为一种符合ErrorType协议的类型。
那么,这一切意味着什么呢?
用通俗易懂的话说,它意味着在RxSwift 中可以通过多种不同的方式发出错误信息:
- create { observer in
- observer.onError(NSError.init(domain: "NetworkServer", code: 1, userInfo: nil))
- }
上述代码创建了一个信号(或使用RxSwift术语来说,是一个可观察的序列)并立即发出一个错误。
下面给出一种可替代的表达方式:
- create { observer in
- observer.onError(MyDomainSpecificError.NetworkServer)
- }
因为一个Observable只强制要求错误必须是符合ErrorType协议的类型,所以,你差不多可以发送任何你想要的。但也有一点尴尬的问题,请参考如下代码:
- enum MyDomanSpecificError: ErrorType {
- case NetworkServer
- case Parser
- case Persistence
- }
- func handleError(error: MyDomanSpecificError) {
- // Show alert with the error
- }
- observable.subscribeError {[weak self] error in
- self?.handleError(error)
- }
上述代码是不会工作的,因为函数handleError期待是MyDomainSpecificError类型而不是ErrorType。为此,你被迫要做两件事:
1.尝试把error转换成MyDomanSpecificError。
2.当不能把error转换成MyDomanSpecificError时,自己来处理这种情况。
***点可以轻易通过as?语法技术加以修复,但第二种情况较难处理一些。一种潜在的解决方案是引入一种Unknown类型:
- enum MyDomanSpecificError: ErrorType {
- case NetworkServer
- case Parser
- case Persistence
- case Unknown
- }
- observable.subscribeError {[weak self] error in
- self?.handleError(error as? MyDomanSpecificError ?? .Unknown)
- }
在ReactiveCocoa中,当创建一个Signal<T,E>或SignalProducer<T,E>时因为你作了类型“修复”,如果你尝试发送别的东西时编译器会发出抱怨。因此,底线是:在ReactiveCocoa中,编译器仅允许发送与你期望相同的错误。
这是ReactiveCocoa值得点赞的又一个方面!
(三) UI绑定
标准iOSAPI,如UIKit,并不使用FRP。为了使用RxSwift或ReactiveCocoa,你必须弥合这些Api,例如把设备的点按操作转换成信号或可观测对象。
正如你所想象的,这其中蕴藏着“巨大能量”;为此,ReactiveCocoa和RxSwift都各自提供大量的桥接函数及开箱即用的绑定支持。
ReactiveCocoa从其早期的Objective C时代带来了很多好东西。你会发现已经做了大量的工作,这已经弥合了与Swift共同使用的问题。这其中包括UI绑定,以及其他目前尚未翻译为Swift的运算符。当然,这稍微有点奇怪,你正在使用不属于SwiftAPI部分(如RACSignal)的一些数据类型,这将迫使用户把Objective C类型转换为Swift类型。
不只如此,我觉得我们花费了更多时间来讨论源码而不是文档,这已经慢慢地落后于时代了。不过,要注意的是,文档从理论角度看的确是优秀的,只是没有更多地关注实用方面。
为了弥补这种情况,你可以自行查阅一部分ReactiveCocoa教程。
另一方面,RxSwift绑定易于使用!不只是提供了一个庞大的分类目录(https://github.com/ReactiveX/RxSwift/blob/master/Documentation/API.md),也提供了大量的范例(https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Examples.md),以及更完整的文档(https://github.com/ReactiveX/RxSwift/tree/master/Documentation)。对于一些人来说,这已经是选择RxSwift而胜过选择ReactiveCocoa的足够理由。
这是RxSwift值得点赞的又一个方面!
(四)社团支持
ReactiveCocoa出现的历史已经远远超过RxSwift。有许多人可以继续开展这项工作,而且有相当数量的在线教程。此外,StackOverflow网站也提供了专门针对它(https://stackoverflow.com/questions/tagged/reactive-cocoa)的子论坛。
ReactiveCocoa有一个专门的Slack群,但很小,只有209人,所以经常有很多问题未得到及时解答。在紧急关头,我有时不得不联系ReactiveCocoa核心成员请教,当然假设别人也有类似的需求。尽管如此,你最有可能找到一些在线教程来解释你的问题。
相对于ReactiveCocoa,RxSwift自然更新一些,目前基本呈现出“很多人看一个人表演”的状态。它也有一个专门的Slack群,有961名成员,而且面临着比这个数目大得多的讨论量。如果有相关问题,你总能在此群中找到人来帮助你。
(五)使用哪一个更好
读者Ash Furrow的建议是:如果你是一位新手,那么选择从哪一个框架开始都无所谓。不错,的确存在许多技术方面的不同,但是对于新手来说这些内容都是很有意思的。你可以尝试使用一个框架,再试试另一个框架。由你自己确定到底哪一个更符合你自己,然后你就能够理解你为什么选择这个框架了。
如果你是一位新手,我也建议你这样做。其实,只有你积累了足够丰富的经验时你才会欣赏到二者之间的奥妙区别。
但是,如果你担任着一种特殊职务,你需要选择其一,并且没有时间来随意体验,那么我的建议如下:
建议选择ReactiveCocoa框架:
l 如果你想更好地描述你的系统。并且,想使用不同的类型来区别热信号和冷信号,并且在错误处理方面使用类型化参数,这些会为你的系统开发带来惊喜。
l 如果你想使用大规模测试框架,为许多人使用,并应用于许多项目中。
建议选择RxSwift框架:
l 如果UI绑定对你的工程很重要。
l 如果你是一位FRP新手,并需要及时的帮助信息。
l 如果你已经了解了RxJS或者RxJava。那么,既然这两个框架和RxSwift框架都隶于Reactivex组织,一旦你熟悉了其中之一,剩下的也就是语法问题了。
五、小结
无论选择RxSwift还是ReactiveCocoa框架,你都不会后悔的。这两个都是功能极其强大的框架,都会帮助你更好地描述你的系统。
值得注意的是,一旦你选择了RxSwift还是ReactiveCocoa框架,在两者之间来回切换只是几个小时的问题。就我的体验来说,从ReactiveCocoa转到RxSwift最关键的是熟悉其错误处理技术。总之,最关键的问题在于克服FRP编程技术,而不是具体的实现方面。
***,提供几个链接供你在学习上述两个框架道路上作为参考:
l Conal Elliott的博客(http://conal.net/blog/);
l Conal Elliott在Stackoverflow网站上的重要问答(https://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming);
l André Staltz的重要文章“Why I cannot say FRP but I just did”(https://medium.com/@andrestaltz/why-i-cannot-say-frp-but-i-just-did-d5ffaa23973b#.62dnhk32p);
l RxSwift代码仓库(https://github.com/ReactiveX/RxSwift);
l ReactiveCocoa代码仓库(https://github.com/ReactiveCocoa/ReactiveCocoa);
l Rex代码仓库(https://github.com/neilpa/Rex);
l 针对iOS开发者的FRP宝库(https://gist.github.com/JaviLorbada/4a7bd6129275ebefd5a6)。
l RxSwift探讨资源(http://rx-marin.com/)。
原文标题:ReactiveCocoa vs RxSwift
【51CTO.com独家译文,合作站点转载请注明来源】