我们的教程已经来到了第九期,还有一期就要结束了。不知道你有没有跟随我的步伐一点点去实践呢,又有没有尝试把自己心里的某个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
函数就很普通,因此也就可以被普普通通的调用。
关于错误处理
的基本用法就先演示到这里了。下面请大家思考一个问题,是不是只要我的程序能在理想化的情况下运行,每一步都不出错,那么我就可以完全不需要错误处理
了呢?单就这个问题来说,答案是:对,是这么回事,如果一切顺利,其实错误处理
的机制没有任何价值。 那么为什么各种语言都要在错误处理
费心设计、健全机制呢?
这就牵扯出来一个重要的编程哲学问题,面向失败编程。这句话显然不是要劝我们写会运行失败的程序,而是在任何时候,编程都要考虑潜在导致失败的风险,要能捕捉这些风险,控制这些风险。就像优秀的棋手,每下一步棋,不光要思考之后怎样组织进攻,更要思考如何放手,而不至使自己陷入被动的境地。
那么为什么编程一定要考虑这些风险呢?这些风险就这么普遍么?如果你还有这样的疑问,说明你还没有认识到计算机世界以及人类世界的复杂。我们编写的程序,都是跑在计算机(此处把手机等各种智能设备、物联网设备类比为广义的计算机),最终被人或者其他计算机使用的。
这里面先说人,就是极为不靠谱的,他们不会按照你的设想操作你的程序,如果没有限制,他们会在输入窗口输入各种匪夷所思的内容,甚至有些邪恶的小伙伴会专门输入恶意的内容,期待看到你程序崩溃的样子。
然后再说机器,它们虽然没有情感,没有道德层面的好坏之分,但是它们也是不能太相信的存在。毕竟一部计算机由各种配件组成,而每一个配件都有坏掉的风险,比如当你尝试在有坏道的磁盘上读写文件,就会有文件丢失的风险。更何况把计算机链接在一起的网线,也是极为脆弱,相信你一定在新闻里看到过网线被挖断导致网络设施瘫痪的故事。
而业界频频提及的分布式应用,更是要把不靠谱的计算机用不靠谱的网络组成在一起提供程序功能,可想而知,如果放任自流,那么它出问题的概率更是提高了不知道多少倍。
所以如果认真观察,你就会发现,越是优秀的公司,越是优秀的技术方案,越是在对抗风险上不遗余力,甚至他们的软件产品的重要卖点都离不开对抗风险。所以控制风险,是写在每个程序基因里的,也是根植于每个程序员内心之中的。而错误处理
,就是这种设计哲学的最直观表达,是每个程序员都要熟悉,并且赖以生存的基石。
参考资料:
- https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html
- https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html