在開發過程當中,異常處理算是比較常見的問題了。git
舉一個比較常見的例子:用戶修改註冊的郵箱,大概分爲如下幾個步驟:github
上面的步驟若是一切順利,那代碼確定乾淨利落,可是人生不如意十有八九,上面的步驟很容易出現問題:shell
各類異常都會致使此次操做的失敗。編程
在傳統的處理方案裏,通常是遇到異常就往上拋:swift
這種方案想必你們都不陌生,好比下面這段代碼:vim
NSError *err = nil; CGFloat result = [MathTool divide:2.5 by:3.0 error:&err]; if (err) { NSLog(@"%@", err) } else { [MathTool doSomethingWithResult:result] }
而另外一種方案,則是將錯誤的結果繼續日後傳,在最後統一處理:服務器
這種方案有兩個問題:app
咱們把方案二抽象出來,就像是一段車軌:ide
對於同一個輸入,會有 Success 和 Failure 兩種輸出結果,對於 Success 的狀況,咱們但願它能繼續走到後面的流程裏,而對於 Failure 的狀況,它怎麼處理並不重要,咱們但願它能避開後面的流程:函數
因而乎,兩段車軌拼接的時候,便成了這樣:
那麼三段什麼的天然也不在話下了。咱們把下面那根 Failure 的線路擴展一下,便會看到兩條平行的線路,這即是「雙軌模型」 (Two Track Model) ,這是用「面向軌道編程」思想解決異常處理的理論基礎。
這就是 「面向軌道編程」 。一開始我以爲這概念應該只是來搞笑的,仔細想一想彷佛倒也是很貼切。將事件流當作兩條平行的軌道,若是順利則在上行軌道,繼續傳遞給下個業務邏輯去處理,若是出現異常也不慌,直接扔到下行軌道,一直在下行軌道傳遞到終點,在最後統一處理。
這樣處理使得整個流程變成了一條雙進雙出的流水線,有點像是 shell 裏的 pipeline ,上一次的輸出做爲下一次的輸入,十分順暢。並且拼接起來也很方便,咱們能夠把三段拼接成一段暴露給其餘對象使用:
接下來看看在 Swift 中如何應用這種思路處理異常。
首先咱們須要兩種類型的輸出結果:
照着這個想法,咱們能夠定義一個 Result 枚舉用作輸出:
enum Result<T> { case Success(T) case Failure(String) }
利用 Swift 的枚舉特性,咱們能夠在成功的枚舉值裏關聯一些返回值,而後在失敗的狀況下則帶上失敗的消息內容。不過 enum 目前還不支持泛型,咱們能夠在外面封裝一個 Box
類來解決這個問題:
final class Box<T> { let value: T init(value: T) { self.value = value } } enum Result<T> { case Success(Box<T>) case Failure(String) }
再看下一開始咱們舉的那個例子,用這個枚舉類從新寫下就是這樣的:
var result = divide(2.5, by:3) switch result { case .Success(let value): doSomethingWithResult(value) case .Failure(let errString): println(errString) }
「看起來好像也沒什麼嘛,你不仍是用了個大括號處理兩種狀況嘛!」(嫌棄臉
確實正如這位熱情的朋友所說,寫完這個例子我也沒以爲有什麼優勢,難道我就是來搞笑的?
「並不。」(嚴肅臉
接下來咱們舉個栗子玩一玩。爲了更好的觀賞效果,請容許我使用浮誇的寫法和粗暴的命名舉這個栗子。
好比對於即將輸入的數字 x ,咱們但願輸出 4 / (2 / x - 1)
的計算結果。這裏會有兩處出錯的可能,一個是 (2 / x)
時 x
爲 0 ,另外一個就是 (2 / x - 1)
爲 0 的狀況。
先看下傳統寫法:
let errorStr = "輸入錯誤,我很抱歉" func cal(value: Float) { if value == 0 { println(errorStr) } else { let value1 = 2 / value let value2 = value1 - 1 if value2 == 0 { println(errorStr) } else { let value3 = 4 / value2 println(value3) } } } cal(2) // 輸入錯誤,我很抱歉 cal(1) // 4.0 cal(0) // 輸入錯誤,我很抱歉
那麼用面向軌道的思想怎麼去解決這個問題呢?
大概是這個樣子的:
final class Box<T> { let value: T init(value: T) { self.value = value } } enum Result<T> { case Success(Box<T>) case Failure(String) } let errorStr = "輸入錯誤,我很抱歉" func cal(value: Float) { func cal1(value: Float) -> Result<Float> { if value == 0 { return .Failure(errorStr) } else { return .Success(Box(value: 2 / value)) } } func cal2(value: Result<Float>) -> Result<Float> { switch value { case .Success(let v): return .Success(Box(value: v.value - 1)) case .Failure(let str): return .Failure(str) } } func cal3(value: Result<Float>) -> Result<Float> { switch value { case .Success(let v): if v.value == 0 { return .Failure(errorStr) } else { return .Success(Box(value: 4 / v.value)) } case .Failure(let str): return .Failure(str) } } let r = cal3(cal2(cal1(value))) switch r { case .Success(let v): println(v.value) case .Failure(let s): println(s) } } cal(2) // 輸入錯誤,我很抱歉 cal(1) // 4.0 cal(0) // 輸入錯誤,我很抱歉
同窗,放下手裏的鍵盤,冷靜下來,有話好好說。
面向軌道以後,代碼量翻了兩倍多,並且~~彷佛~~變得更難讀了。浪費了你們這麼多時間結果就帶來這麼個玩意兒,實在是對不起觀衆們熱情的掌聲。
仔細看下上面的代碼, switch
的操做重複而多餘,都在重複着把 Success 和 Failure 分開的邏輯,實際上每一個函數只須要處理 Success 的狀況。咱們在 Result
中加入 funnel
提早處理掉 Failure 的狀況:
enum Result<T> { case Success(Box<T>) case Failure(String) func funnel<U>(f:T -> Result<U>) -> Result<U> { switch self { case Success(let value): return f(value.value) case Failure(let errString): return Result<U>.Failure(errString) } } }
接下來再回到栗子裏,此時咱們已經再也不須要傳入 Result 值了,只須要傳入 value 便可:
func cal(value: Float) { func cal1(v: Float) -> Result<Float> { if v == 0 { return .Failure(errorStr) } else { return .Success(Box(2 / v)) } } func cal2(v: Float) -> Result<Float> { return .Success(Box(v - 1)) } func cal3(v: Float) -> Result<Float> { if v == 0 { return .Failure(errorStr) } else { return .Success(Box(4 / v)) } } let r = cal1(value).funnel(cal2).funnel(cal3) switch r { case .Success(let v): println(v.value) case .Failure(let s): println(s) } }
看起來簡潔了一些。咱們能夠經過 cal1(value).funnel(cal2).funnel(cal3)
這樣的鏈式調用來獲取計算結果。
funnel
起到了一個什麼做用呢?它幫咱們把上次的結果進行分流,只將 Success 的軌道對接到了下個業務上,而將 Failure 引到了下一個 Failure 軌道上。也就是說具體的業務只須要處理灰色部分的邏輯:
「面向軌道」編程確實給咱們提供了一個頗有趣的思路。本文只是一個簡單地討論,進一步學習能夠仔細閱讀後面的參考文獻。好比 ValueTransformation.swift 這個真實的完整案例,以及 antitypical/Result 這個封裝完整的 Result 庫。文中的實現方案只是一個比較簡單的方法,和前兩種實現略有差別。
面向鐵軌,春暖花開。願每段代碼都走在 Happy Path 上,願每一個人都有個 Happy Ending 。
參考文獻: