Udacity调试课笔记之断言异常
这一单元的内容不是很多,如Zeller教授所说,就是如何写、检查断言,并如何使用工具实现自动推导出断言的条件。
现在,多数的编程语言,尤其是高级编程语言都会有内置的断言语句或断言函数。而随手编写个简易的断言也不件难事。使用内置的断言会有很多优点,比如获知出错断言的位置,可以通过编程语言的编译参数等来打开或关闭断言——即所谓的优化。
个人觉得,本单元的笔记想写成一篇博文会比较空。算起来,上一单元教授了一个方法、过程,可以让人去遵循、实践。这一单元教的断言,想得简单一点,就只是一条声明语句或函数,仅仅是语法,而且只是一条语法,那有什么可说的;从复杂角度来看,如何准确把握断言条件的选择,这又要靠个人经验的积累,也不是一门甚至一个单元的课程能讲清楚的。所以本单元会比较空洞。
言归正传,Zeller教授把断言分为两种,在测试代码中的断言可以检测某一次运行的结果;而在源代码中的断言则可以在每次运行时进行检测——所以Zeller教授强烈建议在代码,哪怕是产品代码中,都要保留断言。
断言的作用想来认识它的人心里也都清楚,不就是判断异常状态吗?Zeller教授将其细分成三点:
1、捉虫,尽早地发现错误。
2、点有什么区别。
3、文档,断言的条件、它的执行情况都可以作为文档。尤其是根据断言设置的条件,不仅是检查测试的结果,同时还可以生成测试——的用例。
而为了更好地实现断言的目标,可以给每个公共函数设置前置条件断言和后置条件断言。在函数的一开始,用前置条件检查参数性质;在函数结束之前,用后置条件检查结果。
只要函数的大小合理(一个函数1000行就什么都不用说了),那么这些公共函数的前置条件、后置条件综合起来,就会织成一张很大的网,网住程序的大部分状态,减少bug的藏身之处。如果在运行时,断言被触发,那将会离实际的bug更近,从缺陷代码到断言异常之间的程序状态就更少,就更好调试;如果断言最终一个都没被触发,也能因为断言保证了程序一些部分的正确性,而减少调试的工作量。
对于单个函数而言,前置、后置条件断言相当有用。而从整个程序的角度来看,断言也有用武之地。
程序中,有一些数据对象可能会若干个要保持一致的性质。比如像数据结构——树,就一定要无环,所以在增删结点操作之前、之后都需要检查这一性质,正如前置条件断言对参数性质的检查一样。这种始终保持一致的性质就叫数据不变量。
由于数据不变量主要用于大型、复杂的数据结构,在它们的操作类函数的开始和结尾进行性质检查,所以个人想来,数据不变量可以算作是个扩大、统一版的前置、后置条件断言。
Zeller教授举了两个例子。一个是时间炸弹——平常玩潜伏的缺陷代码(遵循Zeller教授的观点,尽量不用bug一词),但一到关键时候,就会迫不及待地蹦跶出来,把你炸个焦头烂额。所以用数据不变量来严密监控之。随便吐槽一句,《潜伏》结局余则成学母鸡这个举动也太显眼了吧?
二是复杂的红黑树。红黑树要保持一致的性质就多了,Zeller教授一下就写出了5个检查函数,略晕。
另外还存在一个系统不变量,定义是在整个执行过程中保持不变的性质。呃,这个定义相当之含糊。我想不到特别适当的例子。Zeller教授举的是 C/C++内存溢出。但是我觉得防止内存溢出不能算作是一个性质,故而认为这个例子举得不恰当。
讲了这么多有的没的,归根到底,还是只关心怎么用,有没有比较推荐的使用方式。Zeller教授推荐步骤:
1、定义数据不变量。根据数据结构的性质,确定数据不变量,检查对数据结构的操作,尽早发现错误。
2、定义前置条件。检查函数参数的性质,相对来说,比较容易找到适当的前置条件。
3、定义后置条件。检查返回结果。在较难确定的条件下,后置条件可以相对放宽松些。
4、系统不变量则是根据程序需要来定义。
而如何自动推导不变量条件,Zeller教授推荐一个工具Daikon,编程练习写个简单版的Daikon。这个工具的核心思想是通过多次运行,用运行中函数的每个参数、返回结果的值,在工具的模式库中进行匹配。匹配上的模式保留,不匹配的删掉。很多次运行并分析后,留下的模式就作为参考的不变量条件。
思想很简单,穷举加匹配,重点就是看模式库中是否存在需要的模式了。库中模式越多,找到正确不变量条件的机会就越高,结果也就是花费时间越多。个人感觉,在对代码熟悉、业务熟悉的条件下,比如深刻理解了需求后,写出的代码,自己动脑寻找的不变量条件,应该要比这个自动工具的质量高。
使用断言还有一些注意事项:
1、断言也是费时的,尤其是检查内容比较复杂时,性能影响越大。所以可以关掉。
2、断言的条件中,应该只含有结果、getter一类,而不应该有设置器(setter)等进行操作的函数或语句。
3、断言不该用于检查公共前置条件。看不明白是吧?大概就是说,一定要检查的一些内容不应该用断言,因为断言可能被关掉。
Zeller教授举的是第三方输入,我不太熟悉。我就改举java里的IO,或是所有语言里打开文件的操作。为防止打开文件失败,一定要用if 或 try ... Catch语句来检查打开状态,否则一旦断言被关掉,这个后果……反而会出错。
最后就是来讨论下,为什么要用断言异常了。C/C++和Python里虽然断言默认是打开、支持的,但我很少用过。而Java里更是默认不使用断言异常。就像上面说的,可以用if或try...catch 来代替断言。断言可有两个缺点,一是影响性能,二是对用户不友好,你看,一旦触发断言,程序90%跟你说不玩了,这太不友好了。
先列Zeller教授举的断言三优点:
1、程序及时退出,哪怕只是小错误而退出,也比坏数据好,因为坏数据不知道会造成什么样的后果。很多人说的史上最昂贵的bug,阿丽亚纳五号运载火箭,就因为惯性制导系统无法将一个64位的数据转换到16位格式而烧掉了5亿美元。
Zeller教授说,本来开发时是有对格式转换进行检查的断言的,结果产品中因为考虑性能问题,能关掉了这些断言。另外即使断言被触发,这火箭的软件系统也有足够优秀的异常处理机制来继续工作。所以可能就因为节省了这么一些断言,而创造了世上最奢侈的烟花。和火箭、航天飞机放的烟花比起来,北京奥运的烟花花费确实算不上多……
2、断言可以让调试更简单。理由之前说过,更早地发现错误,更少的bug位置。
3、方便回溯缺陷。从出错断言开始,往回找,和2差不多的。
不过,Zeller教授也承认可以关闭一些影响性能的断言。拿红黑树来说,没必要在一次时间复杂度O(log n) 的插入操作的前面,分别进行时间复杂度为O(n)的遍历操作,就为了检查是否存在环这一性质。这样的断言没必要。
再来说说我的理解。首先是为什么if和try...catch不能代替断言,也就是为什么还要用断言,这么一个不友好的语句。
个人理解,因为一个最简单的断言函数大概就是,如果断言条件为假,则抛出异常。其实也就是一个if语句能完成的事。但是一个简单的if语句抛出的异常所能显示的信息,不如标准的断言异常的信息多。而一个assert(condition)在很多时候从便捷上、作用上都高于一个if (condition) throw XXX; 所以If 不能代替断言。
而try...catch的情况相似,除了像IO等操作可能已经有内置的异常机制,面对新的异常条件时,同样要用if(condition)throw 异常,或assert(condition)来抛出异常,方能catch到,用if的话,也就可能需要定义新的异常类型,更加不如断言来得方便。当然try...catch和断言功能上重叠得不多,更加没有取代的可能了。
try...catch更像是断言的母亲,在断言捣蛋、对用户不好的时候,她来揪着断言的小耳朵,给用户赔礼道歉。
这一单元就这么简单:
会用断言
勤用断言
善用断言