在开发过程当中,异常处理算是比较常见的问题了。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 。
参考文献: