Swift # 异常处理
面向轨道编程 - Swift 中的异常处理
问题
在开发过程中,异常处理算是比较常见的问题了。
举一个比较常见的例子:用户修改注册的邮箱,大概分为以下几个步骤:
- 接收到一个用户的请求:我要修改邮箱地址
- 验证一下请求是否合法,将请求进行格式转化
- 更新以前的邮箱地址记录
- 给新的邮箱地址发送验证邮件
- 将结果返回给用户
上面的步骤如果一切顺利,那代码肯定干净利落,但是人生不如意十有八九,上面的步骤很容易出现问题:
- 用户把邮箱地址填成了家庭地址
- 用户是个黑客,没登录就发送了更新请求
- 发送验证邮件的时候服务器爆炸了,发送邮件失败
各种异常都会导致这次操作的失败。
方案一
在传统的处理方案里,一般是遇到异常就往上抛:

这种方案想必大家都不陌生,比如下面这段代码:
NSError *err = nil;
CGFloat result = [MathTool divide:2.5 by:3.0 error:&err];
if (err) {
NSLog(@"%@", err)
} else {
[MathTool doSomethingWithResult:result]
}
方案二
而另一种方案,则是将错误的结果继续往后传,在最后统一处理:

这种方案有两个问题:
- 在发生异常的时候,如何把异常继续传给下面的函数?
- 当整个流程结束的时候,一个函数如何输出多个结果?
车轨
我们把方案二抽象出来,就像是一段车轨:

对于同一个输入,会有 Success 和 Failure 两种输出结果,对于 Success 的情况,我们希望它能继续走到后面的流程里,而对于 Failure 的情况,它怎么处理并不重要,我们希望它能避开后面的流程:

于是乎,两段车轨拼接的时候,便成了这样:

那么三段什么的自然也不在话下了。我们把下面那根 Failure 的线路扩展一下,便会看到两条平行的线路,这便是“双轨模型” (Two Track Model) ,这是用“面向轨道编程”思想解决异常处理的理论基础。

这就是 “面向轨道编程” 。一开始我觉得这概念应该只是来搞笑的,仔细想想似乎倒也是很贴切。将事件流当做两条平行的轨道,如果顺利则在上行轨道,继续传递给下个业务逻辑去处理,如果出现异常也不慌,直接扔到下行轨道,一直在下行轨道传递到终点,在最后统一处理。
这样处理使得整个流程变成了一条双进双出的流水线,有点像是 shell 里的 pipeline ,上一次的输出作为下一次的输入,十分顺畅。而且拼接起来也很方便,我们可以把三段拼接成一段暴露给其他对象使用:

