问题

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

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

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

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

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

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

方案一

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

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

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(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 。

参考文献:

  • 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. ios – 使用swift进行异常处理

    catch来处理它.如果故事板中没有视图控制器,则无法执行任何操作.这是程序员的错误,创建它的人应该处理这些问题.你不能因为这种错误而责怪iOS运行时.

  2. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  3. Swift41/90Days - 面向轨道编程 - Swift 中的异常处理

    问题在开发过程中,异常处理算是比较常见的问题了。我们把下面那根Failure的线路扩展一下,便会看到两条平行的线路,这便是“双轨模型”,这是用“面向轨道编程”思想解决异常处理的理论基础。这就是“面向轨道编程”。也就是说具体的业务只需要处理灰色部分的逻辑:“面向轨道”编程确实给我们提供了一个很有趣的思路。比如ValueTransformation.swift这个真实的完整案例,以及antitypical/Result这个封装完整的Result库。面向铁轨,春暖花开。

  4. 面向轨道编程 - Swift 中的异常处理

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. swift详解之十-------------异常处理、类型转换 ( Any and AnyObject )

    异常处理、类型转换注:本文为作者倾心整理,希望对大家有所帮助!在swift中,错误用复合ErrorType协议的值表示。swift处理异常和别的语言不同的是swift不会展开调用堆栈。在swift中throw语句的性能几乎和return一样通过try!所以上面的例子还能这么写结果是一模一样的Any和AnyObject的类型Swift为不确定类型提供了两种特殊类型别名:AnyObject可以代表任何class类型的实例。Any可以表示任何类型,包括方法类型。

  6. Swift2网络操作和异常处理

    相信写过Swift的人应该都知道Alamofire,它是AFNetworking的Swift版本,同一个作者写的。"的哲学,不过Swift一直很强调安全性,Apple显然也并不仅仅满足于让Swift困守iOS开发领域,加上早就公布了年底要开源,大家也很期待它作为一门通用编程语言在其他领域的作为。从各方面来看,Swift2.0增加了对异常处理的支持都在情理之中。在我看来异常处理最重要的用途有两点:写底层框架的时候可以抛出一些异常让框架的使用者去处理,这样框架会显得更加灵活。

  7. Swift 2.0 异常处理

    WWDC2015宣布了新的Swift2.0.这次重大更新给Swift提供了新的异常处理方法。在Swift中,guard有点像if但是他们有两个非常重要的区别guard必须强制有else语句只有在guard审查的条件成立,guard之后的代码才会运行。所以,使用catch你可以对异常的解析进行更为高级的处理7MyError.NotExist{//dealwithnotexistMyError.OutOfRange{//dealwithnotexist}这里值得提一下在Swift2.0中一个跟异常处理没有关系

  8. swift注意点

    如果我们想要像Objective-C里那样定义可选的接口方法,就需要将接口本身定义为Objective-C的,也即在protocol定义之前加上@objc。另外和Objective-C中的@optional不同,我们使用没有@符号的关键字optional来定义可选方法//swift中的错误处理,Objective-C没有原生的异常处理机制。后来通过添加NSException类,还有NS_DURING,NS_HANDLER和NS_ENDHANDLER宏才有了异常处理。这种方案现在被称为“经典的异常处理”,还

  9. Swift2.0-异常处理Exception handler

    Swift2.0-异常处理前言关于我们为什么要使用异常处理,请看百度百科为我们作出的描述,想要更详细的资料请点这里以上摘自百度百科:关联,在Objective-C中,异常处理一般都是使用NSError类接收异常和抛出异常,使用方法像这样不得不说,Swift的异常处理更为优雅,下面会重点介绍。去执行该函数不建议使用try!

  10. Swift try 异常处理机制

    不处理异常如果我不想处理异常怎么办,或者说,我非常确定某个方法或者函数虽然声明会抛出异常,但是我自己知道我在使用时候是绝对不会抛出任何异常的。当然,如果你使用try!,而你的方法或者函数抛出了异常,那么你会得到一个运行中异常所以我们开发者需要慎用哦。

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部