.Net 中的反應式程式設計

發表於2016-05-11

一、反應式程式設計(Reactive Programming)

1、什麼是反應式程式設計:反應式程式設計(Reactive programming)簡稱Rx,他是一個使用LINQ風格編寫基於觀察者模式的非同步程式設計模型。簡單點說Rx = Observables + LINQ + Schedulers。

2、為什麼會產生這種風格的程式設計模型?我在本系列文章開始的時候說過一個使用事件的例子:

這個程式碼定義了一個FileSystemWatcher,然後在Watcher事件上註冊了一個匿名函式。事件的使用是一種命令式程式碼風格,有沒有辦法寫出宣告性更強的程式碼風格?我們知道使用高階函式可以讓程式碼更具宣告性,整個LINQ擴充套件就是一個高階函式庫,常見的LINQ風格程式碼如下:

能否使用這樣的風格來編寫事件呢?

3、事件流
LINQ是對IEnumerable的一系列擴充套件方法,我們可以簡單的將IEnumerable認為是一個集合。當我們將事件放在一個時間範圍內,事件也變成了集合。我們可以將這個事件集合理解為事件流。

事件流的出現給了我們一個能夠對事件進行LINQ操作的靈感。

二、反應式程式設計中的兩個重要型別

事件模型從本質上來說是觀察者模式,所以IObservable和IObserver也是該模型的重頭戲。讓我們來看看這兩個介面的定義:

這兩個名稱準確的反應出了它兩的職責:IObservable-可觀察的事物,IObserver-觀察者。

IObservable只有一個方法Subscribe(IObserver observer),此方法用來對事件流注冊一個觀察者。

IObserver有三個回撥方法。當事件流中有新的事件產生的時候會回撥OnNext(T value),觀察者會得到事件中的資料。OnCompleted()和OnError(Exception error)則分別用來通知觀察者事件流已結束,事件流發生錯誤。

顯然事件流是可觀察的事物,我們用Rx改寫上面的例子:

注:在.net下使用Rx程式設計需要安裝以下Nuget元件:

三、UI程式設計中使用Rx

Rx模型不但使得程式碼更加具有宣告性,Rx還可以用在UI程式設計中。

1、UI程式設計中的第一段Rx程式碼

為了簡單的展示如何在UI程式設計中使用Rx,我們以Winform中的Button為例,看看事件模型和Rx有何不同。

新增了一個Button,點選Button的時候彈出一個對話方塊。使用Rx做同樣的實現:

有朋友指出字串“Click”非常讓人不爽,這確實是個問題。由於Click是一個event型別,無法用表示式樹獲取其名稱,最終我想到使用擴充套件方法來實現:

我們平時常用的事件型別也就那麼幾個,可以暫時通過這種方案來實現,該方案算不上完美,但是比起直接使用字串又能優雅不少。

2、UI程式設計中存在一個很常見的場景:當一個事件的註冊者阻塞了執行緒時,整個介面都處於假死狀態。.net中的非同步模型也從APM,EAP,TPL不斷演化直至async/await模型的出現才使得非同步程式設計更加簡單易用。我們來看看介面假死的程式碼:

Thread.Sleep(2000);模擬了一個長時間的操作,當你點下Button時整個介面處於假死狀態並且此時的程式無法響應其他的介面事件。傳統的解決方案是使用多執行緒來解決假死:

這個程式碼的複雜點在於:普通的多執行緒無法對UI進行操作,在Winform中需要用Control.BeginInvoke(Action action)經過包裝後,多執行緒中的UI操作才能正確執行,WPF則要使用Dispatcher.BeginInvoke(Action action)包裝。

Rx方案:

一句SubscribeOn(ThreadPoolScheduler.Instance)將費時的操作跑在了新執行緒中,ObserveOn(this)讓後面的觀察者跑在了UI執行緒中。

注:使用ObserveOn(this)需要使用Rx-WinForms

這個例子雖然成功了,但是並沒有比BeginInvoke(Action action)的方案有明顯的進步之處。在一個事件流中再次使用Ovservable.Start()開啟新的觀察者讓人更加摸不著頭腦。這並不是Rx的問題,而是事件模型在UI程式設計中存在侷限性:不方便使用非同步,不具備可測試性等。以XMAL和MVVM為核心的UI程式設計模型將在未來處於主導地位,由於在MVVM中可以將UI繫結到一個Command,從而解耦了事件模型。

開源專案ReactiveUI提供了一個以Rx基礎的UI程式設計方案,可以使用在XMAL和MVVM為核心的UI程式設計中,例如:Xamarin,WFP,Windows Phone8等開發中。

