代码自解释不是不写注释的理由(23)

发布于2019-04-20 11:27:42

有什么比花时间写注释更令人感到兴奋的事情吗?如果我没有猜错,你可能会说:“不好意思,所有事情都比写注释更令人感到兴奋”。如果有人要你给代码加上注释,对你来说就像是一种侮辱。你的代码写得如此优雅,它已经足以说明它要做的事情,注释是多余的,代码就是一切。

无论是开源项目还是专业软件开发,代码注释通常有两种形式:要么没有和要么毫无用处。任何领域或使用任何编程语言的程序员,无论他们来自世界的哪个地方,似乎都不喜欢给代码写注释。如果你想讲故事,可能会选择不同的人生道路(比如去当作家?)。

这种不情愿甚至形成了某种范式和哲学观点,认为代码注释实际上是有害的,任何试图逃避它的人现在都可以愉快地重新讨论这些主张。但是,稍微夸张一点说,我们实际上是在用这种方式破坏信息。虽然代码注释有时候确实会适得其反,但真正有害的是我们对它的看法。

说到底,代码注释就像是错误处理,我们很早就被告知它很重要,但却无法理解其中的原因。我们越来越厌烦为同样的老师、主管或烦人的队友做这件事。但就像错误处理一样,如果做得好,我们就是从中获益最多的人。但要做到这一点,我们需要面对一些严酷的事实,并承认根本不存在所谓的自解释代码。

所以,让我们来戳破其中的一些泡沫吧!

自解释的代码是不存在的

反对给代码写注释的人认为,“代码应该好到不需要任何多余的解释”。好的代码确实不需要注释来描述变量或函数是干什么用的。

// bad start:
int a = 4 * OFFSET;
// but don't use a comment to tell what it does:
int a = 4 * OFFSET; // initial foo value
// instead choose a name telling it itself:
int initial_foo = 4 * OFFSET;

确实,有意义的变量名根本不需要注释,但这实际上更像是一种体面的编码风格,而不是文档。当这种片面的观点变成反对使用代码注释的普遍理由时,问题就出现了。

问题是,即使变量、方法、类、函数、模块的名称是自解释的,但这些并不能描述出代码的全局面貌,也不一定能说明各部分代码为什么要那么写。当然,清晰的实现往往会让我们产生一种错觉,认为不需要再写注释了。当你花了几个小时甚至几天时间解决了手头的问题,那些代码在当下可能是完美的,然后你把它们打包、提交。

但是一个月后会怎样?你能记住多少细节?它们还是那么有意义吗?

软件开发困难重重

当然,有人可能会争辩说:“代码就在那里,你看一下就明白了”。如果我们说的是某块代码是干什么用的,那么或许这么说是有道理的。但对于任何超出这个范围的东西,深挖代码可能是在浪费时间,就像在阅读一本没有索引的书,你要从头读起,才可能找到你需要的东西。

而且,这不仅仅是为了了解别人的代码,或者向别人解释你的想法。当你重新查看旧代码或者修复错误时,你的脑子里是不是经常犯嘀咕,或者因为 git blame 显示了你的名字而感到惊讶?然而,再往后,它们可能被忘得一干二净,然后你会再次相信一切都应该是自解释的,所有的细节都应该是明确无误的。

无论你怎么努力,软件本身并不会完全自解释。这既不是你的错,我也不是想要质疑你的能力,这与人类本身有关,我们低估了软件的复杂性,而且人类的思维具有波动性。注释的目的不是为了指出代码中存在的缺陷,而是为了抵制编程语言本身存在的缺点。即使是最干净的代码也不可能自己解释写代码的人在写代码时在想些什么。有可能一切都是完美的,但仍然会出错。注释并不是干净代码的替代方法,而是代码的固有组成部分。

代码注释解析

在进一步讨论我们的问题之前,先让我们来看看不同的代码注释风格。

/**
* Javadoc-style documentation comment.
*/
void foo(void) {
if (bar > 10) {
/* regular comment */
...
}
}

常规注释就是编程语言本身定义的注释。根据经验,它们不应该被广泛使用,因为它们倾向于用来解释代码在做什么。

另一方面,文档注释从外部角度描述了全局变量、函数和模块。在函数体内部,它们基本上就是常规注释,工具通常会忽略它们。如果在函数内部有一些值得描述的东西,看看是否可以把它们放进函数描述本身。

文档注释本质上就是常规注释加上一些额外的附件,例如额外的正斜杠/// doc comment、感叹号//! doc comment或者/*! multiline doc comment /,或者Javadoc注释中的附加星号/* doc comment */。实际上,其他编程语言和工具也支持Javadoc,所以这里就以它为例子。

