程序员的注释之道

SAP高级开发工程师 范德成

2014年10月25日

写这篇文章之前,我所思考的前一个问题是代码的质量。而在编写了好的代码的前提下,代码的注释就成了代码质量的另一部分——它的作用初看时显得并不那么大,但是越到后面越显得重要。当一名勤奋的程序员为了一个大项目,洋洋洒洒地写了数千行代码之后,他转而去做该项目的另一个模块。等到一年后,他回头再来看他之前写的这几千行代码时,如果没有详细有意义的注释,那就得挠头了——因为当初没有写注释。

为什么会出现这种情况呢?我们当今所使用的编程语言,比如Java、C#、python等,不是早就是高级语言了嘛?已经不只是给机器看的代码,这些代码也能让人读懂了嘛?的确,它们是高级语言。但问题却在于,代码本身只写了怎么做一件事,做了这件事的结果如何。至于它做的是什么事,为什么要做这件事,在什么情况下可以用它来做这件事,代码本身是体现不出来的。

因此,注释有其独特的用处。代码本身的质量,来自正确性、安全性、可用性、可读性、可维护性、效率等好几个方面。好的注释,则能够帮助代码提升其可读性和可维护性,并最终为正确性等其他几个方带来正面影响。

那么,在保证代码本身的高质量的前提下,注释应该怎么写,才能有效呢?以下是我个人在多年工作中总结出来的经验教训。它适用于绝大多数命令式(imperative)编程语言:

1. 要为复杂的函数接口写清晰的注释。

2. 注释中要写清楚重要的细节。

3. 注释本身不要有冗余信息。

4. 注释要随时更新。

5. 当遇到复杂的、不直观的实现时,也要为实现写注释。

6. 要为简化、抽象和缩写的变量名或函数名,注释其全称及其含义。

7. 不要为不言自明的代码加注释。

8. 不要为频繁变化的代码写冗余的注释。

第一点,要为复杂的函数接口写清晰的注释。这里的函数接口指的是函数名及其参数、返回值、异常等的规范。更严格地来说,关于函数接口的注释定义了一个函数的契约。虽然我们所使用的编程语言不一定支持面向契约的编程,或者我们不选择这样一种编程模式,但我们仍可以在概念上用注释来表示一个函数的契约。

函数接口的注释该怎么写呢?以C#为例,它的一个函数会有函数名,参数和返回值。同时,参数和返回值又分别有其类型,还会有潜在的异常抛出。如下,是一个假想的做归并排序的函数的接口(该接口明显过于复杂,但其目的是为了演示如何写注释):

bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)
{
}

C#语言支持XML注释,其功能类似于Javadoc。而且,它的XML注释正好支持我们要注释的契约的所有内容。因此,我们可以用XML注释来写。对于这个函数,我们的注释如下:

/// <summary>
/// Performs merge sort on a part of an array with a comparer.
/// </summary>
/// <typeparam name="T">the type of members in the array to be sorted</param>
/// <param name="array">the array to sort</param>
/// <param name="begin">the beginning index of the part in the array to be sorted</param>
/// <param name="len">the length of the part in the array to be sorted</param>
/// <param name="comparer">the comparer used to compare elements in the array; see documentation on <see cref="System.Collections.Generic.IComparer<T>">IComparer</see> for more information</param>
/// <returns>
/// Whether the part of the array is already sorted.
/// </returns>
/// <remarks>
/// <para>This method checks whether the specified part of the source array is already sorted. If it is already sorted, the method returns true directly without changing the array. Otherwise, it sorts the part and returns false.</para>
/// <para>This method performs stable sort on the specified part of the array.</para>
/// <para>After calling this method, the range of the array from position <paramref name="begin" /> and of length <paramref name="len" /> is sorted.</para>
/// <para>The time complexity of this method is O(n log n), where n is the length of the part being sorted.</para>
/// </remarks>
/// <exception cref="System.IndexOutOfRangeException">
/// An IndexOutOfRangeException exception is thrown if the indices <paramref name="begin" /> and <paramref name="len" /> are out of range.
/// </exception>
bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)

中文翻译如下:

