文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

作为一个软件开发者,我可以确定一件事: 你将花费大量的时间调试代码。

生活中总有一些不可避免的常事:死亡, 税收以及程序员制造错误。

既然你花这么多时间调试代码, 那么你最好能够擅长调试, 你说呢?

不幸的是许多开发者,甚至是经验丰富的,往往在这方面,呃,怎么说呢....很差劲。

有很多开发人员随心所欲的快速实现新功能和飞快地编写代码, 但是谁来清理他们留下的垃圾代码呢?

知道如何写出好代码是一回事,而知道如何去调试在你生命中见过的最丑陋的代码就是另外一回事了,而这些代码的第一个版本可能是由所谓的传奇人物鲍勃在他的地下室里花了48个小时所完成的,他是一个“奇怪的”家伙。

幸运的是,调试像其他任何技能一样都是可以学会的。

如果你应用了正确的技术并加以联系,你就可以成为这一方面的高手。

谁知道呢?你甚至可以享受它。

调试的关键是要意识到它的一切都是关于心态 

要采取一种系统化的方法直击问题本身—不要着急,也不要期待一下就能找到问题根源,能进得去也能出得来。

第 2 段(可获 1.64 积分)

要保持冷静和镇静:从逻辑和分析的角度来针对问题,而不带有任何情感的。

在这一章,我将会制定一个系统性的调试方式,可以帮你避免可怕的调试者心态并且将你的调试技巧提升到一个新的高度。

什么是调试?

在我们深入之前,先小试一下。

到底什么是调试呢?

看起来相当显而易见,对吗?

你打开调试器,然后用代码来“调试” 问题。

但是,这正是错误的地方。

调试跟调试器一点关系都没有。

第 3 段(可获 1.28 积分)

调试是试图从源代码基础上发现问题的根源, 识别可能的原因,从而推出各种假设,直到找到问题的源头,然后最终消除源头并且确保再也不会发生这种情况。

好吧,我像我们可以姑且称它为修复bug,语义上的。

关键是,调试并不仅仅是在调试器里来回切换并修改代码,直到能够正常工作。

调试的第一法则:不要使用调试器

啊,你在说啥?

有一个新的bug要让我修复?

哦,这个有点棘手?

请不要害怕,先生。我将会释放我精神武器的全部力量来对付这个邪恶恐怖的家伙。

第 4 段(可获 1.49 积分)

带着这种心态,作为程序员的你坐在了你的书桌面前。

你打开了调试器。

你小心翼翼的浏览着代码。

时间看起来有点模糊了,几分钟变成了几个小时,几个小时变成几个星期。

你已经是一个坐在键盘前的老人了,仍然在调试同一个东西,但是在某程度上你“更近了”。

你的孩子都长大成人。你的妻子也离你远去。

唯一还留下的只有。。。bug

大多数程序员在调试代码解决一个问题的时候第一件事就是打开好用且古老的调试器,并开始四处查看。

错了。

不要这样做。

调试器应该是你最后的选择。

第 5 段(可获 1.39 积分)

当你刚打开调试器时,你实际上等于在说,“我并不知道什么导致了问题的发生,但是我会先到处看看。”

就像当你的车子出故障了之后,你一点都不懂车子,因此你就打开了前引擎罩来试图看看哪里出问题了。

你在寻找什么?

你甚至都不知道。

别误会我的意思。

调试器确实是很不错而且强大的工具。

使用恰当的话,调试器能够帮助你解决各种各样的问题,而且能够帮助你了解当你的代码运行的时候发生了什么。

然而,这并不是一个正确开始的地方,而且很多bug都可以在没有调试期的情况下可以轻而易举被解决。

第 6 段(可获 1.58 积分)

你也知道,调试器就像Facebook或者YouTube上猫的搞笑视频一样,有一种魔力将你吸引过去。 

重现错误

那么,如果你不是通过调试器来调试一个问题,那你会怎么做呢?

好吧,我很高兴你问这个问题。

任何理智的人应该做的第一件事是 重现bug来确保那确实是一个bug 并且确保你能够调试它。