实现
接下来看看在 Swift 中如何应用这种思路处理异常。
首先我们需要两种类型的输出结果:
- 成功,返回某种类型的值
- 失败,返回一个 Error 对象或者失败的具体信息
照着这个想法,我们可以定义一个 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()
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) {
{
println(errorStr)
} else {
let value1 = / value
let value2 = value1 -
{
println(errorStr)
} else {
let value3 = / value2
println(value3)
}
}
}
cal() // 输入错误,我很抱歉
cal() // 4.0
cal() // 输入错误,我很抱歉
那么用面向轨道的思想怎么去解决这个问题呢?
大概是这个样子的:
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> {
{
return .Failure(errorStr)
} else {
/ value))
}
}
func cal2(value: Result<Float>) -> Result<Float> {
switch value {
case .Success(let v):
))
case .Failure(let str):
return .Failure(str)
}
}
func cal3(value: Result<Float>) -> Result<Float> {
switch value {
case .Success(let v):
{
return .Failure(errorStr)
} else {
/ 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() // 输入错误,我很抱歉
cal() // 4.0
cal() // 输入错误,我很抱歉
同学,放下手里的键盘,冷静下来,有话好好说。
反思
面向轨道之后,代码量翻了两倍多,而且似乎变得更难读了。浪费了大家这么多时间结果就带来这么个玩意儿,实在是对不起观众们热情的掌声。
仔细看下上面的代码, 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)
}
}
}
funnel 帮我们把上次的结果进行分流,只将 Success 的轨道对接到了下个业务上,而将 Failure 引到了下一个 Failure 轨道上。
接下来再回到栗子里,此时我们已经不再需要传入 Result 值了,只需要传入 value 即可:
func cal(value: Float) {
func cal1(v: Float) -> Result<Float> {
{
return .Failure(errorStr)
} else {
/ v))
}
}
func cal2(v: Float) -> Result<Float> {
))
}
func cal3(v: Float) -> Result<Float> {
{
return .Failure(errorStr)
} else {
/ 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) 这样的链式调用来获取计算结果。
“面向轨道”编程确实给我们提供了一个很有趣的思路。本文只是一个简单地讨论,进一步学习可以仔细阅读后面的参考文献。比如 ValueTransformation.swift 这个真实的完整案例,以及 antitypical/Result 这个封装完整的 Result 库。文中的实现方案只是一个比较简单的方法,和前两种实现略有差异。
面向铁轨,春暖花开。愿每段代码都走在 Happy Path 上,愿每个人都有个 Happy Ending 。
文章来源:http://blog.callmewhy.com/2015/04/20/error-handling-in-swift/
|--> Copyright (c) 2015 Bing Ma.
|--> GitHub RUL: https://github.com/SpongeBob-GitHub
Swift # 异常处理的更多相关文章
- Swift异常处理:throw和rethrow
Swift异常处理体现了函数式语言的特性.因此我们能够传一个会抛出异常的函数闭包(高阶函数)作为參数传到还有一个函数中(父函数),父函数能够在子函数抛出异常时直接向上抛出异常,这时用rethrowke ...
- iOS - Swift 异常处理
前言 在 Swift 1.0 时代是没有异常处理和抛出机制的,如果要处理异常,要么使用 if else 语句或 switch 语句判断处理,要么使用闭包形式的回调函数处理,再要么就使用 NSError ...
- Swift异常处理
在Swift里,抛出的异常必须继承Error这个协议.那么这个协议是什么呢? 按住command再点击Error我们可以看到, public protocol Error { } extension ...
- Swift异常处理的try?与try!
首先要明白抛出异常后异常的运动:异常被抛出后,中断整个处理,异常不断向外层(范围)传递,直到遇到catch代码块群,会与catch代码块的条件进行匹配,匹配符合则进入此代码块处理.如果遇到没有条件的c ...
- swift 中异常的处理方法
swift 中什么时候需要处理异常,在调用系统某个方法的时,该方法最后有一个throws 说明该方法会抛出异常,如果一个方法抛出异常,那么需要对该异常进行处理 swift 异常处理提供了三种方法 方式 ...
- Swift try try! try?使用和区别
Swift try try! try?使用和区别 一.异常处理try catch的使用 1. swift异常处理 历史由来 Swift1.0版本 Cocoa Touch 的 NSError ,Swif ...
- Swift 使用 日常笔记
//------------------- var totalPrice: Int = { willSet(newTotalPrice) { //参数使用new+变量名且变量名首地址大写 printl ...
- Mac终端使用swift REPL异常处理方法
Mac终端使用swift REPL异常处理方法 终端使用swift命令出现 warning: Swift error in module libmarisa.dylibDebug info from ...
- Swift 2.0 异常处理
转自:http://www.jianshu.com/p/96a7db3fde00 WWDC 2015 宣布了新的 Swift 2.0. 这次重大更新给 Swift 提供了新的异常处理方法.这篇文章会主 ...
随机推荐
- JList用法小结
JList用法小结 分类: JAVA技术2007-08-11 01:02 18485人阅读 评论(11) 收藏 举报 stringvectorclassjavaactionobject ...
- bat(批处理文件)初步 第一篇 基本符号
近期我使用的一款软件中须要大量的环境变量设置,而我又不想讲这些变量都加入到系统的环境变量中,一方面是由于有一些同名的库文件的版本号却不一样,都 写在系统环境中会相互干扰:还有一方面则是大部分的路径仅仅 ...
- 在内存中建立 MySQL 的临时目录(转)
MySQL 系统会在内存(MEMORY)和磁盘(MyISAM)中建立临时表,如何能知道在磁盘中建立了多少临时表以及在内存中建立多少临时表呢?你可以通过下面命令获知: ? 1 2 3 4 5 6 7 m ...
- 如何对 GIT 分支进行规划? (转)
项目背景: 该项目是在2011年11月份使用Asp.net三层帮荷兰某个客户开发的机票预定系统 该客户主要是做中国与欧洲的旅行社业务,特别是最近两年由于中国的发展因此客户也越来越重视机票业务 于是他们 ...
- Golang基于学习总结
1.不支持继承 重载 ,比方C++Java的接口,接口的改动会影响整个实现改接口的类行为的改动,Go 设计者觉得这一特点也许根本没用. 2.必不论什么函数定义必须花括号跟在函数声明后面而不能换行 如 ...
- SQL Server 文件路径
原文:SQL Server 文件路径 系统数据库文件路径 master <drvie>:\program files\microsoft sql server\mssql10.<in ...
- Cloudera impala简单介绍及安装具体解释
一.Impala简单介绍 Cloudera Impala对你存储在Apache Hadoop在HDFS,HBase的数据提供直接查询互动的SQL.除了像Hive使用同样的统一存储平台,Impala也使 ...
- nodejs的安装和使用
一 下载 下载地址: https://nodejs.org/download/ 二 安装 1 win7系统直接双击,就能够执行了: 2 win8须要使用管理员权限执行,否则会报错Error 2502, ...
- .NET单元测试艺术(2) - 第一个单元测试
List 2.1 使用[SetUp]和[TearDown]特性 using System; using System.Collections.Generic; using System.Linq; u ...
- Oracle数据表被drop后的恢复
对于被drop的表和索引,都会存放在回收站中(所以对于生产的数据库必须设置好回收站功能) 由于本次生成环境在drop掉已有的表后,又一次创建了很多的表,全部直接还原的话会提示原有对象存在,表名反复.当 ...