读程序员的制胜技笔记12_与Bug共存(下)

1. 亚伯拉罕·马斯洛(Abraham Maslow)在1966年所说:“如果你唯一的工具是一把锤子,你往往会把每个问题都看成钉子。”

2. 错误恐惧

2.1. 不是每一个bug都是由你的代码中的错误引起的,也不是每一个错误都意味着你的代码中存在一个bug

2.1.1. 开发人员本能地把所有的错误当作bug,并不约而同地、坚持不懈地把它们消灭

2.1.2. 对这些错误用平常心看待,这些所谓的错误是很正常的事情

2.2. 有关异常的真相

2.2.1. 异常可能是编程史上被误解最多的结构

2.2.1.1. 故障代码(failing code)放在一个try语句块里,然后加上一个空的catch语句块,就大功告成了

2.2.1.2. 开发者为整个应用程序添加了一个通用的异常处理程序,但实际上这个程序的工作原理就是忽略所有的异常,也就防止所有的崩溃

2.2.1.3. 如果像那样添加一个空的处理程序就是“万金油”的话,我们早就解决了所有bug

2.2.2. 异常是解决未定义状态问题的一种新方法

2.2.2.1. 在错误处理只用返回值的时代,有可能漏掉了对错误的处理,只假设它没问题,然后继续运行

2.2.2.2. 未知状态意味着你无法预测接下来会发生什么

2.2.3. 错误代码与异常情况不同

2.2.3.1. 在程序运行期间,如果异常情况没有得到处理,是可以检测出来的,而错误代码则不然

2.2.3.2. 对于那些未处理的异常,采取的解决办法也比较简单、粗暴:终止程序

2.2.3.3. 操作系统也是这样做的:如果应用程序出现无法处理的异常,操作系统就会终止该应用程序

2.2.3.3.1. UNIX操作系统的“内核恐慌”
2.2.3.3.2. Windows操作系统臭名昭著的“死亡蓝屏”

2.2.3.4. 在基于微内核的操作系统中问题不大,因为内核级组件的数量很少,甚至设备驱动也在用户空间中运行,对性能的影响也微乎其微,我们一般很难感受到

2.2.4. 对于异常,其实我们忽略的一件事就是它们是意外情况

2.2.4.1. 它们不是用于通用流控制,你可以通过结果值结合流控制来实现它

2.2.4.2. 异常是指在函数作用之外发生的事情,它不能再完成交给它实现的功能的情况

2.2.4.3. 像(a,b) => a/b这样的函数可以保证正常除法运算的执行,但当b的值等于0的时候,这个函数就没法执行,因为b等于0这个情况既是意外也没有被定义

2.2.5. 在每一步,你都可能遇到异常,如果没有被处理,或者处理不当,那就会有程序崩溃的可能性

2.3. 不要捕捉异常

2.3.1. 异常应该导致崩溃,因为这是在不引起进一步问题的情况下找出问题所在的最简单方法

2.3.2. 要害怕那些被空的catch语句块所掩盖的问题,它们潜伏在代码中,伪装成看起来好像正确的状态,在长时间的运行过程中积攒了不少问题,到最后导致明显的速度下降或一个突如其来的崩溃,比如OutOfMemoryException

2.3.3. 不必要的catch语句块可以防止一些崩溃,但它们可能会让你花费数小时来阅读日志

2.3.4. 应该避免使用未定义的catch语句块,因为它们太宽泛了,导致很多不相关的异常被捕获

2.3.5. 异常处理的第一条规则是,不要捕捉异常

2.3.6. 异常处理的第二条规则是IndexOutOf RangeExceptionat

2.3.7. 不要因为异常会导致崩溃就去捕捉异常

2.3.7.1. 如果它是由一个不正确行为引起的,那就修复引起它的错误

2.3.7.2. 如果它是由一种已知的可能性引起的,那么就在代码中为这种特定的情况加上明确的处理语句

2.3.7.3. 盲目地处理异常可能会掩盖你的代码中更深、更严重的问题

2.4. 容异性

2.4.1. 你的代码应该在不处理异常的情况下也能工作,即使是面临崩溃的时候也一样

2.4.2. 你应该设计一个流程,即使在不断得到异常时也能正常工作,而且不应该进入不受控制的状态

2.4.3. 设计软件实现容异性从幂等性(idempotency)下手

2.4.3.1. 如果一个函数或一个URL无论被调用多少次都返回相同的结果,那么它就是具有幂等性的

2.4.3.2. 对于网络请求来说,幂等性通常被认为是一种简化的方式

2.4.4. 当我们设计函数时,无论它被调用多少次,返回结果都一致,当它被意外中断时,一致性让我们因此获得好处

