[译] Writing system software: code comments.

原文: Writing system software: code comments.

本文使用 gpt-4o-mini 翻译。使用的 宝玉二次翻译法。总体阅读仍显晦涩,但基本意思已经表达清楚。

在很长一段时间里,我一直想为我的“编写系统软件”系列在 YouTube 上录制一个关于代码注释的新视频。但是,经过深思熟虑,我意识到这个话题更适合写成一篇博客文章,因此我决定在这里进行分析。在这篇文章中,我将探讨 Redis 中的注释,并尝试对其进行分类。我希望能够阐明为什么我认为编写注释对编写高质量代码至关重要,这种代码不仅在长期内可维护,而且在修改和调试时能够被他人和开发者本人理解。

并非所有人都认同这一观点。很多人认为,如果代码设计得足够好,注释就显得多余。他们认为,良好的设计使得代码本身就能说明它的功能,因此没有必要添加注释。对此,我有两个主要的不同看法:

  1. 很多注释并没有说明代码在做什么,而是解释了从代码本身无法直接理解的内容。通常,这些缺失的信息是关于 为什么 代码要执行某个操作,或是为什么选择了一种明确的做法而不是其他更自然的选择。
  2. 虽然逐行记录代码的功能通常并不必要,因为代码本身是可以理解的,但编写可读代码的一个重要目标是减少读者在阅读代码时需要理解的努力和细节。因此,在我看来,注释可以作为降低读者认知负担的有效工具。

下面的代码片段很好地说明了上述第二点。请注意,文章中的所有代码片段均来自 Redis 源代码,并且每段代码前都会标注其出处。我们使用的分支是当前的“unstable”,哈希值为 32e0d237。

scripting.c:

    /* 初始栈:数组 */
    lua_getglobal(lua, "table");
    lua_pushstring(lua, "sort");
    lua_gettable(lua, -2);       /* 栈:数组,表,table.sort */
    lua_pushvalue(lua, -3);      /* 栈:数组,表,table.sort,数组 */
    if (lua_pcall(lua, 1, 0, 0)) {
        /* 栈:数组,表,错误 */

        /* 我们不关心错误,假设问题出在数组中有“false”元素,
         * 所以我们尝试用一个更慢但能处理这种情况的函数
         * 重新排序,即:table.sort(table, __redis__compare_helper) */
        lua_pop(lua, 1);             /* 栈:数组,表 */
        lua_pushstring(lua, "sort"); /* 栈:数组,表,sort */
        lua_gettable(lua, -2);       /* 栈:数组,表,table.sort */
        lua_pushvalue(lua, -3);      /* 栈:数组,表,table.sort,数组 */
        lua_getglobal(lua, "__redis__compare_helper");
        /* 栈:数组,表,table.sort,数组,__redis__compare_helper */
        lua_call(lua, 2, 0);
    }

Lua 使用栈结构的 API。读者只需参考 Lua API 文档,就能在跟随每个函数调用的过程中,心理上重建栈的状态。但为什么要让读者付出这样的努力呢?在编写代码时,原作者无论如何都要进行这样的心理分析。我在代码中所做的,只是为每一行添加关于当前栈布局的注解。现在,阅读这段代码变得非常简单,尽管 Lua API 的使用可能较为复杂。

我的目标并不仅仅是分享我对注释作为工具的看法,强调它们在提供源代码某个局部不明显的背景方面的作用。同时,我也想提供一些证据,证明那些历史上被认为无用甚至有害的注释,实际上是有其价值的,即那些说明 代码在做什么 而非 为什么 的注释。

注释的分类

我开始这项工作的方式是随机阅读 Redis 源代码的不同部分,以检查注释在不同上下文中的有效性及其原因。很快我发现,注释在功能、写作风格、长度和更新频率等方面存在很大的差异,因此它们的用途也各不相同。最终,我将这项工作转变为一个分类任务。

在我的研究中,我识别出了九种类型的注释:

  • 函数注释
  • 设计注释
  • 为什么注释
  • 教学注释
  • 检查表注释
  • 指导注释
  • 简单注释
  • 债务注释
  • 备份注释

在我看来,前六种注释都是非常积极的形式,而最后三种则值得怀疑。在接下来的部分中,我将通过 Redis 源代码中的实例对每种类型进行分析。

函数注释