不能被重现的bug百分之百都无法调试。

因此,如果你无法重现一个问题,调试它就没有任何意义了。你明白我的意思了吧?

不仅你无法调试一个不能重现的问题,而且即使你确实修复了它,你也无法验证你确实已经修复了它。

第 7 段(可获 1.7 积分)

因此,当你想要试图调试一个bug的时候,第一件要做的事情就是要确保你可以亲自重现该bug。

如果你不能,去寻求一些帮助。

如果是一个测试者提出的这个bug,让他为你重现一下。

如果这个bug时有时无的发生,而且不能总是重现,那就意味着你不清楚重现问题所需要的场景是什么。

时有时无的问题并不存在。

如果确实是一个问题,它就可以被复现;只是需要你知道如何让它出现。

关于时有时无问题的一些题外话:

好吧,你的老板要求你修复该问题。

第 8 段(可获 1.4 积分)

他们在生产环境上看到过了。客户也看到了。所以它肯定是个问题。

关于“我无法重现它”的说词一点不起作用—他们并不买账。

那你会怎么做?

你还是无法调试一个你不能重现的问题。

但是你可以做的是收集更多的证据。

比如,在你的代码里插入日志语句。

收集尽可能多的关于问题发生时的信息以及在什么情况下它会发生。

然后手动重现相关环境和场景,如果可以的话。

不要轻易给你无法重现的问题抛出所谓的“解决办法”。

第 9 段(可获 1.29 积分)

如果你没能充分理解问题而无法重现它,那么你也只能通过猜测以极低可能性修复掉它,但是你却很难判断你的修复是否真的起作用。

找到一个能够重现问题的方法,即使只是在生产环境上可以重现的问题。

坐下来思考思考

在你能够重现问题之后,下一步是大部分软件开发人员都会跳过的,因为他们太急于解决问题了,但是这一步却是至关重要的。

这真的是一个极其简单的步骤。

仅仅是坐下来思考一下。

没错,就是这样。

考虑下该问题以及导致它的可能原因是什么。

第 10 段(可获 1.4 积分)

仔细考虑下系统是如何工作的,而什么可能会导致你看到的奇怪现象的发生。

你也许会急于进入代码和调试器去“查看一些东西”,但是在你查看什么东西之前,重要的是你要知道你想要找什么东西和要看到什么东西。

你也许会想到一些关于导致问题发生的东西的办法或假设。

如果你没有,请耐心点。保持安静并思考。

如果能帮助的话站起来四处走走,但是在你继续之前,你至少应该有一些想测试的想法。

第 11 段(可获 1.44 积分)

如果你确实想不出来什么东西的话,继续不要使用调试器,相反的应该浏览下源代码,看看能否得到更多关于系统如何运作的蛛丝马迹。

在你继续往前走之前,你应该至少有两到三个可以测试的假设了。

测试你的假设

好吧,现在你已经有了些不错的假设,对吗?

通量电容器连上了某东西,所以如果从某个地方测得的电压不合格,… 那么该东西一定是没有配置正确!

呃… 大概就像这样的一些假设。

第 12 段(可获 1.29 积分)

好了,现在让我们打开调试器来测试下我们的假设吧!耶,开干吧!

不!错误。

不要动,年轻的小伙子。

我们还用不上调试器。

等等,什么?如果不用调试那我怎么来测试我的假设呢,你说呢?

单元测试。

没错,正确:就是单元测试。

尝试写一个单元测试来测试你的假设。

如果你认为系统的某个部分没有正常工作的话,写个你认为可以用来探索该问题的单元测试。

如果你是对的,并发现了问题,你就可以正好就地修复它,而且现在你还有了一个能够验证你的修复方案的单元测试,并能确保问题不会再发生。

第 13 段(可获 1.63 积分)

(还是要确保去尝试并重现实际的bug,直到你认为它已经被修复掉了。)

如果你错了并且你写的单元测试如愿通过了的话,你只不过是给项目添加了另外一个单元测试,从而使得系统稍微健壮了一些,而且你也证实了你的一个假设是错误的。