2.4.5. 数据库的事务机制可以帮你避免程序进入异常状态

2.4.5.1. 事务可以在程序因某个异常而中断端的时候进行回滚操作

2.4.5.2. 在很多场景当中这个操作并不是必需的

2.5. 没有事务的容异性

2.5.1. 具有幂等性可能并不足以保证其具有容异性,但至少为其打下了一个很好的基础

2.5.2. 事务可以防止这种情况,因为它们会回滚所有的变化,而不会留下任何脏数据

2.5.3. 并不是每个存储都支持事务机制

2.5.3.1. 比如说文件系统

2.6. 异常与错误

2.6.1. 异常标志着错误(勉强可以这么说),但不是所有的错误都有资格成为异常

2.6.2. 常规的错误值在大多数情况下都能很好地返回响应

2.6.2.1. 没有返回值也没任何问题,只要你觉得返回值没任何用处

2.6.3. 你可以根据你预想的调用者对信息的需求程度来设置不同类型的错误结果

2.6.4. 使用enum的好处是,当你使用swtich语句的时候,编译器会告诉你有未处理的情况

2.6.5. 对于未处理的情况,你会得到一个编译器警告,因为它们没写完

3. 不要调试

3.1. 调试(debugging)是一个古老的术语,它的出现甚至要早于编程

3.1.1. 在20世纪40年代,格雷丝·霍珀(Grace Hopper)在一台出问题的Mark Ⅱ计算机的继电器中发现了一只真正的虫子(bug)

3.1.2. 最初在航空业中用于描述识别飞机故障的过程

3.1.3. 现在它被硅谷更先进的做法所取代

3.2. 用调试器运行程序,设置断点,一步一步地追踪代码,并检查程序的状态

3.3. 连续检查应用程序的状态可能是最古老的调试程序的方法

3.4. 不是所有的程序都有可见的控制台输出、网络应用程序或服务

3.5. printf()调试法

3.5.1. 在程序中插入控制台输出行来查找问题是一种古老的做法

3.5.2. printf()调试的名字来自C语言中的printf()函数

3.5.3. printf()调试是可以显示运行中的程序的状态,所以程序员可以了解问题发生的地方

3.5.4. printf()或Console.WriteLine()会将状态信息输出到控制台,你可以通过历史记录来查看

3.6. .NET为其提供了替代方案

3.6.1. Debug.WriteLine()方法

3.6.1.1. 用于将输出写入调试器输出控制台,在Visual Studio的调试器输出窗口中显示,而不是在应用程序自己的控制台输出

3.6.1.2. 最大的好处是对它的调用会从成品(release版本)的二进制文件中完全剥离,所以它不会影响发布代码的性能

3.6.2. Trace.WriteLine()方法

3.6.2.1. Trace.WriteLine()在跟踪意义上是一个更好的工具,因为.NET跟踪的除了通常的输出外,还有运行时配置的监听器

3.6.2.2. 可以把跟踪的输出写进文本文件、事件日志、XML文件,以及任何你能想到的安装了正确组件的东

3.7. 初识转储

3.7.1. 逐步调试的一个替代方案是检查崩溃转储

3.7.2. 崩溃转储文件不一定是在崩溃后创建的

3.7.3. 崩溃转储文件是包含程序的内存空间快照内容的文件

3.7.4. 微软在MS-DOS的2.0版本而不是1.0版本中加入了目录这个文件系统

3.7.5. UNIX系统中也被称为核心转储(core dump)

3.7.5.1. 一个非侵入性(non-invasive)的操作,只会暂停进程直到操作完成,但之后会保持进程运行

3.7.6. 从.NET上扩展名为.pdb的调试文件中获取这些信息,并将内存地址与符号和行号相匹配

3.8. 小黄鸭调试法是一种通过把问题告诉坐在你桌子上的小黄鸭来解决问题的方法

3.8.1. 可以帮助你意识到你还没有尝试过所有可能的解决方案

4. 总结

4.1. 对bug进行优先级排序,以避免将资源浪费在修复不重要的bug上

4.2. 只有当你的行动是有意识、有计划的时候,才能捕捉到异常情况

4.2.1. 不然,就不要去进行bug捕捉

4.3. 写有容异性的代码,它首先得有抵御崩溃的能力,而不是作为“马后炮”

4.4. 对于那些常见错误或者因为对代码期待过高而导致的错误,使用结果代码(result code)或者enum,而不是使用异常

4.5. 使用框架提供的跟踪功能,比笨重的逐步调试能更快发现问题

4.6. 如果其他方法不可用,则使用崩溃转储分析来识别生产环境中运行代码的问题

posted @ 2023-11-14 07:10  躺柒  阅读(42)  评论(0编辑  收藏  举报