/// <summary>
/// 利用一个比较器,对一个数组中的一段内容执行归并排序。
/// </summary>
/// <typeparam name="T">被排序数组的元素类型</param>
/// <param name="array">要排序的数组</param>
/// <param name="begin">数组中要排序的段的起始下标</param>
/// <param name="len">数组中要排序的段的长度</param>
/// <param name="comparer">用于比较数组中元素大小的比较器;详细信息请参见<see cref="System.Collections.Generic.IComparer<T>">IComparer</see>的文档。</param>
/// <returns>
/// 该数组段是否本来就是有序的。
/// </returns>
/// <remarks>
/// <para>本方法将检查源数组的指定段是否已经处于有序状态。如果是这样,本方法将不修改数组内容,直接返回true。否则,本方法对指定段进行排序并返回false。</para>
/// <para>本方法对数组的指定段执行的排序是稳定排序。</para>
/// <para>在调用本方法之后,数组中从<paramref name="begin" />开始,长度为<paramref name="len" />的段将被排序。</para>
/// <para>本函数的时间复杂度为O(n log n),其中n是被排序部分的长度。</para>
/// </remarks>
/// <exception cref="System.IndexOutOfRangeException">
/// 若下标<paramref name="begin" />和<paramref name="len" />的范围溢出了,则抛出一个IndexOutOfRangeException异常。
/// </exception>
bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)

C#的XML注释还支持更多标签,比如example(示例)等。但是在我们日常的编程过程中,这些标签只是根据需要,偶尔用到。而上面我讲的这些则是经常要用到的。我们来看一下。首先,对于该函数,我们有一个简介(见summary部分)。然后,对于每个参数,以及函数的返回值,我们都要作一下说明。典型的异常要做说明,但并非每一个都有必要。对于复杂的函数,在简介里面没有办法用一句话概括所有意思的,需要写一段注解(remarks部分)。

其中函数的简介,力求用一句话(最多两句)把该函数该做什么事情给讲清楚。参数和返回值的注释,要把它们的含义讲一下,把它们的特殊值讲一下。特殊值的例子就是和平时传的值有所区别的值。比如,对于某些可选参数,传入null表示忽略该参数,那么这样的值就是特殊值,需要得到说明。注解部分则要加入一些函数契约的细节。比如,前置条件(函数调用之前需要满足的条件)、后置条件(函数调用后,数据会变成什么样,比如这里的已排序状态就是后置条件)、时空复杂度(如果有需求的话)、典型的应用场合、特殊的应用场合(这对于一些需要在特定上下文中执行的业务API来说很重要)等等。

这里,我想特别说明一下对于异常的注释。各个语言中,对于异常在语法上有着不同要求。C#不支持checked exception,它的设计者Anders Hejlsberg也不建议我们使用checked exception;Java则要求除了程序bug以外的异常都作为checked exception。所谓checked exception,是这样的一些异常类型,当它们被当前函数抛出时,当前函数必须在原型(即函数的接口)中声明这些异常。这样做的好处是,调用方知道将会收到哪些异常。缺点则是,应用程序扩展起来很不方便:当需要从底层增加一个新的异常类时,要么就得在应用程序函数体内调用这些API的地方捕获这些异常,要么就得在应用程序的函数原型中声明这些异常。否则就会导致编译错误。这对于一些库来说,就要求它们为了应用程序着想,把它们的所有异常类从一个基类衍生出来,从而应用程序只需要声明那个基类即可。出于这个原因,我们写注释,就只为典型的异常(在实际场合中容易遇到的异常)写注释。那些很难出现,甚至理论上不可能出现的异常完全不用写。而且,必要的时候,虽然checked
exception声明的可能是基类,但我们的注释却要反映出子类异常的具体发生情况。

第二点,注释中要写清楚重要的细节。在上一点的例子中已经带到过这一点:排序算法的时间复杂度和稳定性就是这种重要细节。

第三点,注释本身不要有冗余信息。注释是用来解释程序的。写注释的时候,为了注释的完整性,其内容可能会和程序代码的含义有一些重叠,亦即冗余信息。这一点很难完全避免。但是,注释内部不同部分之间出现的冗余则可以被避免。要避免这种冗余,没有定法,主要就是在写注释的过程中多读几遍,把重复内容删掉;有些地方可以用引用的方式,避免写重复的注释。

第四点,注释要随时更新。这一点是为了拥有高质量的注释,我们所必须做的事情。每次函数期望的功能、接口、契约发生改变时,注释也应该被相应地更新。当然,在实际的软件工程中,完全理想的时刻更新很难保证,但至少要力求对于大的变化,注释是足够新的,而且,我们在阅读程序代码的过程中如果发现注释不正确,也可以顺便调研一下程序的行为,并据此来更新注释。