你可以将它想成是对问题空间的提升。

每当你完成一个单元测试并且它通过测试,你就是在消除各种可能性。你正在穿越你的调试旅程,一旦你发现你走入了一个死胡同之后,就立即关闭并锁上你身后的门。

如果你在调试的过程中迷失了数个小时或者数天之后,你就应该立刻意识到这是多么的有价值。

第 14 段(可获 1.54 积分)

说调试器不好的其中一个原因,是因为当我们检查和复查我们的假设的时候,它会导致我们一遍又一遍的重复踏入同一条错误的道路, 要么忘了我们已经走过的路,要么让我们怀疑我们是否已经足够仔细的查找问题。

单元测试就像攀登一座山峰时,你把卯放在一个能够确保你不会向后跌太远的地方。

编写单元测试来测试你的假设也同样会确保你不会随意的尝试事情和漫无目的四处查看。

当你写一个单元测试时,你必须要有一个明确的用来测试的假设,以便于你调试相应的问题。

第 15 段(可获 1.44 积分)

现在,我是一个现实主义者。

我是务实的。

我知道有的时候要写一个单元测试去测试一个假设会相当困难或者是不可能的。

在这种情况下,借助于调试器是没问题的,但前提是你遵循了这条规则: 

你做这件事时要有一个明确的目的。

当你在使用调试器的时候,要清楚你究竟在寻找些什么和你在检查什么。

不要仅仅是进去之后漫无目的四处查看。

我知道这样看起来,我对整件事有点神经质和迂腐,但是请相信我,这样做是有理由的。

我想你让成为一个熟练的调试者,而且你只能在对你调试的方式深思熟虑了之后才能到达这种程度。

第 16 段(可获 1.64 积分)

检查你的假设

大多数情况下,你的假设都不会成立。

这就是生活。

如果是这种情况的话,下一步你最好是去检查你的假设,看看事情是怎样工作的。

我们通常假设代码是以某种方式工作的或者某些输入或输出时某些值。

我们经常会想,“见鬼,这不可能发生。我正在查看这段代码呢,不可能会导致这样的输出的。”

通常,我们错了。

这种事我们大家都是难免的。

对于这些假设,你最好是去检查下他们。

第 17 段(可获 1.41 积分)

检查的最好方法是什么呢?

没错,就是进行更多的单元测试。

编写一些单元测试用例检查一些在工作流中必须进行的,而你一直在调试的明显的问题。

这些测试大多很容易通过,你会说,“嗯“。

但是,每隔一段时间,你会写一个单元测试来测试一些明显的假设,测试结果会使你感到震惊。

请记住,如果你的问题的答案是显而易见的,那么它就不会仅仅只是一个问题。

再次,让我告诉你实用主义的一面,是的,你也可以打开调试器来检查你的假设。

第 18 段(可获 1.48 积分)

但是只有在你尝试先使用单元测试检查过假设之后(才可以使用调试器)。

同样的,这就像你在攀爬一座山峰时沿途将锚嵌入岩石中。

如果可以请尽量避免使用调试器,只有在你必须的时候再使用,但是,再次说明,这也只是为了验证或者反证你所提出的某个具体的假设。

分而治之

我记得曾经解决一个非常棘手的bug,当时一个打印机未能正确解释一个使用PostScript打印语言编写的打印文件。

我尝试了我能想到的任何事情来调试这个问题。

也测试了各种各样的假设。

Nothing panned out.

第 19 段(可获 1.23 积分)

看起来这个bug像是由于该打印文件中多个命令的某种组合所导致的,而我并不知道是哪些。

那么,我做了些什么?

嗯,我把这个打印文件分成了两半。

Bug仍然存在。

接着,我又继续切分其中一部分。

这一次它消失了。

我对另外一半进行了尝试。反复来回继续,以此类推。

我不断地对该打印文件的各个片段进行尝试,直到我将整个文件从几千行的代码缩减到了只有五行。

也就是这五行代码导致了那个bug。

