当前位置:Java -> 诊断患者的最佳方法是剖开他

诊断患者的最佳方法是剖开他

"最有效的调试工具仍然是谨慎的思考,加上明智地放置打印语句。" — Brian Kernighan。

切开患者并使用打印进行调试曾经是诊断问题的最佳方式。如果你仍然主张其中之一作为优越的故障排除方法,那么你要么面对的是非常狭隘的问题,要么需要更新你的知识。这是一个经常发生的情况,例如最近的一条推特:

Francois Chollet tweet

这条推特被推到了 HN 的首页,并且人们纷纷加入了那些经常重复的胡说八道。不,这不是绝大多数开发者的最佳方式。它应该像可能避免手术一样被反对。

过分依赖打印调试是一种心理障碍;调试不仅仅是在代码上一步步执行。这需要一种完全新的解决问题的思维方式。一种远比仅仅打印几行代码要优越的方式。Debugging 101 graphic

在我继续之前,我的偏见是显而易见的。我写了一本关于调试的书,我也经常在博客中谈论它。这是我的一个爱好。

但是我想从规则的例外开始:何时我们需要打印一些东西...

日志记录并非打印调试!

我们的工具库中最重要的调试工具之一是日志记录器,但它绝不同于打印调试:


日志记录器 打印

输出的持久性

永久的

短暂的

代码中的持久性

永久的

应该移除

全局可切换性

意图

作为设计的一部分

临时添加

日志是我们经过深思熟虑地添加的内容;我们希望保留日志以便将来调查 bug,甚至可能希望将其公开给用户。我们通常可以在模块级别控制其冗长性,并且通常可以完全禁用它。它在代码中是永久的,通常写入一个可以随时查看的永久文件。

打印调试是我们为了定位临时问题而添加的代码。如果这样的问题有可能再次出现,那么日志通常才更有意义。这对几乎每种类型的系统都是适用的。我们看到开发者不断添加并移除打印语句,而不是创建一个简单的日志来追踪频繁出现的问题。

有一些特殊情况下,打印调试是有一定意义的:在关键任务的嵌入式系统中,由于设备的限制,可能不太现实使用日志。调试器在这些环境中非常糟糕,打印调试是一个简单的 hack。使用调试系统级工具,比如内核、编译器、调试器或 JIT,可能会很难用调试器来调试。在所有这些情况下,日志记录可能没有意义,例如,我不希望我的 JIT 打印出它正在处理的每个字节码和相关的元数据。

这些都是例外,而不是规则。我们很少有人编写这样的工具。我是个例外,即便如此,它也只占我的工作的一小部分。例如,在 Lightrun 工作时,我曾经参与制作生产调试器。调试连接到可执行文件的代理代码是最难的事情之一。由 C++ 和 JVM 代码组成,连接到一个完全不同的二进制文件...对这一部分进行打印调试更简单一些,即使是这样,我们也尽量朝着日志记录的方向努力。然而,在服务器后端和 IDE 中的调试器的可视化方面是调试器的完美应用对象。

为什么要调试?

使用调试器而不是打印输出或日志有三个原因:

  • 功能:现代调试器可以提供许多开发人员不熟悉的出色功能。不幸的是,由于调试是一个难以测试的主题,学术界几乎没有调试课程。
  • 低开销:过去,使用调试器意味着执行缓慢且开销巨大。但情况已经不同了。我们中的许多人在启动应用程序时使用调试操作,而不是运行操作,对于大多数应用程序,几乎看不到开销。如果确实有开销,一些调试器提供了通过禁用某些功能来改善性能的方法。
  • 库代码:调试器可以进入库或框架并在那里跟踪错误。用打印调试来做这件事将需要编译代码,这可能并不是您想要处理的。

我在我的书籍和关于调试的系列中深入探讨了我提到的这些功能,但让我们选择其中一些我之前写过的调试器的一些出色功能。

为了进行积极对话,这里是我认为现代调试器的一些最佳功能。

跟踪点

每当有人提出打印调试讨论时,我听到的只是,“我不了解跟踪点。” 虽然调试器中的跟踪点并不是什么新功能,但很少有人意识到它们。跟踪点是一个不会停止的断点;它只会继续执行。与停止执行不同,您可以在那一点执行其他操作,比如打印到控制台。这类似于打印调试;只是它没有许多缺点:没有运行时开销,没有意外提交到代码库,更改时无需重新启动应用程序等。

分组和命名

前一个视频/帖子包含了关于分组和命名的讨论。这使我们可以将跟踪点分组在一起,作为一个组来禁用它们等。这可能看起来像一个次要功能,直到您开始考虑打印调试的过程。我们慢慢地浏览代码,添加一个打印并重新启动。然后突然之间,我们需要回去,或者如果接到电话并需要调试其他问题...

当我们将跟踪点和断点打包成一个组时,我们可以像版本控制中的一个分支那样设置一个调试会话。这样可以更容易地保留我们的思路,随时返回到适用的代码行。

对象标记

当被问到我的最喜爱的调试功能时,我总是犹豫不决,对象标记是我的前两个最喜爱的功能之一... 它看起来像一件简单的事情;我们可以标记一个对象,并将其保存为一个特定的名称。

然而,这是一个强大且重要的功能。在调试过程中,我曾经记下对象或内存区域的指针。有时,内存区域看起来可能是相同的,但地址可能不同,或者跟踪对象可能很困难。对象标记允许我们保存一个对象的全局引用,并在条件断点或用于视觉比较时使用它。

渲染器

我最喜欢的另一个功能是渲染器。它允许我们定义调试器监视区域中元素的外观方式。想象一下,您有一个复杂的对象层次结构,但很少需要那些信息... 渲染器允许您自定义IntelliJ/IDEA向您呈现对象的方式。

跟踪新实例

调试器常常被忽视的能力之一是内存跟踪。Java调试器可以显示堆中所有对象实例的可搜索集合,这是一个出色的功能,可以揭示出人们意想不到的行为。但它还可以更进一步,它可以跟踪对象的新分配,并为您提供到适用对象分配的堆栈。

冰山一角

我写了很多关于调试的内容,所以在这篇文章中重复所有这些内容没有意义。如果您是一个更喜欢使用打印调试的人,那么请问自己:为什么?

不要隐藏在过时的Brian Kernighan的引用背后。事情在变化。您是否在打印调试是唯一选择的边缘案例中工作?

您是否将日志记录视为打印调试或反之亦然?

还是只是因为打印调试是您的团队始终工作的方式,并且已经固守不变?如果是这其中的一种情况,那么也许是时候重新评估调试器的当前状态了。

推荐阅读: 国企、研究所提前批/秋招信息汇总(更新)

本文链接: 诊断患者的最佳方法是剖开他