ReactiveCocoa 3.0 初見(2)
在我的前一篇部落格中首次嘗試了ReactiveCocoa3.0 (RC3),在那篇部落格中提到了新的 Signal 介面和 |> 操作符。 在這篇部落格中我將繼續我們探索RC3 API的旅程,我將著重講述訊號發生器,還會從整體上談論一下新 ReactiveCocoa API 的易用性。
如果你已經使用過 ReactiveCocoa,那麼你或許已經遇到過熱訊號和冷訊號的概念區分問題,由於這兩個概念都是通過同樣的 RACSignal 型別表現出來的,所以區分這兩個概念一直以來都是個問題。ReactiveCocoa的設計指導文件建議採用不同的命名方式來區分他們,但是這仍然很難以捉摸。
在RC3中使用不同的型別(訊號和訊號發生器)來代表他們,讓熱訊號和冷訊號之間的區別變得更加明顯,也讓有著細微區別的操作符命名法變得不再細微(你觀察一個訊號,但是啟動一個訊號發生者)。在RC3中令人困惑的熱訊號和冷訊號已經徹底消失了。
訊號
比較 Signal 和 SignalProducer 最好的方法就是嘗試使用它們。
在我的上一篇部落格中,我建立了一個簡單的訊號,它可以每秒傳送一個 next 事件:
Objective-C12345678910 | func createSignal()->Signal<String,NoError>{var count=0returnSignal{sink inNSTimer.schedule(repeatInterval:1.0){timer insendNext(sink,"tick #(count++)")}returnnil}} |
通過新增幾個 println 語句,我們可以觀察到什麼時候構建訊號什麼時候傳送 next 事件。
123456789101112 | func createSignal()->Signal<String,NoError>{var count=0returnSignal{sink inprintln("Creating the timer signal")NSTimer.schedule(repeatInterval:0.1){timer inprintln("Emitting a next event")sendNext(sink,"tick #(count++)")}returnnil}} |
如果你建立一個訊號的例項:
Objective-C1 | let signal=createSignal() |
但是不新增任何一個觀察者,由於沒有觀察者,你將只能看到訊號的建立和事件的傳送。
Objective-C12345 | Creatingthe timer signalEmittinganext eventEmittinganext event Emittinganext event... |
訊號發生器
訊號發生器的初始化使用了同樣的模式:
Objective-C1234567891011 | func createSignalProducer()->SignalProducer<String,NoError>{var count=0returnSignalProducer{sink,disposable inprintln("Creating the timer signal producer")NSTimer.schedule(repeatInterval:0.1){timer inprintln("Emitting a next event")sendNext(sink,"tick #(count++)")}}} |
訊號和訊號發生器在初始化時候的區別不太明顯,訊號發生器和訊號採用同樣的範型型別作為引數,一個為next事件的型別,另一個為錯誤型別。它們也都很典型地在初始化的時候使用了閉包表示式,不過這個閉包表示式並不是返回一個容器變數 RACDisposable ,而是用一個複合的容器例項作為引數。
如果你建立了一個訊號發生器,但是沒有訂閱(沒有新增觀察者):
Objective-C1 | let signalProducer=createSignalProducer() |
你會發現沒有 log 被顯示在控制檯視窗上,定時器也沒有啟動。
訊號發生器是在啟動時產生訊號的工廠。為了啟動定時器,你可以呼叫定義在訊號發生器中的 start 方法或者和 signal API 一樣,通過 |> 操作符來使用 start 函式。
Objective-C1234 | signalProducer|>start(next:{println($0)}) |
在前面的訊號例子中,如果你添加了多個觀察者,只會啟動一個訊號定時器。然而對於訊號發生器,你每新增一個觀察者就會建立一個新的定時器。
訊號發生器被用來代表一種過程或者任務,這種過程在啟動訊號發生器被初始化。而訊號代表了一個事件流,不管有沒有觀察者,事件都會發生。所以,訊號發生器適合被用來做網路請求,而訊號更適合被應用在處理UI事件流中。
操作訊號發生器
用於操作訊號發生器的函式都被定義為柯里自由函式,訊號也一樣。
例如,下面是on操作,它把一些副作用注入到操作流中:
Objective-C1234 | func on<T,E>(started:(()->())?=nil,...)(producer: SignalProducer<T,E>)->SignalProducer<T,E>{...} |
(為了便於閱讀,一些函式變數被刪除掉了。)
|>操作符也被過載了,使得他可以被應用在訊號發生器上:
Objective-C1234 | public func|><T,E,X>(producer: SignalProducer<T,E>, transform: SignalProducer<T,E>->X)->X{returntransform(producer)} |
這麼做的結果是,你可以建立訊號發生器操作過程渠道:
Objective-C1234567 | signalProducer|>on(started:{println("Signal has started")})|>start(next:{println("Next received: ($0)")}) |
結合上面那個使用 timer 的訊號發生器會產生如下的輸出:
Objective-C1 | Signalhas started |
123456 | Creatingthe timer signal producerEmittinganext eventNextreceived: tick#0Emittinganext eventNextreceived: tick#1... |
在訊號發生器上使用訊號的操作
如果你仔細觀察訊號發生器的操作,你會很快意識到它缺少一些“核心”的功能,比如map和filter。
訊號發生器使用lift方法,來將指定的訊號操作應用到發生器啟動時建立的訊號上,而不是重複實現signal的操作。
下面是實現方法:
Objective-C12345678910111213 | let signalProducer=createSignalProducer()let mapping: Signal<String,NoError>->Signal<Int,NoError>=map({string inreturncount(string)})let mappedProducer=signalProducer.lift(mapping)mappedProducer|>start(next:{println("Next received: ($0)")}) |
上面的程式碼使用柯里化的 map 函式建立一個從訊號到其他型別的對映,然後對映被轉接到訊號發生器上。上面程式碼的結果以log顯示如下:
Objective-C12345 | Emittinganext eventNextreceived:7Emittinganext eventNextreceived:7Emittinganext event |
在XCode中,你可以驗證原來的訊號發生器型別SignalProducer<String, NoError>已經轉變成SignalProducer<Int, NoError>型別了:
這很酷是吧!不過它還可以變得更好…
|>操作符還有另外一種過載方式,它能夠以更直接的方式在訊號上應用操作:
Objective-C1234 | public func|><T,E,U,F>(producer: SignalProducer<T,E>, transform: Signal<T,E>->Signal<U,F>)->SignalProducer<U,F>{returnproducer.lift(transform)} |
結果是,訊號操作可以直接在管道中被執行:
Objective-C12345678 | signalProducer|>on(started:{println("Signal has started")})|>map{count($0)}|>start(next:{println("Next received: ($0)")}) |
上面的程式碼展示了訊號生產者的on操作可以自由混搭訊號的操作,比如map。
關於為什麼有些操作被定義在訊號上,有些操作被定義在訊號生產者上,我還是有些不確定。我們能看到在當前的API中,訊號的操作大體上重心在next事件中,允許我們在不同的執行緒中轉換賦值、跳躍、延遲、組合和觀察。而訊號生產者的API更關心訊號的生命週期事件(完成、異常),使用then、flatmap、takeUntil和catch等等類似的操作。
當前情況下,這種方式還有些限制,比如我從一個UITextField中創造一個事件流作為訊號,但我不能為它新增flatMap操作,我已經把這個標記為一個問題提了出來。
介面說明
RC3 API包含很多很聰明的概念,它是函式式Swift的一個很好的例子。我個人已經從這個庫中學到很多。
另外,RC3還有一些更實用的好處。使用範型意味著訊號是強型別的,可以消除全部的runtime錯誤,最終呈現出更簡潔的程式碼。還有,訊號和訊號生產者的區別很明顯。
然而我不禁會想這些API是不是有點太聰明瞭!
在我的上一篇關於ReactiveCocoa的文章中,我一直在用一個簡單清晰的方式來表明一個主題,那就是向讀者展示ReactiveCocoa 的核心概念很容易掌握。我已經收到一些來自讀者的很好的反饋,很多人在讀完後聯絡我表示他們理解了。我著實好奇RC3 介面是不是會讓那些曾經很艱難地理解ReactiveCocoa的人感到困惑。
以後我會針對一些引起關注的RC3 API進行細緻地闡述。
在RC3中,範型的使用很有必要、有很高的價值,而且範型也是一個被充分理解的主流概念。點贊。
RC3的操作使用柯里自由函式,我對它們有幾點要說。首先,柯里函式是一個很先進的概念,它們不是你會認為的主流技術。其次,自由函式更不容易被發現,XCode自動補全不會給你任何可用的操作型別的提示。
|>操作符也是一個很前衛的概念,尤其是那個允許你將訊號操作應用到訊號產生者上的過載用法。挑剔地說,如果你不能理解它是如何工作,也就