转载

[译] ReactiveCocoa 3.0 初窥

本篇博文介绍了 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 基础,当然,您不用成为一个顶级专家。

创建 SIGNALS

使用 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 实例生命周期结束时,需要被释放。在上例中这一步被包含在闭包表达式之中的最后一步

监听 SIGNALS

有若干种方法你可以监听一个 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 意味着监听事件时可以更安全的获取数据类型,此外,如果你使用的是很复杂的泛型嵌套,你也不用去显式的声明泛型了。

转换 SIGNALS

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 时间携带的数据类型已经发生了改变:

[译] ReactiveCocoa 3.0 初窥

操作符和柯里函数的使用会花费你不少的精力。由于额外的泛型参数整个函数接口变得更加复杂。为了解释这些特性如何协同工作我建了一个示例项目,使用了操作符和大量非成员函数。我建了一个包含注释的 playground, 可以在gist上查看 ,了解更多细节。

总结

RC3 的核心思想和 RC2 完全一致,Signal 产生事件,不过内部的实现却大相径庭。虽然你不需要真正去理解 Signal 和操作符的实现机制,但是在你调试程序的时候会变得非常有用。

当你使用 RC3 时,可能你所面对的编译错误会误导你,通常情况下并不会定位到正确地位置。

[译] ReactiveCocoa 3.0 初窥

在你查错的时候把这些 Signal 通道排除是非常有用的,以便你可以定位错误的源头。

你可能会问会什么要用操作符和柯里函数来构造一个这么复杂的 API ?当我第一次上手 RC3 的时候也有相同的疑问!

使用非成员函数的一个优点是它们不会受到继承规则的约束。譬如,Swift 的 foundation 库定义了一个 map 函数可以将任何实现 CollectionType 协议的对象转换成任一对象。所以,你可以将这个函数应用到任何集合类,即便和 foundation 类没有继承关系。

在我的 下篇博文 中我将介绍更多 RC 中的新概念,包括 Signal Producer,RC3 中 Cold Signal 的替代方案!

正文到此结束
Loading...