当然,你也可以使用常规注释,并忘掉那些时髦的标签。不过,一些文档生成器(如 Doxygen 或 Sphinx)可以直接根据注释创建 PDF、HTML 或手册页,大多数现代 IDE 为它们提供了额外的显示支持,省得你老是进行上下文切换,而且还可以为你提供一些有用的信息。

除了注释的后处理之外,注释的格式并不重要,重要的是你想要表达什么。

冗余的注释聚焦在错误的信息上

我们已经得出结论,即不应该记录代码在做什么,而是记录为什么要这么做以及怎样做,但这究竟意味着什么呢?

人们不喜欢写注释的一个常见原因是“它们只是在陈述已经很明显的东西”,所以注释是多余的。对于一般性的注释,确实难以反驳,特别是在面向对象语言的封装方面。一些简单的函数,比如 get_temperature() 的一般性描述可能如下所示:

/**
* Returns the temperature.
*/
int get_temperature(void) {
return temperature;
}

这里的注释确实没有增加太多的价值,它本质上只是重复了函数的名字,只是在说明这个函数的作用。这不是我们想要的,我们想要的是代码没有告诉我们的东西。

这个函数非常简单,所以写注释是绝对没有必要的。但话又说回来,软件开发当中没有什么东西是真正简单的。如果你够仔细,就会发现每个函数都有值得写的东西,而这些东西并不能从它的名字甚至是简单的一两行代码中看出来。

/**
* Returns the temperature in tenth degrees Celsius
* in range [0..1000], or -1 in case of an error.
*
* The temperature itself is set in the periodically
* executed read_temperature() function.
*
* Make sure to call init_adc() before calling this
* function here, or you will get undefined data.
*/
int get_temperature(void) {
return temperature;
}

事实证明,这个看似简单的函数有很多额外的信息可以写。如果只是看代码,可能无法明显地看出其中的信息,包括内部数据处理和程序流程。当然,深挖代码最终会获得同样的信息,但这样会浪费很多时间和脑力。

有人可能会说,我们没办法为这些实现细节写注释。为什么要这样?为什么不详细说明那些现细节,让别人可以更容易地理解代码在做什么?

每个函数都有自己的特点,至少会有一个细节、副作用、异常、限制,等等,它们都值得写出来,这意味着你可能需要从不同的角度来看待这个函数,才能找出它们。为此,你不可避免地要沉浸在代码隐藏的细节当中,这样才可能发现一些之前没有想到过的特殊情况。因此,代码注释不仅可以帮助读代码的人理解代码,还能帮助写代码的人更好地了解代码的内部细节。

如果你确实找不到有用的信息,那么应该问问自己为什么要写这些代码。这些代码存在的理由是什么?而这些理由就是有用的信息。之前的例子也可以是这样:

/**
* Returns the temperature.
*
* This is for testing purpose only and should
* never be called from a real program.
*/
int get_temperature(void) {
return temperature;
}

请注意,这段代码与之前完全相同,于是这又把我们引向了另一个问题“看似自解释的代码的注释通常都很简单”:它可能含糊不清,可能会导致错误的假设和潜在的缺陷。指出这些细节并消除潜在的歧义对于提升代码质量来说至关重要,这说明注释应该成为代码的重要组成部分。

同样,如果不深入研究代码,就无法发现每个函数的特点。当然,在这些不起眼的细节中,总有一些比另外一些更值得我们注意,并不是说函数所涉及的东西都会很有趣。认知偏差的范围很广,有些东西在这个时刻对你来说是显而易见的,并不意味着对于其他人来说也是这样——包括未来的你。

让注释成为代码的一部分

现在我们来看看另一个人们不喜欢写代码注释的原因:当代码发生改变时,注释会过时。但其实这只是一个偷懒的借口,在写代码时通常不会考虑将来会不会再去修改代码,一旦代码被提交并合并,就是确定和完美的,并永远保持原样。

代码注释的另一个更大的问题是,它们被视为独立于代码的东西,完全与代码相分离。但如果我们将其视为代码的组成部分,或者一种补充实体,那么只要代码发生变化就会很自然地去调整注释。

打破恶性循环

没有人喜欢糟糕的代码注释,但排斥写注释对解决这个问题并没有任何帮助。修复开发人员和代码注释之间的不正常关系是改善这种状况的唯一方法,而将注释视为代码的组成部分是改善这种关系的第一步。

毫无疑问,在形成这种思维方式之前需要进行练习。从长远来看,这对提升代码质量来说是有益而无害的。

英文原文:

https://hackaday.com/2019/03/05/good-code-documents-itself-and-other-hilarious-jokes-you-shouldnt-tell-yourself/