本篇博文介绍了 ReactiveCocoa 3.0 (以下简称 RC)全新的 Swift 接口,包括泛型,操作符以及柯里化函数的有趣用法。
这是我为 RC3 系列准备的第一篇文章。这篇文章主要的侧重点是介绍 Swift 版本的 Signal
类,在下一期文章中我会展示一个更完整的应用。
我成为RC的粉丝已经很久了,Ray Wenderlich 网站上已经有我发表的数篇文章,以及一些与 RC 相关的幻灯片。
在Swift问世之初,我们就可以利用桥接 Objective-C 版的 RC 接口在 Swift 中使用 RC。但是能够充分利用 Swift 的特性,如泛型等,会使得 Swift RC 更加纯粹完美!
感谢 RC 团队,他们在过去的几个月里一直工作在全新的 Swift RC 分支上。就在一周前第一个 beta 版本问世,也就是今天这篇文章的主角。
在阅读这篇文章我假定您已经有了一定的 RC 基础,当然,您不用成为一个顶级专家。
使用 Cathage 可以在项目中非常方便的引入 RC,在工程的根目录创建一个 Cartfile
文件引用 RC3:
github "ReactiveCocoa/ReactiveCocoa" "v3.0-beta.1"
如文档所述,运行 carthage update
.
RC3 包含了全新的 Swift API,同时也兼容 Objective-C。所以你会发现两个 Signal 类, Obj-C 版本的 RACSignal
以及 Swift 版本的 Signal
。
Swift 版本的 Signal 类一个重要特性就是它是泛型的:
class Signal<T, E: ErrorType> { ... }
类型参数 T
表明 Signal 对象发出的 ‘next’ 事件所附带的数据类型,错误类型由 E
表示,并要求实现 ErrorType 协议。 Swift 的 signals 类与 OC 版本具有类似的用法,下面展示了一个每秒抛出事件的 signal 类:
func createSignal() -> Signal<String, NoError> { var count = 0 return Signal { sink in NSTimer.schedule(repeatInterval: 1.0) { timer in sendNext(sink, "tick #(count++)") } return nil } }
Signal
类的初始化方法需要传入一个生成器,在上例中传入的是一个闭包。生成器被调用后传入一个 sink 对象,sink 对象的类型是 SinkOf<Event<String, NoError>>
。任何发送给 sink 对象的事件都会被 signal 对象抛出。
sendNext
函数把局部变量 count 作为第二个参数,构造了一个事件传递给 sink。
Swift Signal 类与 ObjC 版本的 Signal 类有相似的内存管理机制,当一个 signal 实例生命周期结束时,需要被释放。在上例中这一步被包含在闭包表达式之中的最后一步
有若干种方法你可以监听一个 Signal。最简单的方法是使用 observe
方法,为你所感兴趣的事件提供一个函数或者闭包回调。
这是一个监听 next 事件的例子:
let signal = createSignal() signal.observe(next: { println($0) })
输出如下:
tick #0 tick #1 tick #2 tick #3 tick #4
或者,你可以提供一个 sink 变量监听 signal 的时间:
createSignal().observe(SinkOf { event in switch event { case let .Next(data): println(data.unbox) default: break } })
Event 类型是一个枚举,关联了 next 和 error 事件。Sinkof 初始化方法构造了一个 SinkOf<Event<String, NoError>> 类型的sink 变量,同样的,后面的闭包表达式作为参数传给了初始化方法。
由于 Swift 语言的限制,Event 类型的枚举(next,error 事件)携带的数据被 LlamaKit Box 类封装在暗盒中,作为一个 RC3 的使用者你很少需要直接跟 Event 类型打交道,有很多 API 可以帮你进行封装和解封的操作。
上面的例子展示了些许 Swift 跟 RC 结合所带来的优势。使用泛型定义 Signal 意味着监听事件时可以更安全的获取数据类型,此外,如果你使用的是很复杂的泛型嵌套,你也不用去显式的声明泛型了。
Swift Signal 类型与它的 Objc 版本具有诸多相似的特性.可是,他们之间有一个根本的区别。
对于一个简单的map操作,你通常会定义一个Signal的方法,伪代码如下:
class Signal<T, E: ErrorType> { func map(transform: ...) -> Signal }
不过,map 函数以及其他能应用于 Signal 的操作实际上都是非成员函数:
class Signal<T, E: ErrorType> { } func map(signal: Signal, transform: ...) -> Signal
不幸的是把成员函数变成非成员函数,接口看起来将很不协调,比如一个 map 操作紧跟着一个 filter 操作看起来像这样:
let transformedSignal = filter(map(signal, { ... }), { ... })
幸运的是 RC 从 F# 语言中学来了一个非常时髦的操作符 |>
。Swift 中的 map 操作其实是一个柯里化函数:
public func map<T, U, E>(transform: T -> U) (signal: Signal<T, E>) -> Signal<U, E> { }
在第一次调用的时候你提供一个转换函数,它将会根据你提供的转换函数返回一个将 Signal 映射到另一个 Signal 的新函数。|> 运算符允许你将 Signal 对象映射到其他类型的操作串联起来。
public func |> <T, E, X>(signal: Signal<T, E>, transform: Signal<T, E> -> X) -> X { return transform(signal) }
在实际应用中,如果想让当前 signal 对象抛出大写字母事件,你可以用如下的柯里函数映射:
let signal = createSignal(); let upperMapping: (Signal<String, NoError>) -> (Signal<String, NoError>) = map({ value in return value.uppercaseString }) let newSignal = upperMapping(signal) newSignal.observe(next: { println($0)})
输出如下
TICK #0 TICK #1 TICK #2 TICK #3 TICK #4
注意 upperMapping
常量有一个显式的类型 (Signal<String, NoError>) -> (Signal<String, NoError>),因为编译器没法通过你提供的参数推断出这个变量的类型。
使用运算符,你可以用如下方式来转换 Signal
let newSignal = signal |> upperMapping
甚至你可以用此运算符来关联回调来监听此 Signal:
signal |> upperMapping |> observe(next: { println($0) })
最后,与其将这个柯里函数赋给 upperMapping 常量,你也可以用如下方式将其做关联
signal |> map { $0.uppercaseString } |> observe(next: { println($0) })
注意这样做你就不用在显式告知编译器 map
函数的返回类型了,编译器可以从上下文中推断得到。这一切棒极了!最后一点,你可以改变流经这个管道的数据的类型,以及 signal 的类型,如下例所示:
signal |> map { count($0) } |> observe(next: { println($0) })
上述的映射操作将 Signal<String, NoError> 变换为 Signal<Int, NoError>。你可以看到 next 时间携带的数据类型已经发生了改变:
操作符和柯里函数的使用会花费你不少的精力。由于额外的泛型参数整个函数接口变得更加复杂。为了解释这些特性如何协同工作我建了一个示例项目,使用了操作符和大量非成员函数。我建了一个包含注释的 playground, 可以在gist上查看 ,了解更多细节。
RC3 的核心思想和 RC2 完全一致,Signal 产生事件,不过内部的实现却大相径庭。虽然你不需要真正去理解 Signal 和操作符的实现机制,但是在你调试程序的时候会变得非常有用。
当你使用 RC3 时,可能你所面对的编译错误会误导你,通常情况下并不会定位到正确地位置。
在你查错的时候把这些 Signal 通道排除是非常有用的,以便你可以定位错误的源头。
你可能会问会什么要用操作符和柯里函数来构造一个这么复杂的 API ?当我第一次上手 RC3 的时候也有相同的疑问!
使用非成员函数的一个优点是它们不会受到继承规则的约束。譬如,Swift 的 foundation 库定义了一个 map 函数可以将任何实现 CollectionType 协议的对象转换成任一对象。所以,你可以将这个函数应用到任何集合类,即便和 foundation 类没有继承关系。
在我的 下篇博文 中我将介绍更多 RC 中的新概念,包括 Signal Producer,RC3 中 Cold Signal 的替代方案!