函数注释 的目的是让读者在阅读代码之前能够理解其功能。读者在阅读完注释后,应该能够将某些代码视为黑箱,遵循特定的规则。通常,函数注释 位于函数定义的顶部,但也可能出现在其他地方,例如记录类、宏或其他功能性隔离的代码块,以定义某些接口。

rax.c:

    /* 在当前节点的子树中寻找最大键。内存不足时返回 0,否则返回 1。
     * 这是不同迭代函数的辅助函数。 */
    int raxSeekGreatest(raxIterator *it) {
    ...

函数注释 实际上是一种内联的 API 文档。如果注释写得足够好,用户通常可以回到他们正在阅读的内容(调用该 API 的代码),而无需深入了解函数、类或宏的实现。

在所有类型的注释中,函数注释 是编程社区普遍认为必要的。唯一需要考虑的是,是否将大量的 API 参考文档放置在代码内部是个好主意。对我来说,答案很简单:我希望 API 文档与代码完全一致。随着代码的变化,文档也应该随之更新。因此,通过将 函数注释 作为函数或其他元素的前言,我们使 API 文档与代码紧密相连,达成了以下三点:

  • 随着代码的更改,文档也能轻松同步更新,避免 API 参考过时。
  • 这种方法最大限度地提高了更改作者的可能性,因为通常是他们对更改有更深入的理解,同时也是 API 文档更新的作者。
  • 阅读代码时,可以方便地找到函数或方法的文档,从而使读者专注于代码本身,而不必在代码和文档之间切换。

设计注释

函数注释 通常位于函数开头不同,设计注释 通常位于文件的开头。设计注释 主要说明了某段代码如何以及为什么使用特定的算法、技术、技巧和实现。这提供了对代码实现内容的高层次概述,使得阅读代码变得更加简单。此外,我通常对有 设计注释 的代码更加信任。至少我知道在开发过程中,某个阶段进行了明确的设计。

根据我的经验,设计注释 在解释实现方案时也非常有用,尤其是在实现看起来过于简单的情况下,能够说明竞争方案是什么,以及为什么选择一个简单的解决方案。若设计合理,读者会相信这个解决方案是合适的,而这种简单性源于深思熟虑的过程,而非懒惰或仅仅是对基本内容的理解。

bio.c:

    * 设计
    * ------
    *
    * 设计是简单的,我们有一个表示要执行的工作的结构,
    * 每种工作类型都有不同的线程和工作队列。
    * 每个线程在其队列中等待新工作,并按顺序处理每个工作。
    ...

为什么注释

为什么注释 解释了代码执行某个操作的原因,即使代码的功能非常清楚。以下是来自 Redis 复制代码的一个示例。

replication.c:

    if (idle > server.repl_backlog_time_limit) {
        /* 当我们释放回溯时,总是使用新的
         * 复制 ID,并清除 ID2。这是必要的,
         * 因为当没有回溯时,master_repl_offset
         * 不会更新,但我们仍会保留复制 ID,
         * 这会导致以下问题:
         *
         * 1. 我们是主实例。
         * 2. 我们的副本被提升为主。它的 repl-id-2
         *    将与我们的 repl-id 相同。
         * 3. 作为主实例,我们接收了一些更新,
         *    但这些更新不会增加 master_repl_offset。
         * 4. 后来我们转为副本,连接到新的主,
         *    它将通过第二个复制 ID 接受我们的 PSYNC 请求,
         *    但这样会导致数据不一致。 */
        changeReplicationId();
        clearReplicationId2();
        freeReplicationBacklog();
        serverLog(LL_NOTICE,
            "Replication backlog freed after %d seconds "
            "without connected replicas.",
            (int) server.repl_backlog_time_limit);
    }

如果我仅仅查看函数调用,几乎没有什么值得怀疑的:如果超时到达,改变主复制 ID,清除次要 ID,最后释放复制回溯。然而,为什么在释放回溯时需要更改复制 ID 却并不明显。

这种情况在软件达到一定复杂性后经常发生。无论涉及到什么代码,复制协议本身都有一定的复杂性,因此我们需要采取措施,以确保其他不良后果不会发生。可能这些类型的注释在某种程度上是思考系统并检查是否应该改进的机会,以便不再需要这种复杂性,从而也可以删除注释。然而,通常简化某些内容可能会使其他事情变得更复杂,或者根本不可行,或者需要未来的工作来打破向后兼容性。

这里还有另一个例子。

replication.c:

    /* 当服务器有待发送给客户端的挂起数据
     * 时,不能发出 SYNC 命令。我们需要一个新
     * 的回复缓冲区,记录 BGSAVE 和当前
     * 数据集之间的差异,以便在需要时可以
     * 复制到其他副本。 */
    if (clientHasPendingReplies(c)) {
        addReplyError(c, "SYNC and PSYNC are invalid with pending output");
        return;
    }

如果在仍有挂起输出(来自过去命令)要发送给客户端时运行 SYNC,命令应失败,因为在复制握手期间,客户端的输出缓冲区用于累积更改,并可能在我们为第一个副本创建 RDB 文件时被重复用于处理其他副本。这就是我们这样做的原因。我们所做的事情看似简单,但如果没有注释,为什么这样做就变得不那么明显。

人们可能会认为这样的注释仅在描述复杂协议和交互时才需要,就像复制的情况一样。真的是这样吗?让我们完全改变文件和目标,看看这样的注释是否仍然无处不在。

expire.c:

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db + (current_db % server.dbnum);

        /* 现在增加数据库 ID,以确保如果我们在当前数据库
         * 中用尽时间,我们将从下一个数据库开始。这允许
         * 在数据库之间均匀分配时间。 */
        current_db++;
        ...

这是一个有趣的例子。我们希望在有时间的情况下从不同的数据库中过期键。然而,与其在处理当前数据库的循环结束时增加“数据库 ID”,不如我们选择当前的数据库并立即增加下一个要处理的数据库的 ID(在下一次调用该函数时)。这样,如果函数因在单次调用中花费太多时间而终止,我们就不必重新从同一个数据库开始,避免让逻辑上已过期的键在其他数据库中积累,因为我们过于专注于反复处理同一个数据库。

通过这样的注释,我们解释了为什么在这个阶段要增加 ID,同时也提醒未来的维护者在修改代码时应保留这一点。请注意,如果没有注释,代码看起来完全无害。选择、增加、继续执行工作。没有明显的理由让我们不把增加操作移到循环的末尾,那里看起来更自然。

有趣的是,循环的增量确实在原始代码中位于末尾。在修复期间将其移到此处,同时添加了注释。因此可以说,这是一种“回归注释”。

教学注释

教学注释 并不试图解释代码本身或需要注意的某些副作用。相反,它们教授的是代码所涉及的 领域(比如数学、计算机图形学、网络、统计学、复杂数据结构等),这些领域可能超出了读者的知识范围,或者细节过于繁琐,难以记忆。

在版本 5 中,LOLWUT 命令需要在屏幕上绘制旋转的方块(http://antirez.com/news/123)。为此,它使用了一些基本的三角学:尽管所用的数学很简单,但许多阅读 Redis 源代码的程序员可能没有数学背景,因此函数开头的注释解释了函数内部将会发生的事情。

lolwut5.c:

    /* 在指定的 x,y 坐标中心绘制一个方块,具有指定的
     * 旋转角度和大小。为了绘制一个旋转的方块,我们使用
     * 一个简单的参数方程:
     *
     *  x = sin(k)
     *  y = cos(k)
     *
     * 这些方程描述了从 0 到 2*PI 的圆。因此,如果我们从
     * 45 度(即 k = PI/4)开始,得到第一个点,然后通过
     * 将 K 增加 PI/2(90 度),我们将得到方块的其他三个点。
     * 为了旋转方块,我们只需从 k = PI/4 + rotation_angle 开始,
     * 然后就完成了。
     *
     * 当然,上述方程描述的是半径为 1 的圆内的方块,
     * 因此为了绘制更大的方块,我们必须将获得的坐标乘以
     * 放大倍数并进行平移。然而,这比实现 2D 形状的
     * 抽象概念以及执行旋转和位移变换要简单得多,因此
     * 对于 LOLWUT 来说,这是一个不错的选择。 */

我认为 教学注释 非常有价值。如果读者对这些概念不熟悉,它们可以提供知识,或者至少为进一步的研究提供起点。这意味着 教学注释 可以增加程序员阅读某些代码的能力:编写能够被更多程序员理解的代码是我的一个重要目标。许多开发者可能数学能力有限,但却是优秀的程序员,能够提出绝妙的修复或优化。而且,通常代码应该被阅读而非执行,因为它是为人类编写的,供人类使用。

在某些情况下,教学注释 几乎是不可避免的,以便编写出合理的代码。一个很好的例子是 Redis 的基数树实现。基数树是一种复杂的数据结构。Redis 的实现重新阐述了整个数据结构理论,展示了不同情况以及算法如何合并或拆分节点。每段注释后面都有实现了前述内容的代码。在几个月未触碰实现基数树的文件后,我能够在几分钟内打开它,修复一个错误,然后继续做其他事情。因为注释的内容与代码本身的实现相同,所以不需要重新学习基数树的工作原理。

注释内容较长,因此我只展示某些片段。

rax.c:

    /* 如果我们停下的节点是一个压缩节点,我们需要
     * 在继续之前将其拆分。
     *
     * 拆分压缩节点有几种可能的情况。
     * 想象一下,我们当前停留的节点 'h' 是一个压缩
     * 节点,包含字符串“ANNIBALE”(这意味着它表示
     * 节点 A -> N -> N -> I -> B -> A -> L -> E,且
     * 该节点的唯一子指针指向 'E' 节点,因为请记住,
     * 我们在图的边缘有字符,而不是在节点内部。
     *
     * 为了展示一个真实的案例,想象我们的节点还指向
     * 另一个压缩节点,最终指向没有子节点的节点,表示 'O':
     *
     *     “ANNIBALE” -> “SCO” -> []
     ...
     * 3a. 如果 $SPLITPOS == 0:
     *     用拆分节点替换旧节点,如果有的话,复制辅助
     *     数据。修复父节点的引用。最终释放旧节点
     *     (我们仍然需要其数据以供算法的下一步)。
     *
     * 3b. 如果 $SPLITPOS != 0:
     *     修剪压缩节点(同时重新分配),使其包含
     *     $splitpos 字符。更改子指针以链接到拆分节点。
     *     如果新的压缩节点长度仅为 1,则将 iscompr 设置为 0
     *     (布局相同)。修复父节点的引用。
     ...

如您所见,注释中的描述与代码中的标签相匹配。以这种形式很难展示所有内容,因此如果您想了解完整的想法,请查看完整文件:

https://github.com/antirez/redis/blob/unstable/src/rax.c

这种级别的注释并不适用于所有情况,但对于像基数树这样的复杂数据结构,确实充满了细节和边缘情况。它们难以记忆,某些细节是特定于某种实现的。显然,为一个链表这样做并没有多大意义。是否值得这样做完全取决于个人的敏感度。

检查表注释

检查表注释 是一种非常常见且特殊的注释形式:有时由于语言限制、设计问题或系统自然复杂性,无法将某个概念或接口集中在一个部分,因此代码中会有地方提醒你在其他地方记得做某些事情。一般而言,其内容为:

    /* 警告:如果在这里添加类型 ID,请确保也修改
     * 函数 getTypeNameByID()。 */

在理想情况下,这种情况不应该发生,但在实际中,有时别无选择。例如,Redis 类型可以通过“对象类型”结构表示,每个对象可以链接到其所属的类型,这样就可以如此使用:

    printf("Type is %s\n", myobject->type->name);

但你猜怎么着?这对我们来说代价太高,因为 Redis 对象的表示如下:

    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:LRU_BITS; /* LRU 时间(相对于全局 lru_clock)或
                                * LFU 数据(最不重要的 8 位频率
                                * 和最重要的 16 位访问时间)。 */
        int refcount;
        void *ptr;
    } robj;

我们使用 4 位而不是 64 位来表示类型。这只是表明有时事情并没有想象中那样集中和自然。当情况如此时,使用防御性注释会有所帮助,以确保如果某段代码被修改,能够提醒你还需在其他部分进行相应的修改。具体来说,检查表注释 能执行以下一项或两项操作:

  • 它告诉你在修改某些内容时需要执行的一组操作。
  • 它警告你某些更改应该如何进行。

blocked.c 中的另一个例子,当引入新的阻塞类型时:

     * 在实现新类型的阻塞操作时,实施
     * 应该修改 unblockClient() 和 replyToBlockedClientTimedOut(),
     * 以处理这两个函数的 btype 特定行为。
     * 如果阻塞操作等待某些键状态改变,应该
     * 更新 clusterRedirectBlockedClientIfNeeded() 函数。

检查表注释 在某种程度上也类似于某些 为什么注释 的上下文中使用:当不明确为何某段代码必须在特定位置执行时,检查表注释 会更偏向于告诉你在修改时应遵循的规则(在此例中,规则是遵循特定顺序),而不会破坏代码的行为。

    /* 更新我们关于服务插槽的信息。
     *
     * 注意:这必须在我们更新主/副本状态后进行,
     * 以确保设置 CLUSTER_NODE_MASTER 标志。 */

检查表注释 在 Linux 内核中非常常见,那里某些操作的顺序是极其重要的。

指导注释

我在 指导注释 方面的使用程度甚至可以说,Redis 中大多数注释实际上都是 指导注释。此外,指导注释 正是大多数人认为完全无用的注释。

  • 它们并没有说明代码中不清楚的部分。
  • 指导注释中没有设计提示。

指导注释 的唯一作用是:在程序员处理源代码时,帮助他们清晰地划分、节奏和引导接下来要阅读的内容。

指导注释 存在的唯一原因就是降低程序员阅读某段代码时的认知负担。

rax.c:

    /* 如果有节点回调,则调用节点回调,并在回调返回
     * true 时替换节点指针。 */
    if (it->node_cb && it->node_cb(&it->node))
        memcpy(cp, &it->node, sizeof(it->node));

    /* 对于“下一个”步骤,每次找到关键字时停止,
     * 因为该关键字在子节点中按字典序小于
     * 后续关键字。 */
    if (it->node->iskey) {
        it->data = raxGetData(it->node);
        return 1;
    }

上述代码中的注释并没有为代码增加任何价值。以上的 指导注释 将帮助你阅读代码,而且它们会确认你理解得没错。更多示例。

networking.c:

    /* 记录与副本的连接断开 */
    if ((c->flags & CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)) {
        serverLog(LL_WARNING, "Connection with replica %s lost.",
            replicationGetSlaveName(c));
    }

    /* 释放查询缓冲区 */
    sdsfree(c->querybuf);
    sdsfree(c->pending_querybuf);
    c->querybuf = NULL;

    /* 释放用于阻塞操作的结构 */
    if (c->flags & CLIENT_BLOCKED) unblockClient(c);
    dictRelease(c->bpop.keys);

    /* UNWATCH 所有键 */
    unwatchAllKeys(c);
    listRelease(c->watched_keys);

    /* 取消订阅所有 pubsub 通道 */
    pubsubUnsubscribeAllChannels(c, 0);
    pubsubUnsubscribeAllPatterns(c, 0);
    dictRelease(c->pubsub_channels);
    listRelease(c->pubsub_patterns);

    /* 释放数据结构 */
    listRelease(c->reply);
    freeClientArgv(c);

    /* 取消链接客户端:这将关闭套接字,移除 I/O
     * 处理程序,并移除客户端在不同地方的引用。 */
    unlinkClient(c);

Redis 中充满了 指导注释,因此基本上你打开的每个文件都会包含大量这样的注释。为什么要这样做?在我分析的所有注释类型中,我承认这是绝对最主观的一种。我并不认为没有这些注释的代码就一定劣于有这些注释的代码,但我坚信,如果人们认为 Redis 代码易于阅读,部分原因正是因为所有的 指导注释

指导注释 除了上述功能外,还有其他一些用处。由于它们清晰地将代码划分为独立的部分,因此在代码中添加新内容时,很可能会将其放入适当的部分,而不是随机插入某个地方。将相关语句放在一起是一个巨大的可读性提升。

此外,确保在调用 unlinkClient() 函数之前查看上面的 指导注释指导注释 简要告知读者该函数将要执行的操作,避免了如果你只对整体概念感兴趣而需要跳回到函数内部的麻烦。

简单注释

简单注释 是最不受欢迎的注释类型之一。这类注释通常显得冗余,或者与代码几乎没有任何附加信息。以下是一个 简单注释 的例子:

    array_len++; /* 增加我们数组的长度。 */

在这种情况下,注释的内容与代码几乎相同,直接阅读代码就足够了。这样的注释不仅没有帮助,反而可能会分散注意力。简单注释 的存在通常表明代码可能不够清晰,或者开发者对代码的理解不够深入。

简单注释 的另一种常见形式是强调某个特定的实现细节,但这些细节在代码中已经很明显。例如:

    if (x > 0) {
        /* 如果 x 大于 0,执行某些操作 */
        doSomething();
    }

在这种情况下,条件的意义已经通过代码本身得到了很好的表达,因此注释是多余的。

简单注释 不应被视为有效的注释方式。开发者在编写代码时应努力使代码本身足够清晰,以至于不需要添加 简单注释。避免使用这种类型的注释可以提高代码的可读性和可维护性。

债务注释

债务注释 是指在源代码中硬编码的技术债务声明:

t_stream.c:

    /* 在这里我们应该执行垃圾回收,以防此时
     * 列表包中删除了太多条目。 */
    entries -= to_delete;
    marked_deleted += to_delete;
    if (entries + marked_deleted > 10 && marked_deleted > entries / 2) {
        /* TODO: 执行垃圾回收。 */
    }

上述片段来自 Redis 流的实现。Redis 流允许从中间删除元素,使用 XDEL 命令。这在不同的上下文中可能会很有用,尤其是在隐私法规背景下,某些数据在任何情况下都不能被保留。对于主要是追加数据结构的使用场景,这是一个非常特殊的用例,但如果用户开始删除超过 50% 的中间项,流会开始分裂,变成“宏节点”。条目仅被标记为已删除,但只有当给定宏节点中的所有条目都被释放后,它们才能被回收。因此,用户的大量删除将改变流的内存行为。

目前,这看起来不是个问题,因为我不指望用户会删除流中的大部分历史记录。然而,将来可能希望引入垃圾回收:一旦删除条目与现有条目之间的比率达到一定水平,宏节点就可以被压缩。此外,垃圾回收后,临近的节点可能会粘合在一起。我有点担心,之后我可能会忘记垃圾回收的入口点,因此我添加了 TODO 注释,甚至写下了触发条件。

这可能不是一个好主意。更好的做法是在文件顶部的 设计注释 中写明为什么目前不执行垃圾回收,以及如果我们想稍后添加它的入口点。

FIXME、TODO、XXX、“这是一个 hack” 等都是 债务注释 的形式。它们通常不太理想,我尽量避免使用,但并不总是可能。有时,与其永远忘记一个问题,我更愿意在源代码中留个记号。至少应该定期搜索这些注释,看看是否可以将笔记放在更合适的地方,或者该问题是否不再相关,或者是否可以立即修复。

备份注释

最后,备份注释 是开发者为某段代码或整个函数的旧版本添加的注释,因为他们对新版本的更改感到不安。令人困惑的是,即使在我们拥有 Git 的情况下,这种情况仍然会发生。我想人们对失去被认为更稳定或更合理的代码片段感到不安,尽管这些代码片段存在于几年前的提交中。

但是,源代码并不是用来做备份的。如果你想保存某个函数或代码部分的旧版本,那么你的工作就没有完成,不能提交。要么确保新函数比旧函数更好,要么将它保留在你的开发树中,直到你确信。

备份注释 结束了我的分类。让我们尝试一些总结。

注释作为分析工具

注释就像是“橡皮鸭调试”的升级版,区别在于你并不是在与橡皮鸭对话,而是在与未来的代码读者交流。这种对话比与橡皮鸭更具挑战性,因为读者可能会通过 Twitter 反馈你的注释。因此,在这个过程中,你需要认真思考你所表达的内容是否 是可以接受的,是否值得,以及是否足够好。如果答案是否定的,那么你就需要反思并提出更合适的表达。

这一过程与编写文档时的思考非常相似:作者尝试概述给定代码片段的要点、保证和副作用。这通常也是发现潜在问题的好机会。在描述某个功能时,发现其漏洞是很容易的……你无法完全描述所有内容,因为你对某个特定行为并不确定:这种行为往往是复杂性随机显现的。你当然不希望这样,所以你会回过头去修正问题。我认为这是写注释的一个极好的理由。

编写好的注释比编写好的代码更难

你可能会觉得写注释是一种较低级的工作。毕竟你 会编码!然而,请考虑这一点:代码是由一系列语句和函数调用组成的,无论你的编程范式是什么。有时,如果代码本身不够清晰,这些语句实际上并没有太大意义。注释总是要求开发者进行某种设计过程,并更深入地理解他们正在编写的代码。此外,为了写出好的注释,你需要提高你的写作技能。这些写作技能也会帮助你撰写电子邮件、文档、设计文档、博客文章和提交消息。

我写代码是因为我有一种迫切的愿望去分享和沟通,胜过其他任何事情。注释是对代码的补充,帮助表达我们的努力,毕竟我喜欢写注释,就像我喜欢编写代码一样。

(感谢 Michel Martens 在撰写这篇博客文章期间提供的反馈)

Blog