我们的教程已经来到了第九期,还有一期就要结束了。不知道你有没有跟随我的步伐一点点去实践呢,又有没有尝试把自己心里的某个idea用代码的方式呈现出来呢?今天要讲的概念错误处理,几乎可以说是众多编程语言都离不开的特性,它早已成为编程世界里的一块基石,是构建复杂软件项目都离不开的,从这个角度上来说,它很基础。不过要学习错误处理需要掌握的系统关键字是很少的,几行代码就可以演示错误处理的过程,从这个角度上来说,它是简单的。

但是包括我在内的很多人,在初学编程的时候,都对错误处理的概念感到有些无所适从,并不是学不会,而是不太容易理解它存在的价值。今天咱们就来念叨下错误处理背后隐含的编程哲学问题,不过在探讨形而上的问题之前,咱们先来看看形而下的问题,到底错误处理长什么样。

想象这样一种场景,考试不及格(成绩小于60分)属于一种错误,我们可以这样定义:

struct 不及格:Error{}

这里我们定义了一个结构体,并且大大方方给它起了中文名(这样当然也是可以的,不用担心程序会报错,只是外国人不太容易看懂罢了。),同时让这个结构体实现了Error协议。这样我们就拥有了一个错误,你就可以在需要它的时候让他登场了,而“登场”就需要一个特殊的操作——throw

比如我们有一个设置成绩的函数:

func setScore(_ score:Int) throws {
    if score < 60{
        throw 不及格()
    }
        
    print(score)
}

注意上面函数的声明处有个throws,就是为了通知要调用这个函数的代码,告诉大家“我的逻辑有风险,调用我须谨慎”。所以调用函数的代码就得加个try,像下面这样:

try setScore(90)
try setScore(0)

而如果你真的关心函数调用如果出现异常,会出现怎样的异常的话,代码还要写得再丰富些才好:

do {
    try setScore(0)
} catch is 不及格{
    print("成绩不及格")
}

上面的代码相信你都能看懂,只是可能会觉得有点奇怪,绕这么个大圈子,就是想知道成绩是不是不及格,这样的意义在哪呢?别着急,让我们丰富下我们的示例。首先我们定义一个学生:

struct Student{
    let name:String
    let score:Int
    
    init (name:String, score:Int){
        self.name = name
        self.score = score
    }
}

let 张三 = Student(name: "张三", score: 60)

这个Student有两个常量,分别存储了姓名成绩。这次我们不关心他是不是考试及格了,我们关心这个Student的两个关键常量是否合法,比如姓名的字数应大于1成绩不能小于0

这次我们用enum定义错误会比之前用struct更加实用,在生产环境大家也多倾向于用enum来承载Error

enum StudentParamError:Error{
    case nameTooShort //姓名太短
    case nameTooLong //姓名太长
    case scoreIsNegative //得分是负数
    case scoreOver100 //得分大于100
}

现在我们改进下Student构造函数(init)

init (name:String, score:Int) throws{
    if name.count <= 1 {
        throw StudentParamError.nameTooShort
    }
    
    if name.count > 4 {
        throw StudentParamError.nameTooLong
    }
    
    if score < 0 {
        throw StudentParamError.scoreIsNegative
    }

    if score > 100 {
        throw StudentParamError.scoreOver100
    }

    self.name = name
    self.score = score
}

这样我们以后就可以安全的创建Student了,因为万一提供给它的参数有问题,它都可以自我审查,有问题及时抛出,而不需要我们在外围代码进行额外判断了。当然如果你关心Student创建失败的原因,还是要用上面介绍方法去对错误进行catch比较:

var zhangsan:Student?

do{
    zhangsan = try Student(name: "张三", score: 101)
}catch StudentParamError.nameTooShort{
    print("姓名太短")
}catch StudentParamError.nameTooLong{
    print("姓名太长")
}catch StudentParamError.scoreOver100{
    print("成绩大于100")
}catch StudentParamError.scoreIsNegative{
    print("成绩是负数")
}catch{
    print("其他未知问题")
}

