(转)程序员的注释之道

程序员的注释之道

作者 范德成

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

为什么会出现这种情况呢?我们当今所使用的编程语言,比如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表示忽略该参数,那么这样的值就是特殊值,需要得到说明。注解部分则要加入一些函数契约的细节。比如,前置条件(函数调用之前需要满足的条件)、后置条件(函数调用后,数据会变成什么样,比如这里的已排序状态就是后置条件)、时空复杂度(如果有需求的话)、典型的应用场合、特殊的应用场合等等。

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

posted @ 2014-10-25 19:01  quanben  阅读(206)  评论(0编辑  收藏  举报