ReactiveCocoa vs RxSwift

ReactiveCocoa vs RxSwift

原文:Rui Peres on April 26, 2016

RxSwift vs ReactiveCocoa

函数式响应型编程(Functional Reactive Programming)是一种变得越来越流行的编程范式,尤其是在 Swift 开发者之中。它将复杂的异步过程,变得容易编写和理解。

在这篇文章里,你将可以对比函数式响应型编程中最流行的两个框架:RxSwift 和 ReactiveCocoa。

下面来简单的了解一下,什么是函数式响应型编程?然后详细的比较一下这两个框架,了解了这些之后,你就可以选择一个适合你的框架来使用。

那么开始吧!

什么是函数式响应型编程?

如果你已经熟悉了函数式响应型编程的概念,可以跳过这一章节,直接阅读下一章节 ReactiveCocoa vs RxSwift

甚至在 Swift 出现之前,函数式响应型编程(FRP)在近些年来欢受迎程度就大幅增长,与面向对象编程形成鲜明对比。从 Haskell 到 Go,再到 Javascript,都有 FRP 的实现方式。为什么呢?FRP 到底有什么特异功能?
最重要的问题是,你如何将这种编程范式应用到 Swift 上呢?

函数式响应型编程是由 Conal Elliott 创建的一种编程范式。他给了一个严谨详细的语义定义,你可以从这里去进一步了解。简单定义的话,FRP 是由两种概念组合而成的:

  1. 响应型编程(Reactive Programming),它关注的是异步数据流,你需要监听并根据其中的数据做出响应。想要了解更多,看看这篇不错的介绍
  2. 函数式编程(Functional Programming),他的函数定义具有数学风格,计算过程中尽量避免使用变量和状态值,代码更加灵活无副作用。想要了解更多,请看我们的另一篇 "Swift functional programming tutorial"

André Staltz 在他的文章 "Why I cannot say FRP but I just did" 中,阐述了标准的 FRP 范式和它的可实现方式之间的差别。

一个简单的例子

�想要理解这个概念最简单的方式,就是通过例子来说明。想象有一款应用,需要关注用户的位置变化,并且在发现他的位置靠近一个咖啡店的时候提示他。

通过 FRP 的方式需要这样实现:

  1. 需要创建一个对象,它发出你需要响应的位置变化事件的数据流。
  2. 然后通过筛选这些位置信息,把那些靠近咖啡店的位置信息显示出来。

在 ReactiveCocoa 中,代码大概长这样:

locationProducer // 1  
  .filter(ifLocationNearCoffeeShops) // 2  
  .startWithNext {[weak self] location in // 3  
    self?.alertUser(location)
}

说明一下这部分代码:

  1. locationProducer 在位置信息每次发生变化的时候,都会抛出一个事件(event)。ReactiveCocoa 把它称作 signal,RxSwift 中称作 sequence
  2. 然后利用函数式编程(Functional Programming)技术去处理这些变化事件的数据。filter 函数的用法和数组(array)中的相同,将数据流中的每个值作为参数传给 ifLocationNearCoffeeShops处理,如果返回 true,这个事件会继续传递到下一步。
  3. 最后,startWithNext 就是一个订阅方法,当有(过滤过的)事件传递到这里的时候,你传入的闭包表达式将会执行,并将那些数据作为参数提供给你。

上面的代码看起来很像是将数组中的数据进行转换。但是这个更高级...它是异步的;那个过滤方法和闭包表达式只有在位置变化事件发生的时候才会被执行。

语法看起来也许会怪一些,不过希望你可以明白这段代码所表达的意思。它既体现了函数式编程的精髓,又十分贴切 values over time(直译:时间轴上的数据)的概念:这也就是它的定义。你需要关心的是数据来了,而不是产生这些数据的细节

如果你想了解更多的 ReactiveCocoa 语法,去看看我写的一些例子吧:Github 地址

事件转换