或者你只关心宏观上是不是参数问题导致的,可以把上面的代码简化一下:

var zhangsan:Student?

do{
    zhangsan = try Student(name: "张三", score: 101)
}catch is StudentParamError{
    print("参数有误")
}catch{
    print("其他未知问题")
}

当然,还有一种情况也是最常见的情况,我不关心具体错误,我现在就要张三现在出来,立刻、马上:

let zhangsan = try? Student(name: "张三", score: 101)

此时zhangsan的类型就变成可选类型了。如果参数有误或者其他什么原因,构造张三的过程中报错了,那么你将得到一个nil,否则就会把一个完完整整的张三交到你手上。

错误处理是有一种传递机制在里面的,在一个函数内部调用其他有潜在风险的代码,你将有两种选择,一种是我不处理错误,我只传递风险,就像下面的代码:

func hello() throws{
    let zhangsan = try Student(name: "张三", score: 101)
    print("\(zhangsan.name),你好")
}

另一种就是我会把风险扼杀在摇篮里,不去打扰调用我的人:

func hello() {
    let zhangsan = try? Student(name: "张三", score: 101)
    if let zhangsan = zhangsan{
        print("\(zhangsan.name),你好")
    }
}

你能猜到这两种写法对调用者来说最大的区别是什么么?答案是:try hello()hello()。因为前者并没有规避风险,所以hello函数也是用throws标记了的,因而要调用它的地方,必须加上try以示对风险的重视。而后者的hello函数就很普通,因此也就可以被普普通通的调用。


关于错误处理的基本用法就先演示到这里了。下面请大家思考一个问题,是不是只要我的程序能在理想化的情况下运行,每一步都不出错,那么我就可以完全不需要错误处理了呢?单就这个问题来说,答案是:对,是这么回事,如果一切顺利,其实错误处理的机制没有任何价值。 那么为什么各种语言都要在错误处理费心设计、健全机制呢?

这就牵扯出来一个重要的编程哲学问题,面向失败编程。这句话显然不是要劝我们写会运行失败的程序,而是在任何时候,编程都要考虑潜在导致失败的风险,要能捕捉这些风险,控制这些风险。就像优秀的棋手,每下一步棋,不光要思考之后怎样组织进攻,更要思考如何放手,而不至使自己陷入被动的境地。

那么为什么编程一定要考虑这些风险呢?这些风险就这么普遍么?如果你还有这样的疑问,说明你还没有认识到计算机世界以及人类世界的复杂。我们编写的程序,都是跑在计算机(此处把手机等各种智能设备、物联网设备类比为广义的计算机),最终被或者其他计算机使用的。

这里面先说,就是极为不靠谱的,他们不会按照你的设想操作你的程序,如果没有限制,他们会在输入窗口输入各种匪夷所思的内容,甚至有些邪恶的小伙伴会专门输入恶意的内容,期待看到你程序崩溃的样子。

然后再说机器,它们虽然没有情感,没有道德层面的好坏之分,但是它们也是不能太相信的存在。毕竟一部计算机由各种配件组成,而每一个配件都有坏掉的风险,比如当你尝试在有坏道的磁盘上读写文件,就会有文件丢失的风险。更何况把计算机链接在一起的网线,也是极为脆弱,相信你一定在新闻里看到过网线被挖断导致网络设施瘫痪的故事。

而业界频频提及的分布式应用,更是要把不靠谱的计算机用不靠谱的网络组成在一起提供程序功能,可想而知,如果放任自流,那么它出问题的概率更是提高了不知道多少倍。

所以如果认真观察,你就会发现,越是优秀的公司,越是优秀的技术方案,越是在对抗风险上不遗余力,甚至他们的软件产品的重要卖点都离不开对抗风险。所以控制风险,是写在每个程序基因里的,也是根植于每个程序员内心之中的。而错误处理,就是这种设计哲学的最直观表达,是每个程序员都要熟悉,并且赖以生存的基石。


参考资料:

往期回顾: