调试策略
本节阅读量:大多数情况下,调试程序时花费最多时间的环节是查找错误的实际位置。一旦找到了问题所在,与之相比,其余的步骤(修复问题并验证问题是否已修复)就微不足道了。
本课将探索如何查找错误。
通过阅读代码发现问题
假设你发现了一个问题,并且需要定位该问题的原因。多数情况下(如果程序规模较小),通过阅读代码就可以快速定位问题的位置。
例如以下程序片段:
|
|
如果期望此程序按字母顺序打印名称,但它却按相反的顺序打印,那么问题可能出在sortNames函数中。将问题范围缩小到该函数后,通过查看函数代码就可以发现问题。
然而,随着程序变得复杂,通过代码检查来发现问题也变得越来越困难。
首先,需要查看的代码更多了。逐行查看一个数千行的程序需要很长时间(更不用说这非常无聊)。其次,代码本身往往更复杂,有更多可能出错的地方。第三,代码的行为可能不会为你提供关于哪里出错的太多线索。如果你编写了一个输出股票推荐的程序,而它没有任何输出,那么你可能对从哪里开始查找问题毫无头绪。
最后,错误可能是由错误的假设引起的。几乎不可能通过目测来发现由错误假设导致的错误,因为在检查代码时,你可能会做出相同的错误假设,而不会注意到问题所在。
如果我们通过代码检查无法找到问题,该怎么办呢?
通过运行程序查找问题
如果我们不能通过代码检查找到问题,可以采取另一种方法:观察程序运行时的行为,并尝试从中诊断问题。这种方法可以概括为:
- 尝试研究如何复现问题
- 运行程序,收集信息,缩小查找范围
- 重复前面的步骤1和2,直到解决问题
在本章的其余部分,我们将讨论这些方法和技术。
复现问题
发现问题的第一步也是最重要的一步,是能够重现问题。重现问题意味着让问题以一致的方式出现。原因很简单:除非你能观察到问题的发生,否则很难找到它。
如果软件问题很明显(例如,每次运行程序时,程序都会在同一位置崩溃),那么重现问题可能很容易。然而,有时重现问题可能会困难得多。问题可能仅发生在某些计算机上,或在特定情况下(例如,当用户输入某些特定内容时)。在这种情况下,整理一组重现步骤是很有帮助的。重现步骤是一个明确的步骤列表,按照这些步骤操作就可以让问题再次出现,并且具有高度的可预测性。我们的目标是能够让问题尽可能多地再次发生,这样就可以反复运行程序,并寻找线索来确定是什么导致了问题。如果问题可以100%地重现,这是最理想的,但即使重现率低于100%也可以接受。仅在50%的时间内出现的问题,可能意味着诊断所需的时间是100%可重现问题的两倍,因为有一半的时间程序不会表现出问题,无法提供任何有用的诊断信息。
专注于问题
一旦我们能够合理地重现问题,下一步就是找出问题在代码中的位置。根据问题的性质,这可能容易也可能困难。举个例子,假设我们不太清楚错误到底发生在哪里,该如何找到它?
在这里,让我们玩一个猜数字游戏。请你猜一个1到10之间的数字。对于你的每个猜测,我会告诉你猜的结果是太高、太低还是正确。此游戏的一个实例可能如下所示:
|
|
在上面的游戏中,你不必猜所有的数字就能找到正确的数字。通过不断猜测,并综合考虑从每次猜测中获得的信息,你仅通过几次猜测就可以”找到”正确的数字(如果使用最优策略,你总是可以在4次或更少的猜测中找到正确的数字)。
我们可以使用类似的过程来调试程序。在最坏的情况下,我们可能不知道错误在哪里。然而,我们确实知道问题一定出在从程序开始到结束之间的某个地方,而我们观察到的错误症状就发生在所执行的代码中。这至少排除了之后执行的程序部分。但这仍然可能留下许多代码需要检查。为了诊断问题,我们将对问题的位置进行一些有根据的猜测,目标是快速地找到问题所在。
通常,无论是什么原因导致我们注意到的问题,都会给我们一个接近实际问题所在位置的初步猜测。例如,如果程序没有在应该向文件写入数据的时候将数据写入文件,那么问题可能在处理向文件写入的代码中的某个地方。然后,我们可以使用类似二分查找的策略来尝试并隔离问题的实际位置。
例如:
- 如果在程序中的某个点上,我们可以证明问题尚未发生,这就类似于收到”太低”的猜测结果——我们知道问题一定出在程序的后面。例如,如果我们的程序每次都在相同的位置崩溃,并且我们可以证明在某个特定点上程序还没有崩溃,那么崩溃必然发生在代码的后面。
- 如果在程序中的某个点上,我们可以观察到与问题相关的不正确行为,那么这就类似于收到”太高”的猜测结果,我们知道问题一定出在程序中更早的位置。例如,假设一个程序打印某个变量x的值。你希望它打印值2,但它打印的是8。变量x肯定具有错误的值。如果在程序执行过程中的某个时刻,我们发现变量x已经变成了8,那么我们就知道问题一定发生在该时刻之前。
猜数字类比并不完美——有时我们将代码的某个部分排除在考虑范围之外后,仍然无法判断实际问题是在该点之前还是之后。
在下一课中,我们将展示所有这三种情况的示例。
最终,通过足够的猜测和一些好的技术,我们可以找到导致问题的确切代码路径!当你排除了所有其他可能性,剩下的那个一定就是问题的根因。
你想使用什么样的猜测策略取决于你自己——最好的策略取决于错误类型,因此你可能需要尝试许多不同的方法来缩小排查问题的范围。随着你积累调试经验,你的直觉会帮助并指导你。
那么我们如何”猜测”呢?有许多方法可以做到这一点。我们将在下一节中从一些简单的方法开始,然后在这些方法的基础上进行扩展,并在后续小节中探索其他方法。