在上面的例子中,仅仅是关注了位置变化的数据流,除了过滤一些靠近咖啡店的位置,并没有对这些事件做更多的处理。
而 FRP 范式的另一个基本要素,就是能够将这些事件数据进行组合、转换,使其变得更有意义一些。怎么做呢?你可以利用(但不限于)一些高阶函数。
你可以在我们的教程 Swift functional programming tutorial 中,找到一些常见的函数:mapfilterreducecombinezip
我们来优化一下这段代码,过滤掉那些重复的位置信息,并且将传过来的位置数据(CLLocation)转换成一段用户可识别的文本信息。

locationProducer
  .skipRepeats() // 1
  .filter(ifLocationNearCoffeeShops) 
  .map(toHumanReadableLocation) // 2
  .startWithNext {[weak self] readableLocation in
    self?.alertUser(readableLocation)
}

我们来看看新加的两行代码:

  1. 首先在 locationProducer 发出数据流之后,加一步 skipRepeats 操作,这个操作并不是 array 所具有的;它是 ReactiveCocoa 所特有的。这个方法的意图很明显:过滤掉那些相等的数据事件(这些事件的数据需要具有可比性)。
  2. filter 方法执行过后,map 函数的作用就是将一种事件数据转换成另一种,比如把 CLLocation 类型转换成 String 类型。

现在,你已经体会到了 FRP 的一些非凡之处了吧:

  • 它使用简单,却功能强大。
  • 它使代码更加容易被理解。
  • 复杂的数据流,变得容易管理和描述。
image

ReactiveCocoa 和 RxSwift 简述

现在你已经对什么是 FRP 有了一个比较好的认识,并且知道了它如何帮助你简单的管理复杂的异步数据流。接下来让我们来看看这两个流行的 FRP 框架:ReactiveCocoa 和 RxSwift,然后你可以挑一个适合你的来使用。
详细分析之前,我们先简单了解一下这两个框架的发展史。

ReactiveCocoa

