问题

在开发过程中,异常处理算是比较常见的问题了。

举一个比较常见的例子:用户修改注册的邮箱,大概分为以下几个步骤:

  • 接收到一个用户的请求:我要修改邮箱地址
  • 验证一下请求是否合法,将请求进行格式转化
  • 更新以前的邮箱地址记录
  • 给新的邮箱地址发送验证邮件
  • 将结果返回给用户

上面的步骤如果一切顺利,那代码肯定干净利落,但是人生不如意十有八九,上面的步骤很容易出现问题:

  • 用户把邮箱地址填成了家庭地址
  • 用户是个黑客,没登录就发送了更新请求
  • 发送验证邮件的时候服务器爆炸了,发送邮件失败

各种异常都会导致这次操作的失败。

方案一

在传统的处理方案里,一般是遇到异常就往上抛:

这种方案想必大家都不陌生,比如下面这段代码:

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: Tinit(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 / valuelet value2 = value1 - 1if value2 == 0 {println(errorStr)} else {let value3 = 4 / value2println(value3)}}
}
cal(2)    // 输入错误,我很抱歉
cal(1)    // 4.0
cal(0)    // 输入错误,我很抱歉

那么用面向轨道的思想怎么去解决这个问题呢?

大概是这个样子的:

final class Box<T> {let value: Tinit(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 。


参考文献:

  • Railway Oriented Programming - error handling in functional languages
  • Swift: Putting Your Generics in a Box
  • Error Handling in Swift: Might and Magic
  • Error Handling in Swift: Might and Magic—Part II
  • Return Types can Capture Async Processes and Failures
  • Going Beyond Guard Clauses in Swift
  • Functional Error Handling in Swift Without Exceptions
  • ValueTransformation.swift
  • antitypical/Result

Swift41/90Days - 面向轨道编程 - Swift 中的异常处理相关推荐

  1. java 切面 注解_Java自学之spring:使用注解进行面向切面编程(AOP)

    学习目的:学会使用注解进行面向切面编程(AOP),实现在面向切面编程(AOP)中,使用XML配置完成的操作. Part 1 修改cn.vaefun.dao.UserServiceImpl.java,在 ...

  2. Swift 面向协议编程 基础篇 (一) 介绍

    前言 好久没有写文章了,期末复习周也到了.在复习的同时顺便开了一个专题,面向协议编程,[ 基础篇 ],[ 进阶篇 ],[ 实践篇 ]. 介绍 首先,面向对象(OOP)大家并不陌生,苹果的很多框架都是以 ...

  3. Swift 面向协议编程的那些事

    一直想写一些 Swift 的东西,却不知道从何写起.因为想写的东西太多,然后所有的东西都混杂在一起,导致什么都写不出来.翻了翻以前在组内分享的一些东西,想想把这些内容整理下,写进博客吧.我对计划要写的 ...

  4. 知道swift为什么是面向协议编程么?不知道,还不快来学习!

    swift为什么是面向协议编程 面向协议编程 (Protocol Oriented Programming) 是 Apple 在 2015 年 WWDC 上提出的 Swift 的一种编程范式. 面向协 ...

  5. 【AOP 面向切面编程】Android Studio 中配置 AspectJ ( 下载并配置AS中 jar 包 | 配置 Gradle 和 Gradle 插件版本 | 配置 Gradle 构建脚本 )

    文章目录 一.AspectJ 下载 二.拷贝 aspectjrt.jar 到 Android Studio 三.配置 Gradle 和 Gradle 插件版本 四.配置 Gradle 构建脚本 一.A ...

  6. 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术

    文章目录 一.Android 中的 Java 源码打包流程 1.Java 源码打包流程 2.字符串常量池 二.Android 中的字节码操作方式 一.Android 中的 Java 源码打包流程 Ja ...

  7. python面向接口编程_Python 中的面向接口编程

    前言 "面向接口编程"写 Java 的朋友耳朵已经可以听出干茧了吧,当然这个思想在 Java 中非常重要,甚至几乎所有的编程语言都需要,毕竟程序具有良好的扩展性.维护性谁都不能拒绝 ...

  8. python 接口编程_Python 中的面向接口编程

    前言 "面向接口编程"写 Java 的朋友耳朵已经可以听出干茧了吧,当然这个思想在 Java 中非常重要,甚至几乎所有的编程语言都需要,毕竟程序具有良好的扩展性.维护性谁都不能拒绝 ...

  9. Java中的面向接口编程

    面向接口编程是很多软件架构设计理论都倡导的编程方式,学习Java自然少不了这一部分,下面是我在学习过程中整理出来的关于如何在Java中实现面向接口编程的知识.分享出来,有不对之处还请大家指正. 接口体 ...

最新文章

  1. java 底层运行_从表面到底层丨Java和JVM的运行原理,现在带给你
  2. mysql数据库读写操作_一看就会,MySQL数据库的基本操作(二)
  3. Twitter Storm安装配置(Ubuntu系统)单机版
  4. Linux 守护进程创建原理及简易方法
  5. java文件流下载excel_React获取Java后台文件流下载Excel文件
  6. 设计模式速查手册-创建型
  7. 三分钟快速理解javascript内存管理
  8. ubuntu 14.04 nginx php mysql_Ubuntu 14.04安装Nginx+PHP+MySQL
  9. live2d碰撞_Unity Live2D 模型(与UI)拖拽功能 实现源码
  10. bzoj1574[Usaco2009 Jan]地震损坏Damage*
  11. 网站视频倍速播放和进度自定义调整
  12. python平方和psum_python求和函数sum()详解
  13. 123457123456#0#-----com.yuming.FromPuzzleGame01--前拼后广--宝宝农场拼图cym
  14. 微型计算机怎么插入光盘,解决Win 7读光盘“请将磁盘插入DVD驱动器”故障
  15. 可以弹奏的钢琴页面(HTML实现)
  16. L Norms 范数
  17. 苹果待处理订单要多久_苹果官网准备发货到发货要多久呀?
  18. 互联网产品的需求分析
  19. Improving Twitter Sentiment Classification Using Topic-Enriched Multi-Prototype Word Embeddings
  20. 《简化iOS APP上架流程,App Uploader助你搞定!》

热门文章

  1. unity桌面设置vnc_win7系统通过VNCViewer访问Ubuntu桌面环境的操作方法
  2. linux 加密可逆,RSA加密是可逆的吗
  3. 算术平均值滤波matlab程序,基于S7-1200 AD采样的高效数字滤波算法的设计与实践...
  4. 智能车竞赛技术报告 | 智能车视觉 - 太原工业学院 - 晋速-轩辕星
  5. 2021年春季学期-信号与系统-第三次作业参考答案-第十一道题
  6. 是否患有新冠肺炎? 你咳嗽一声
  7. 常熟理工电气院永不言败
  8. ce修改器传奇刷元宝_真原始传奇刷元宝方法 不封号刷元宝技巧
  9. swift 组件化_京东商城订单模块基于 Swift 的改造方案与实践
  10. 服务器负载不高 响应慢_负载均衡有哪几大类别?