第五点,当遇到复杂的、不直观的实现时,也要为实现写注释。有的时候,一个函数是很简短的,它的实现不言自明。但也有时候,一个函数的实现比较复杂。这可能是由于复杂的算法、复杂的业务逻辑等原因,这时,用以表示执行步骤的注释就会很方便。下面的拓扑排序函数就演示了这一点:

def topological_sort(graph, output_func):
    # Time complexity is O(n ^ 2)
    while len(graph) > 0:
        # Output dependency-free nodes
        to_pop_node_name = []
        for node_name in graph:
            # Remove and output all nodes without dependencies
            if len(graph[node_name].dependencies) == 0:
                output_func(node_name)
                to_pop_node_name.append(node_name)
        # Remove the nodes
        for node_name in to_pop_node_name:
            graph.pop(node_name)
        to_pop_node_name = None # finished using
        # Remove dependency links
        for node_name in graph:
            current_node = graph[node_name]
            to_pop_node_name = []
            for child_node_name in current_node.dependencies:
                if child_node_name not in graph:
                    to_pop_node_name.append(child_node_name)
            for child_node_name in to_pop_node_name:
                current_node.dependencies.pop(child_node_name)
            to_pop_node_name = None # finished using

其中,Output dependency-free nodes、Remove the nodes等都是对一个代码块的注释。此类注释能表示出程序的步骤。这种注释还可以跨越更大的范围,此时的技巧是,用大括号或begin、end的字样来表明其范围。如下所示:

int i;
int max = -1;
int sum = 0;

// Do the first thing {
for (i = 0; i < arr.Length; i++) {
    if (max < 0 || arr[i] > max) {
        max = arr[i];
    }
}
// }
// Do the second thing {
for (i = 0; i < arr.Length; i++) {
    sum += arr[i];
}
// }

对于那些老板不喜欢在注释中看见大括号的情况,用begin、end来代替:

// Begin of "Do the second thing"
for (i = 0; i < arr.Length; i++) {
    sum += arr[i];
}
// End of "Do the second thing"

准确使用注释中的大括号或者begin、end的好处是,当一大块代码中还有嵌套注释时,依然可以清晰地表示出一段范围。另外,我个人的准则是不给注释加上step 1、step 1.1、step 2之类字样,原因很简单,一旦在原有的步骤里面插入一个新步骤,那么从这个步骤往后所有步骤的编号都要调整,太费事儿。

另外,对于复杂的算法或业务需求,也需要加注释说明,以免在数年后回头来看这段代码时,忘记当初为什么是这样写的了。对于算法,要说清楚这个算法的需求是什么,它是怎么设计的,什么样的输入需要被处理,处理的原理是什么,等等。对于业务逻辑,需要说明要支持的输入情况(包括全局、静态变量和数据库等环境数据的情况)、所有的处理步骤在业务流程中的含义、处理完成之后对数据和业务状态带来什么影响,等等。以算法为例,下面的例子来自一个拓扑排序之前检查图是否有循环的方法:

def detect_loop(graph, o_loop):
    """
    detect_loop:
        Detects any loop in a graph.

    Parameters:
        graph - the graph to test
        o_loop - a list to receive the looping nodes

    Return value:
        True if a loop has been detected. False otherwise.
    """
    # We are using depth-first search to find loops.
    #
    # If we did not implement recursive calls, we could use trace-back in a
    # non-recursive manner; python default recursion limit is about 900, which
    # is in general enough here, as the dependency we analyze is usually less
    # than 100.
    #
    # Due to the fact that if we traverse a non-tree directed acyclic graph
    # (DAG), we may end up in a time complexity of O(2 ^ n), we make a deep
    # copy of the graph first, and make it into a tree. During the process, we
    # can detect loops. The time complexity is O(n ^ 2) where n is the number
    # of nodes.
    result = False
    copied_graph = GraphNode.deep_copy_graph(graph)
    # Cases:
    # Root 1 leads to a loop--will be detected and the function will return.
    #     The loop will have a link pointing back to an ancestor node or the
    #     current node itself.
    # Root 1 leads to a DAG--any link to a visited node (cannot be an ancestor
    #     node or the current node itself) will be detected and removed, and
    #     made into a tree
    # Root 1 leads to a DAG (call it DAG1), root 2 links to DAG1--no loop can
    #     involve DAG1. Reason: suppose there is a loop involving DAG1, then
    #     from a node that is a part of the intersection, we can go back to
    #     it through the links, thus making DAG1 not a DAG--contradiction.
    # So if root 2 leads to a loop--the loop will be detected by checking the
    #     DFS traversal stack
    # If all of root 1..n-1 lead to DAGs, and root n leads to a loop, it will
    #     be detected only there
    #
    # accessed: used to mark accessed nodes in the DAG. Its members are the
    # names of accessed nodes. When an accessed node is met through a link,
    # the traversal returns and the link is removed, because the link should
    # not be added to the tree.
    accessed = set() # of node name (string)
    # traversal_stack: is used to record the loop to show to the user
    traversal_stack = []
    for node_name in copied_graph:
        result = detect_loop_rec(copied_graph, node_name, accessed, traversal_stack)
        if result:
            # Loop detected
            o_loop.extend(traversal_stack)
            break
    return result