注:在WPF中使用ObserveOn()需要安裝Rx-WPF

3、再來一個例子,讓我們感受一下Rx的魅力

介面上有兩個Button分別為+和-操作,點選+按鈕則+1,點選-按鈕則-1,最終的結果顯示在一個Label中。
這樣的一個需求使用經典事件模型只需要維護一個內部變數,兩個按鈕的Click事件分別對變數做加1或減1的操作即可。
Rx作為一種函數語言程式設計模型講求immutable-不可變性,即不使用變數來維護內部狀態。

這個例子使用了IObservable的”謂詞”來對事件流做了一些操作。

  • Select跟Linq操作有點類似,分別將兩個按鈕的事件變形為IObservable(1)和IObservable(-1);
  • Merge操作將兩個事件流合併為一個;
  • Scan稍顯複雜,對事件流做了一個摺疊操作,給定了一個初始值,並通過一個函式來對結果和下一個值進行累加;

下面就讓我們來看看IObservable中常用的“謂詞”

四、IObservable中的謂詞

IObservable的靈感來源於LINQ,所以很多操作也跟LINQ中的操作差不多,例如Where、First、Last、Single、Max、Any。
還有一些“謂詞”則是新出現的,例如上面提到的”Merge”、“Scan”等,為了理解這些“謂詞”的含義,我們請出一個神器RxSandbox

1、Merge操作,從下面的圖中我們可以清晰的看出Merge操作將三個事件流中的事件合併在了同一個時間軸上。

2、Where操作則是根據指定的條件篩選出事件。

有了這個工具我們可以更加方便的瞭解這些“謂詞”的用途。

五、IObservable的建立

Observable類提供了很多靜態方法用來建立IObservable,之前的例子我們都使用FromEventPattern方法來將事件轉化為IObservable,接下來再看看別的方法。

Return可以建立一個具體的IObservable:

Create也可以建立一個IObservable,並且擁有更加豐富的過載:

Range方法可以產生一個指定範圍內的IObservable

Generate方法是一個摺疊操作的逆向操作,又稱Unfold方法:

Interval方法可以每隔一定時間產生一個IObservable:

Subscribe方法有一個過載,可以分別對Observable發生異常和Observable完成定義一個回撥函式。

還可以將IEnumerable轉化為IObservable型別:

也可以將IObservable轉化為IEnumerable

六、Scheduler

Rx的核心是觀察者模式和非同步,Scheduler正是為非同步而生。我們在之前的例子中已經接觸過一些具體的Scheduler了,那麼他們都具體是做什麼的呢?

1、先看下面的程式碼:

當我們不使用任何Scheduler的時候,整個Rx的觀察者和主題都跑在主執行緒中,也就是說並沒有非同步執行。正如下面的截圖,所有的操作都跑在threadId=1的執行緒中。

當我們使用SubscribeOn(NewThreadScheduler.Default)或者SubscribeOn(ThreadPoolScheduler.Instance)的時候,觀察者和主題都跑在了theadId=3的執行緒中。

這兩個Scheduler的區別在於:NewThreadScheduler用於執行一個長時間的操作,ThreadPoolScheduler用來執行短時間的操作。

2、SubscribeOn和ObserveOn的區別

上面的例子僅僅展示了SubscribeOn()方法,Rx中還有一個ObserveOn()方法。stackoverflow上有一個這樣的問題:What’s the difference between SubscribeOn and ObserveOn,其中一個簡單的例子很好的詮釋了這個區別。

  • 當我們註釋掉:SubscribeOn(thread1)和ObserveOn(thread2)時的結果如下:

    觀察者和主題都跑在name為Main的thread中。

  • 當我們放開SubscribeOn(thread1):

    主題和觀察者都跑在了name為Thread1的執行緒中

  • 當我們註釋掉:SubscribeOn(thread1),放開ObserveOn(thread2)時的結果如下:

    主題跑在name為Main的主執行緒中,觀察者跑在了name=Thread2的執行緒中。

  • 當我們同時放開SubscribeOn(thread1)和ObserveOn(thread2)時的結果如下:

    主題跑在name為Thread1的執行緒中,觀察者跑在了name為Thread2的執行緒中。

至此結論應該非常清晰了:SubscribeOn()和ObserveOn()分別控制著主題和觀察者的非同步。

七、其他Rx資源

除了.net中的Rx.net,其他語言也紛紛推出了自己的Rx框架。

參考資源:

http://rxwiki.wikidot.com/101samples

http://introtorx.com/Content/v1.0.10621.0/01_WhyRx.html#WhyRx

http://www.codeproject.com/Articles/646361/Reactive-Programming-For-NET-And-Csharp-Developers

相關文章