很简单的。

当你在调试时卡住的时候,你需要做的也就是找到一个方法将问题一分为二 —— 或者将尽可能比较大的东西剔除出去。

第 20 段(可获 1.71 积分)

不同的问题其解决办法可能也会不一样,但是请试图找到各种办法,在消除大量的代码或者大部分系统或变量的前提下,还能够保证仍然将问题重现。

看看你是否有可能写出可以完全消除导致错误的那些部分的单元测试来。

接着继续做下去....再继续。

如果一直继续下去,你很有可能就会找到那个导致错误发生的关键组件,那么问题就很容易解决了。

如果你修复掉了他,要理解为什么

让我来给你最后一个关于调试的建议吧—尽管关于这个主题我相信我可以写出一整本书来。

第 21 段(可获 1.55 积分)

如果你要修复一个问题,首先要弄明白为什么你做的事情可以修复它。

如果你不明白为什么你所做的事情能够修复该问题,那么你还没做完要做的事情。

你有可能无意中又引入了另外一个问题,或者(非常可能)你根本就没有修复原先存在的问题。

问题不会平白无故自行消失。

如果你没有修复该问题,那么我敢肯定它肯定没有被修复掉。它只是隐藏起来了。

但是如果你的确修复过该问题,不要就此打住。往更深一步挖掘,并且确保你理解导致问题产生的根本原因,以及你所做的事情是如何修复它的。

第 22 段(可获 1.43 积分)

有太多的软件开发人员喜欢通过玩弄字节来调试问题,当代码开始工作的时候,他们就认为问题已经修复了,而根本不知道为什么。

这是一个非常危险的习惯,原因有很多。

就像我上面提到的,当你在系统中随意的调整东西并且到处改变代码的时候,你可能会无意识的引入其他的各种各样的问题。

但是,也许比那更多,你这样是在将你自己训练成一个糟糕的调试者。

你正在发展一种乱搞东西直到它能够工作的坏习惯。 一点谈不上技术,也极其不严谨。

有的时候你也许会比较幸运,但是你无法拥有可重复性的操作流程或者可靠的调试技能。

第 23 段(可获 1.45 积分)

你不仅要理解什么地方出问题了,为什么出问题和你是怎样修复的,你还应该验证你的修复是否成功。

我知道这看起来像是常识,但是当程序员在说“修复了一个问题”时,他只是在假设修复已经成功了,然后他把代码交给QA,只是为了让QA去重现问题,接着QA反馈给程序员,他又不得不再一次进行同样的操作,这样来回多次,浪费了很多时间,都是无法估算的。

这是对时间的极大浪费,而这完全是可以避免的,只需要多花五分钟时间去验证你修复的东西到底修复了没有即可。

实际上,不要仅仅是验证修复就万事大吉了;还可以为这个问题写一个回归测试,可以确保它再也不会发生。

第 24 段(可获 1.55 积分)

如果你真正理解你所修复的问题,你就应该能够编写一个单元测试来展示这个问题,并且你的修复应该能使得该单元测试通过。

最后,寻找跟这个问题属于同一类的其他实例。

物以类聚,而各种问题也往往是类似的。

如果你发现你对系统所做的某个假设有问题,或者某个组件的代码不正确,那很有可能还有其他问题也是由这个问题导致的。

这也再次说明了为什么你要理解你的问题本质以及你的解决方案能够修复它的原因是如此的重要。

第 25 段(可获 1.31 积分)

如果你知道发生了什么以及为什么会发生,那么你就可以快速的弄清楚是否可能存在其他问题跟目前要解决的问题是由同一个潜在的问题所导致的。

艺术与科学

请记住,调试— 就像软件开发一样— 是艺术和科学的结合体。

你只能通过不断练习才能擅长调试。

但是仅仅练习是不够的。你还需要明确地,系统地进行调试,而不是在调试器里玩耍。

希望我已经给你大概解释清楚了该如何去做;现在剩下的事情就得靠你了。

第 26 段(可获 1.08 积分)

文章评论