第六点,要为简化、抽象和缩写的变量名或函数名,注释其全称及其含义。比如,你用winnt4wks来代表Microsoft Windows NT Workstation 4 i386 Multiprocessor Free的时候,你就应该吧这个全名用注释的方式写在右边(或上方):

// winnt4wks: Microsoft Windows NT Workstation 4 i386 Multiprocessor Free
object winnt4wks;

注意上面的例子中,如果注释是写在变量名上方的,那么首先要用这个变量名先导,然后加个冒号,接下来才是解释。

第七点,不要为不言自明的代码加注释。这一点很自然。比如一段代码大家都知道是干啥的,就根本没必要写注释。写了注释反倒是干扰视听,弄不好将来修改代码时还需要维护,或者忘了维护,造成后来者被误导的情况。比如,下面代码的注释在生产代码中就完全是没必要的:

// Loop and print every element of the array
for (i = 0; i < arr.Length; i++) {
    Console.WriteLine(arr[i]);
}

第八点,不要为频繁变化的代码写冗余的注释。前面说到过,注释和代码所表达的含义可能有一点重合。此时,如果某段代码经常改,那么,基本上可以肯定的是,代码的负责人知道这段代码是什么含义,因为最近刚刚改过。那么,必要的注释仍然要加,但是与代码含义重复的、冗余的信息就不那么必要了,可以省略。

作为一名程序员,掌握了以上要点,就能写出好的注释,让自己的代码变得更加易读、易维护。

时间: 2024-10-21 03:54:12

程序员的注释之道的相关文章

读我编程,我快乐:程序员职业规划之道 第一章 选择市场

职业规划,从大学开始,很少考虑这个事情,大四那会儿还学过相关的一门课,当时也只是抱着学分修修到的心态.最近一年里开始思考这个问题,近期阅读了这本<我编程,我快乐:程序员职业规划之道>,开始学习着规划自己的职业.这本书不仅适合程序员,也适合其他职业. 准备按照章节摘抄出一些觉得有用的.引人思考的片段和一些有趣的练习题,再附加自己的感想. 在<程序员修炼之道>一书中,Dave Thomas和Andy Hunt谈到了编程中的偶然性:当你开始做一个程序的时候,或许手头上有一个从网上复制的示

《程序员的修炼之道:从小工到专家》的读书准备工作