ReactiveCocoa 是在 Github 上发布的。当时开发者们在 Github Mac 客户端上工作,他们发现很难管理他们应用的数据流。后来他们从微软的 ReactiveExtensions(一个 C# 的 FRP 实现)那里得到灵感,创建了他们的 Objective-C 实现。

当他们正准备发布 Objective-C 实现的 3.0 版本的时候,Swift 发布了。他们意识到 Swift 的函数式风格更适合 ReactiveCocoa,所以他们马上着手于 Swift 的实现,成为了 3.0 版本。3.0 版本更具函数式风格,利用到了柯里化(currying)和 pipe-forward 运算符技术。

Swift 2.0 引入了面向协议编程(protocol-oriented programming 概念,这也导致了 ReactiveCocoa API 另一个具有重大意义的变化,4.0 版本减少使用 pipe-forward 运算符,而开始运用协议拓展。

写这篇文章时,ReactiveCocoa 已经是在 Github 上已经收获了超过 13000 颗星的非常流行的库了。

RxSwift

微软的 ReactiveExtensions 启发许多其他的框架,将 FRP 的概念融入了 JavaScript,Java,Scala,还有许多其他的编程语言。最终形成了 ReactiveX,一个为 FRP 实现方式创建通用 API 的组织;这允许许多框架作者可以协同工作。也正因为这样,一个熟悉 RxScal(Scala 的实现)的开发者会发现把它转换成 Java 类似的实现(RxJava)将会很容易。

RxSwift 是相对较新加入 ReactiveX 的,而且不如 ReactiveCocoa 更加流行(写这段话时,它在 Github 上大概有 4000 颗星)。不过事实上,RxSwift 作为 ReactiveX 的一部分,毫无疑问将会很流行并长久发展下去。

值得一提的是,RxSwift 与 ReactiveCocoa 都有一个共同的原型:ReactiveExtensions。

RxSwift 与 ReactiveCocoa 对比

是时候该了解一下细节了。RxSwift 与 ReactiveCocoa 对于 FRP 的支持体现了许多不同的方面,让我们来看看他们其中重要的几部分。

热信号、冷信号

想象一下,你需要发起一个网络请求,并且解析它的响应数据,然后展示给用户:

let requestFlow = networkRequest.flatMap(parseResponse)
 
requestFlow.startWithNext {[weak self] result in
  self?.showResult(result)
}

只有当你订阅一个 signal(也就是进行 startWithNext 操作)的时候,网络请求才会被创建并发起。这种 signal 被称作是冷(cold)的,因为正如你所猜到的,直到你订阅它们之前,它是处于“冻结”状态的。

另一种则称为热(hot) 信号,当订阅一个热信号时,它就已经开始了,所以你可以观察到第三或者第四次事件,或者更多。比较典型的例子就是敲击键盘产生的事件流。对于“开始”敲击键盘,并没有什么意义,就像创建一个服务器请求。

总结一下:

  • 一个冷信号是指,当你想要订阅他的时候,需要执行开始任务,每个新的订阅者都需要执行开始任务,订阅 requestFlow 三次也就意味着相对应的要创建三个网路请求。
  • 一个热信号创建时就已经可以发送事件了,订阅者不需要去开启它。通常 UI 交互是属于热信号。

ReactiveCocoa 针对热、冷信号分别提供了这两种类型:Signal<T,E>SignalProducer<T,E>。而 RxSwift 提供了一种同时支持冷、热信号的类型:Observable<T>

区分热信号、冷信号两种不同的类型真的有必要么?

我个人认为,知道一个信号的含义是很重要的,因为它更好的描述了如何在一个特定的上下文中运用它。当处理一个复杂的系统时,这些将会有很大不同。

且不说有没有这两种类型的支持,仅仅了解热信号、冷信号这些概念就非常重要。
正如 André Staltz 所说的:

如果你忽略了它,那么它一定会回来给你狠狠的一击。别说我没告诉过你。

假设你正在处理一个热信号,然后由于某种原因它变成了冷信号,这个时候你将会对每个订阅者进行副作用编程。这将会给你的应用带来很大影响。举个通用的例子,假设在你的应用中,有三个或四个实体需要监听同一种网络请求,而对于每个新的订阅,都会发起一个新的网络请求。

ReactiveCocoa 加一分!

错误处理

讨论错误处理之前,我们先概括一下 RxSwift 和 ReactiveCocoa 分发的事件的一些性质。这两个框架中,主要有三种事件类型:

  1. Next<T>:每当一个新的值(T 类型)被传到事件流中时,这种事件就会被触发。在上面跟踪定位的那个例子中,T 指的就是 CLLocation
  2. Compleled:表示事件流的终止。收到这个事件之后,将不会在发送 Next<T>Error<E>
  3. Error<E>:表示一个错误。在服务器请求的例子中,当你收到一个服务器错误时,这个事件将会被发送。E 是遵循了 ErrorType 协议的错误类型。收到这个事件之后,将不会在发送 Next<T>Compleled

你应该已经注意到上一章节提到 ReactiveCocoa 的 Singal<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 中称之为观察序列(observable sequence)),然后立刻抛出了一个错误。

这是另一个:

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 转换成 MyDomainSpecificError 类型。
  2. 处理当 error 不能转成 MyDomainSpecificError 的情况。

第一点很容易利用 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 绑定

在 iOS 的标准 API 中,比如 UIKit,并不是使用的 FRP 的语言语法。所以为了使用 RxSwift 和 ReactiveCocoa,你必须桥接这些 API,例如将点击事件(利用 target-action 编码形式)转换成 signalobservable

正如你所想象的,这需要许多工作,所以这两个库都额外提供了许多桥和绑定。

ReactiveCocoa 带来了许多从 Objective-C 时期完成的工作。你会发现有一部分已经完成了,并且已经桥接到了 Swift 上。这些只包括 UI 绑定,其他操作还没有没翻译成 Swift。所以,显得有些奇怪。你正在使用一个不是 Swift API 中的类型(比如 RACSignal),然后又强制用户将 Objective-C 类型转为 Swift(例如使用 toSignalProducer() 方法)。

不止这些,我觉得我看源码的时间比看文档的时间都长,这显然跟不上时代。注意这点是很重要的,虽然从解释理论和思路上来讲,文档写的很好,但从使用角度上来说还远远不够。

正是由于这点,你会发现有许多的 ReactiveCocoa 教程。

与此不同的是,RxSwift 提供的绑定很容易使用!你不仅能看到巨长的目录,还能找到巨多的例子,还有一份更加完善的文档。对于一些人来说,这已经足够让你去选择 RxSwift,而不选 ReactiveCocoa 了。

RxSwift 加一分!

社团

ReactiveCocoa 比起 RxSwift 很久之前就已经出现了。有许多人还在继续维护,在网上也有很多教程,而且通过在 StackOverflow 的 ReactiveCocoa 标签可以找到很好的资源。

ReactiveCocoa 有一个 Slack 的群,不过很小只有 209 个人,所以有许多人提的问题(包括我提的和其他人提的)都没有回答。由于时间紧急,我被迫私信了 ReactiveCocoa 的核心成员,所以我猜测其他人也可能和我的情况的一样。不过,你很有可能在网上找一个教程来解释你的问题。

RxSwift 是新人,不过现在却很有一枝独秀的感觉。他也有一个 Slack 的群,而且很大已经有 961 个成员了,群内讨论热烈。你也可以比较容易在这里找到回答你问题的人。

总之吧,现在两个框架的社团支持以各自不同的强大方式支持着,所以这个小节,打成平手。

你该如何选择?

正如 Ash Furrow 在 “ReactiveCocoa vs RxSwift” 中所说的:

“听我的,如果你是一个新手,选哪个真的没有关系。是的,虽然他们有些技术上的区别,不过对于新手没太大意义。试着用其中一个,然后再试试另一个。看看哪个更适合你,然后在想想为什么你会选择它。”

我的意见也大概如此。只有当你有足够的体验之后,才能领会他们之中细微的差别。

不过,如果你现在正处于一个需要选择其中一个,但是又没时间去使用它们的情况,可以看看我的意见:

选择 ReactiveCocoa,如果:

  • 你想要更好的描述你的系统。用不同的类型来区分热信号和冷信号,同时通过类型化参数处理错误,对于你的系统会有很好的效果。
  • 想要一个大规模的测试框架,被许多人使用在不同的项目中。

选择 RxSwift,如果:

  • UI 绑定对于你的系统很重要
  • 你是一个 FRP 新手,希望得到一些手把手的教学。
  • 你已经了解了 RxJS 或者 RxJava。因为它们和 RxSwift 都是属于 ReactiveX 组织的,如果你了解其中之一,其他的也只是语法上的不同而已。

何去何从?

无论你选择了 ReactiveCocoa 或者选择 RxSwift,你都不会后悔的。它们都是很强大的框架,并且可以帮助你很好的描述你的系统。

需要注意的很重要的一点是,一旦你选择 RxSwift 或 ReactiveCocoa 其中之一,想要切换到另一方也只是几个小时的问题。就我以锻炼的目的,从 ReactiveCocoa 转到 RxSwift 的经验来说,大部分麻烦的问题也就是错误处理了。总结来说,最大的思想转变就是完全使用 FRP,而不是仅仅实现其中一个部分。

以下的链接可以帮助你融入函数式响应性编程、RxSwift 和 ReactiveCocoa 中:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • 内容来自stack overflow的一个回答:ReactiveCocoa vs RxSwift - pros a...
    没故事的卓同学阅读 26,104评论 19 65
  • 今生的相遇 是无法描述的欢喜 一刹那的感知 用纸 用笔 我不懈的拉近着与你的距离 今天 我又用标准的心声 朗读了你...
    冉冉语文阅读 345评论 0 4
  • 但是我想告诉我的孩子们,如果只是把语文学习的目的归于接受知识,明白道理,甚至是在考试中取得很高的分数的话,那我们学...
    薇雨青芜阅读 661评论 0 5
  • 终于厌倦了呆在宿舍的生活,玩完游戏后逃出宿舍,信步走在校园的路上,不知已经多久没有这样没有方向没有事情的走路了,所...
    廿月雨阅读 556评论 0 2
  • 模块规范(module) Ruby 的 require、Python 的 import、 CSS 有 @impor...
    素炒饼阅读 215评论 0 0