调试的秘密艺术
训练你的眼睛👀
认真地:训练你的眼睛👁👁
但如何呢? ˙\ (ツ) /˙
回归基础🔎
不要相信文档🔐
熟能生巧🏌️♂️🏌️♀️
最好的调试工具就是你的思想🧠
这是一门你可以学习的艺术
精通调试技巧的人实属罕见。我从多年从事企业系统工作的经验中深知这一点。如果调试简单,就会有更多人参与其中,每个人都能追踪到 bug。但现实情况是,大多数公司都会安排一位“总能找到的人”,也就是所谓的“除虫员”,专门负责清除那些其他人无法追踪到的 bug。我曾在产品公司和咨询公司工作过,在这两种岗位上,我们业务的很大一部分都围绕着发现棘手的 bug 🐜 并修复它们。
我相信故障排除是一项可以教授、学习和掌握的技能。不幸的是,太多人专注于工具和语言特性,而不是建立一个无论使用哪种语言、平台或工具都能正常工作的思维框架。有兴趣学习即时解决代码问题的秘诀吗?继续阅读!
我从事错误修复工作已有数十年。我最早发表的专题文章之一是一篇名为《除虫者密码》的故障排除文章,发表于1999年一本名为《News/400》的杂志上。我发现,有效的错误查找需要多种技能的结合。仅仅了解技术是不够的。疯狂的搜索是有方法的。有些步骤是可以学习的,而且随着你在职业生涯中接触到越来越多的系统,经验只会锦上添花。
一直让我感到惊讶的是,善于发现缺陷的人和不善于发现缺陷的人之间存在着巨大的差距。你可能会认为,这应该是一个连续的技能谱系,但我发现,人们要么掌握了,要么就不掌握——那些掌握了的人,总是能快速且持续地做到这一点。那么,秘诀究竟是什么呢?
训练你的眼睛👀
帮我个忙,做个快速测验。阅读下面的引文,快速数一下文中有多少个“F”。
完成的文件是多年科学研究和多年
经验的结合的结果。
我马上回来回答这个问题。如果我把它放在那里,那就太简单了。把你的想法记下来,然后我们再来点更复杂的东西。这是另一套说明,相信我,这一切都会引出一些结果。你准备好参加另一场比赛了吗?
我想让你看一段很短的视频。视频里有一些操作说明。只需播放一遍即可。不要尝试暂停或快进。这是链接,或者你可以点击下方视频的播放按钮。继续观看,然后写下你的评分(你会明白的)。
现在我希望你开始明白我的意思,以及掌握调试技巧的第一步。根据我的经验,大多数开发人员都没有用正确的方法调试代码。当他们按下 F5 键并开始单步执行程序时,他们并没有观察正在发生的事情。
什么?我在开玩笑吧?他们设置了断点,还有监视窗口。他们正勤快地敲着 F10 和 F11 键来进入和退出子程序。我什么意思?问题如下:
他们正在等待程序按照预期执行。这很难不去等待,尤其是当你是程序的编写者时!所以,当你单步执行那段代码,然后说:“是啊,是啊,我只是在这里初始化一些变量”,并快速按下 F10 键时,你就错过了它,因为一个字符串文字拼写错误,或者你引用了错误的常量。
“F”测验的答案是 6。大多数人数到 3 是因为他们会在脑中默念单词,并听“f”的发音,而不是直接看字母。人们在调试时就是这样做的——他们只是在摸索程序,而不是观察它实际在做什么。
认真地:训练你的眼睛👁👁
你看到大猩猩了吗?大多数人第一次都没看到。那是因为他们在按照指示做。他们在数传球次数,这正是练习的目的。但是,当你看到它并知道自己在寻找什么时,你能相信它有多么明显吗?你怎么会错过这样的机会呢?
希望到现在为止,我们已经确定你的大脑已经具备了相当好的过滤能力,并且会尽力满足你的期望。所以,当你带着预期单步执行代码时,你猜怎么着?你会看到调试器按照你的预期执行,而错过了可能导致 bug 的真正原因。
但如何呢? ˙\ (ツ) /˙
您可以做几件事来帮助磨练您的调试技能,我鼓励您尝试所有这些事情。
让别人调试你的代码,并主动提出调试他们的代码。理解如何查看代码并了解其功能的最佳方法是单步调试你不熟悉的代码。这乍一看可能很乏味,但它是一门学科和技能,可以帮助你学习如何以正确的方式遍历代码,而不是做任何假设。
尽量不要将代码视为块。换句话说,当你有一个初始化变量的例程时,不要将其视为“初始化块”而跳过它。单步执行并仔细考虑每条语句。不要将语句视为句子,而是回归编程的根源,观察等号左侧和右侧的一组符号。你会惊讶地发现,这能帮助你快速找到错误或重复的赋值。例如,在 MVVM 中,开发人员经常会进行剪切粘贴,最终得到如下代码:
private string _lastName;
private string _firstName;
public string FirstName
{
get { return _firstName; }
set { _firstName = value; RaisePropertyChanged(()=>FirstName); }
}
public string LastName
{
get { return _firstName; }
set { _lastName = value; RaisePropertyChanged(()=>LastName); }
}
你发现bug了吗?如果没有,花点时间你就会发现的。如果代码是你写的,那就难多了,因为你期望它“能正常工作”。
回归基础🔎
市面上充斥着各种花哨的工具,它们告诉我们如何重构代码、扫描类,有时却让我们忘记了那些用来排查问题的基本工具。
我曾经和一位客户一起排查内存泄漏问题,结果发现自己面对的是一张张包含依赖项、句柄和实例的庞大图表。我发现某些对象创建次数过多,但查看代码后发现一切正常。其他的问题又从何而来?
于是,我回到了最基础的部分。我在构造函数中加入了一条调试语句,然后再次运行。突然,我意识到有些实例能够如实地报告自身,而有些则不然。这到底是怎么回事?啊……这个类是从基类派生出来的。于是我又在基类中加入了一条调试语句。果然,它也被实例化了。快速转储一下调用堆栈,问题就解决了……不是靠图表和重构工具,而是靠老一套的侦探方法。
不要相信文档🔐
我早期学到的一个教训是不要相信文档。当时我正写一本关于一个正在开发中的新框架的书。文档内容稀少,内容不固定,而且经常出错。在某个章节里,我写了某些应用程序应该如何运行。文档非常具体地说明了如何将工作划分到不同的线程中。我的导师说:“不要相信它”,并鼓励我进行调试。在单步执行代码并观察实际发生的情况(而不是我预期发生的情况)时,我发现架构与实际情况大相径庭。我能够帮助修复文档,并帮助开发人员避免代码中不必要的开销。
我经常构建一些小项目来学习编程语言和平台。例如,考虑以下 JavaScript 代码:
const doSomething = (payload, fn) => { fn(payload); };
doSomething('This should echo', console.log);
let text = 'Some text';
doSomething(text, text => text += ' appended to.');
console.log(`The text after the call: ${text}`);
let textPayload = { text };
doSomething(textPayload, payload => payload.text += ' appended to.');
console.log(`The text after the payload call: ${textPayload.text}`);
你能预测控制台会输出什么吗?你可以运行这个 jsFiddle来验证你的答案。
这是一段简单的代码,但它帮助我超越了概念化 JavaScript 中原语和对象的区别,亲眼目睹了它们的实际作用。
熟能生巧🏌️♂️🏌️♀️
我经常调试运行良好的代码。我经常发现一些潜在的问题,这些问题并没有立即显现出来。我可能会注意到初始化代码被调用了不止一次(这可能会导致大规模性能问题),或者某个事件被一个实例注册,但该实例超出了范围而没有注销。这可能会导致内存泄漏!
有时,即使是“简单的代码”,当你打破固有的假设时,也能揭示秘密。我曾参与过一些用 Angular.js 编写的大型项目(没错,就是那个需要或 的旧版本)。一些使用数据绑定的非常简单的页面在“幕后”却发生了很多事情。通过调试这些页面,我发现了一些效率低下的问题,例如运行多个“摘要循环”,这可能会随着时间的推移影响性能。$scope
controller as
一个很好的练习是下载一个开源项目,最好是一个实用程序或工具。花些时间检查源代码,确定你认为代码会如何运行。然后,启动调试器,单步执行每个语句。不遗余力!你可能会对自己的发现和学习感到惊讶。你这样做得越多,准备就越充分。你将:
- 通过预览源代码来学习如何分析代码
- 发现开发人员用来解决各种问题的模式
- 可能发现作者没有意识到的问题
这项练习可能只是揭示了您可以做出的改进,并将其作为开源贡献提交回项目。
最好的调试工具就是你的思想🧠
最后,我要给你的建议,就像多年前我开始排查我的第一个企业问题时,我的导师给我的建议一样。他告诉我,目标应该是永远不要启动调试器。每个调试会话都应该从逻辑地遍历代码开始。你应该分析你期望的结果,然后虚拟地遍历它……如果我在这里传递这个,我会得到那个,然后那个会传递到那里,然后就这样循环……这个练习的作用不仅仅是帮助你理解代码。十有八九,我通过遍历源代码来修复错误,而从不需要按F5键。
当我按下 F5 键时,我对代码应该做什么已经有了预期。当代码执行到不同寻常的程度时,通常更容易找出计划出错的地方以及执行代码是如何偏离脚本的。这项技能在许多完全不允许运行调试器的生产环境中尤为重要。我被教导并一直遵循着这样的理念:即使是最棘手的 bug,只需结合源代码、恰当的跟踪语句和深入的思考,就能修复。
启动调试器并在您认为可能存在缺陷的地方设置断点很容易。在关键时刻,这可能是最好的方法,如果您成功了,那就给您一个🎩小提示!但是,如果没有成功,请不要陷入轻信假设的陷阱。相反,退一步,开始分析实际发生的事情,而不是您认为应该发生的事情。
我的流程通常如下所示:
- 尝试修复我认为存在的问题(如果我能编写一个由于缺陷而失败的单元测试,并在修复它后通过,那就更好了)
- 如果没有,请退一步分析代码以确定它到底在做什么
- 打开调试器,但不是在我认为可能存在问题的地方设置断点,而是从头一步一步地进行,并注意正在发生的事情,而不是我认为应该发生的事情
- 添加调试语句以帮助留下面包屑踪迹
- 单独提取代码,看看是否可以在不增加完整解决方案的复杂性和开销的情况下找到缺陷
我这样做的时间已经足够长了,80% 的时间我都能在第一步就找到解决方案,而 20% 的时间则花费在最后几个步骤上。
这是一门你可以学习的艺术
调试是一门艺术,需要耐心、专注和经验的积累。希望之前的练习能帮助你理解那些有时会阻碍你修复代码的“过滤器”,也希望这些技巧能帮助你在下次遇到问题时换个思路。记住,没有无法修复的缺陷……也没有比你耳朵之间的那个更强大的调试器。
你最喜欢的调试技巧是什么?请在下方评论区分享。
文章来源:https://dev.to/dotnet/the-secret-art-of-debugging-1lfi