创造是主动的,完成是被动的 创造是艺术,完成是任务 创造是激情,完成是无奈 当细品这三句话时,我就突然把它联想到了我和我这个专业的关系;大概在家里憋了7天吧,我还是在前天爆发了,在不断的挣扎.纠结.和自我否定等消极情绪中,这三句话点通了我,我试着将恐惧.压力.惰性以及侥幸全部转化为行动以及对我这个专业的不断认识中.去百度.知乎.简书等学术网站寻找软件工程这个专业对我自身的意义与价值,同时也在搜罗着看哪本书能够让自己快速认可我的这个专业,并在这个专业上有所图(当时分流时因为导师的一句话就迷迷糊糊的

《程序员的修炼之道——从小工到专家》读第一章有感

         九月份,我读了<程序员的修炼之道--从小工到专家>这本书,虽然只是读了其中小小的一章,却让我收获了许多,收益匪浅.     从<程序员的修炼之道>这本书的介绍部分可以看出,该书是一本很棒的作品,该书由一系列独立的部分组成,讲述了许多富有娱乐性的奇闻轶事.有思想性的例子.以及有趣的类比,内容丰富多彩,并且本书还阐明了软件开发的许多不同方面的最佳实践和重大陷阱,无论是初学者还是有经验的编程员,都能从中获益.书中更是例举了许多程序员对这本书的看法,从他们的介绍中让我认识

程序员的修炼之道:从小工到专家读后感2

程序的修炼之道:从小工到专家 第一章:注重实效的哲学 1.我的源代码让猫吃了 J.B.Bossuet说过“在所有的弱点中,最大的弱点就是害怕弱点”.一个注重实效的程序员,并不害怕承认无知与错误.一个被认为最好的项目中,尽管有彻底的测试,良好的文档以及足够的自动化(自动化是什么鬼?),程序还是会出错. 但是,一个程序员必须要诚实和坦率来应对这个问题.责任是一个程序员应该主动承担的东西.我们能承诺完成一件确定的事情,但是我们不能直接控制事情的每一个方面,所以做之前需要对这件事情先做风险预估.但是,当

从三流小公司到一线大厂,聊聊程序员的成长之道

? 作者:黄小斜 文章来源:微信公众号[程序员江湖] 我想,没有一个程序员能够一步登天,一下子达到很高的高度,有些路是我们必须要走的,有些阶段是我们必须要经历的.有入门,才有进阶,才有更上一层楼. 别看我现在在大厂,但是我也待过小到不能小的小公司,也许你会感兴趣,那么不妨听我说说,我在学习编程路上待过的公司吧. 不到20人的小公司 我在大学的时候学的不是计算机,上的课程基本上和计算机也没太大关系,但是我从小还是比较喜欢捣鼓电脑的,不瞒你说,以打游戏为主,修电脑为辅. 兴趣使然,于是大学期间想要自

程序员必备注释

1 // _ooOoo_ 2 // o8888888o 3 // 88" . "88 4 // (| -_- |) 5 // O\ = /O 6 // ____/`---'\____ 7 // . ' \\| |// `. 8 // / \\||| : |||// 9 // / _||||| -:- |||||- 10 // | | \\\ - /// | | 11 // | \_| ''\---/'' | | 12 // \ .-\__ `-` ___/-. / 13 // ___`

为什么常说程序员35岁是道坎?

在网上看到两类帖子1.关于程序员"干"不到352.程序员哪怕干到7.80都无妨 这里用我大树级的思维来给大家讲解一下. 首先我们要结合国情,如果你刚出道做程序员,除非你有特别的天分.碰到巨帅的大牛带你,那么一般来说你都是从小白起家的.在这个过程中应对了"初生牛犊不怕虎"的特征,我见过很多刚出道的程序员魄力很大,不少童鞋觉得未来成为第二个马云并不是很难.(想想我自己吧,现在已经觉得比tm登天还难了,我情愿去登天) 一.如果你没有特别的机遇 那么在20-25岁 基本上你

读《我编程,我快乐--程序员职业规划之道》

作为一名程序员,不得不去关注职业规划. <我编程,我快乐>这本书对于程序员的职业发展有很大的启迪作用. 书中印象深刻的几点记录如下: 想想自己所在的公司靠什么盈利 热爱所从事的工作,或者离开 自己所做的工作的价值不要局限在一家公司,要放眼整个行业 要扩大自己的影响力 享受过程,不是结果 保持学习动力,关注最新技术 倾听客户的声音,不要只是闷头写代码 大公司可以获得稳定且固定的工作,小公司可以承担更多的责任 原文地址:https://www.cnblogs.com/lanyangsh/p/103

读后感《程序员的修炼之道:从小工到专家》1

从前言得知,这本书想要告诉我们以一种我们能够遵循的方式去编程,这可能是每个编程人员的福音吧,还有要"思考你的工作",思考出合理高效的解决方案,我希望通过这本书 的阅读,能够更加的注重实效,具有一些适合编程的思想,“纸上得来终觉浅,绝知此事要躬行”,在日后好好实践才是良方. 自开始接触编程以来,编程就是一个抽象的存在,其实不然,“编程是一种技艺”,“编程可归结为让计算机做你(或你的用户)想要做的事情”,通过编程吧一些切合实际的想法或功能变成程序,书中提及的“注重实效的程序员”应该是早期的