正确性和健壮性
正确性
- 行为要严格符合贵越重定义的行为
- 永远不给错误的结果
- 让开发者更容易;输入错误,直接结束
- 对内的实现,倾向于正确
健壮性
指的是在异常情况下,软件能够正常运行的能力
- 没有被规约覆盖的情况是“异常情况”,出现规约定义外的情形时,软件要做出恰当的反应
- 尽可能让软件运行而不是总是推出
- 让用户变得更容易;出错也可以容忍
- 对外的接口倾向于健壮
e.g.
| 问题 | 正确性做法 | 健壮性做法 |
|---|---|---|
| 视频文件有坏帧 | 停止播放,提醒损坏 | 跳过坏帧,从下一正确的继续播放 |
| 用户输入奇怪的格式 | 提示输入错误 | 尝试用不同日期格式解析,告诉用户解析结果 |
| 代码的括号不匹配 | 编译错误 | 尝试补充不匹配的括号继续编译 |
健壮性原则(Postel`s Law)
- 对自己的代码要保守,对用户的行为要开放
- 严于律己,宽以待人
- 总是假定用户恶意,家底自己代码可能失败
- 吧用户想象成可能输入任何东西的傻瓜
软件中的问题
-
Fault,故障
- bugs
- 代码实现的错误
-
Error 错误
- 不正确的内部运行状态
-
Failure
- 运行时候表现出来的、外在的、和规约不一致的行为
-
Problem
- 笼统/泛指各种不正确的情况
-
Mistake
- 侧重描述人的行为存在失误
-
Defect
- 泛指各类设计和实现中存在的问题,导致bug的根源
-
Bug
- 同fault
-
Exception
- 一种应对故障、处理错误的编程机制
-
Anomaly
- 常用于算法领域描述与正常分布不一致的情况
Fault => Error => Failure
导致Failure的必要条件:
- 可达性
- 执行到包含Fault的代码
- 感染性
- 执行Fault的代码后,程序状态是Error
- (错误状态和运行环境有关,在特定环境下才出错)
- 传播性
- Error能传递到程序的输出,被外界感知
Mistake 和 Defect
mistake指的是程序员的错误行为
defect是软件的内在性质,可能导致执行结果和预期(用户希望的结果)不一致
- 程序的果实可能导致软件出现defect
- 并非所有defect都是由于mistake导致的
提升正确性和健壮性的方法
- 故障拒绝
- 防御性变成、代码审查、形式化验证
- 故障检测
- 测试、调试、测试驱动开发
- 容错
- 在运行时通过一定手段消除影响
- 冗余、备份、重试
防御性编程
-
子程序不因传入错误数据而被破坏,哪怕是由其它子程序产生的错误数据
-
其核心是承认程序都会有问题,都需要被修改,这是保护的基础
常用技术:
- 输入检查
- 断言
- 错误处理
- 异常处理
- 隔栏
输入检查
检查:
- 文件
- 用户输入
- 网络
- 其他外部接口
- 参数类型是否一致
- 参数值是否合法
- 长度要求
Assert
用来检查永远不应该发生的状况
断言只在开发阶段被编译到目标代码中,而在生成产品代码时不编译到产品代码中
C++中的Assert
- 在头文件
中定义的assert宏 - false时候会停止运行
|
|
-
func1 finish
-
first
-
Assertion failed: n==1, file example1.cpp, line 6
技巧
- 可用断言在函数开始处检查传入参数的合法性
- 每个assert只检验一个条件
- 不要在断言中使用改变环境的语句,因为assert仅在debug阶段生效,如果这么做,会使程序在真正运行时出错
错误处理
理想情况:希望在发生错误情况时,不只是简单地终止程序运行,而是能够反馈错误情况的信息,并且能够对程序运行中已发生的事情做些处理。
异常
异常(Exception)是把代码中的错误或不正常事件传递给调用方代码的一种特殊手段
-
当一个函数出现自己无法处理的错误时,可以抛出异常,然后由该函数的直接或者间接调用者处理这个错误
-
异常不是bug
- “异常” 是在程序开发中必须考虑的一些特殊情况,是程序运行时就可预料的执行分支(注:异常是不可避免的,如程序运行时产生除 0 的情况、打开的外部文件不存在、数组访问的越界等)
- “Bug”是程序的缺陷,是程序运行时不被预期的运行方式(注:Bug是人为的、可避免的;如使用野指针、动态分配内存使用结束后未释放等)

-
异常的抛出规则
-
异常时通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
-
被选中的处理代码的调用链是,找到与该类型匹配且离抛出异常位置最近的那一个catch
-
抛出异常对象后会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会调用复制构造函数生成一个拷贝对象
-
-
异常的匹配规则
- 首先检查throw语句本身是否在try块内部,如果是,再在当前函数栈中查找匹配的catch语句。如果有匹配的直接跳到catch的地方执行
- 如果没有匹配的catch块或者throw语句不在try块内部,则退出当前函数栈,在调用函数的栈中查找匹配的catch
- 如果到达main函数,都没有匹配的catch,就会终止程序
- 找到匹配的catch会直接跳到catch语句执行,执行完后,会继续沿着catch语句后面执行

- bad_alloc: new的时候内存不足
- out_of_range
- ……
用智能指针,放着catch后内存没有释放
|
|
隔栏
如果所有的代码都做异常和错误处理,会使代码变得臃肿,可读性下降
隔栏(barricade)是在设计上简化错误处理的策略,将程序的外部和内部进行隔离
把某些接口选定为安全区域的边界,对穿越安全区域边界的数据进行合法性校验,并当数据非法时进行处理
- 隔栏的使用使断言和错误处理有了清晰的区分
- 隔栏外部的程序使用错误处理技术,外部的数据是不安全的
- 隔栏内部的程序就应该使用断言技术,因为传入程序内部的数据都已经过了隔栏的处理,应该是正确的,如果出错,则说明程序本身出错
防御式编程的使用考虑
- 防御式编程的矛盾
- 在产品开发阶段,希望显示出的错误越多越好,引入很多防御性的代码
- 在产品发布阶段,希望错误尽可能偃旗息鼓,尽量不要在使用中出现
- 如何权衡
- 过度的防御式编程也会引入问题
- 如果在程序的每一个想到的地方都进行参数检查、错误保护等,那么程序将变得臃肿而缓慢
- 更糟糕的是,防御式编程引入的额外代码增加了软件的复杂度,反而容易造成错误