解释复杂代码的注释有什么问题?


236

许多人声称“评论应解释“为什么”,而不是“如何””。其他人则说“代码应该是自我记录的”,注释应该很少。罗伯特·C·马丁(Robert C. Martin)声称(用我自己的话改写)经常“评论是写得不好的代码的道歉”。

我的问题如下:

解释复杂的算法或带有描述性注释的冗长而复杂的代码段有什么问题?

这样,无需其他开发人员(包括您自己)逐行阅读整个算法来弄清楚算法的作用,他们只需阅读您用普通英语编写的友好描述性注释即可。

英语是“设计”成易于人类理解的。但是,Java,Ruby或Perl旨在平衡人类可读性和计算机可读性,从而损害了文本的人类可读性。人可以更快地理解英语,而他/她可以理解具有相同含义的代码(只要操作不琐碎)。

因此,在编写了用部分人类可读的编程语言编写的复杂代码之后,为什么不添加友好而易懂的英语的描述性简明注释来解释代码的操作呢?

有人会说“代码不难理解”,“使函数变小”,“使用描述性名称”,“不要编写意大利面条式代码”。

但是我们都知道这还不够。这些仅是准则-重要且有用的准则- 但它们并不能改变某些算法很复杂的事实。因此在逐行阅读它们时很难理解。

用一些关于它的一般操作的注释来解释一个复杂的算法真的很糟糕吗?用注释解释复杂的代码有什么问题?


14
如果那是令人费解的,请尝试将其重构为较小的部分。
旺市(Vaughan Hilts)2014年

151
在理论上,理论与实践之间没有区别。实际上,有。
Scott Leadley

5
@mattnz:更直接地,在编写注释时,您会沉迷于此代码解决的问题。您下次访问时,您将有较少的能力这个问题
史蒂夫·杰索普

26
函数或方法的“作用”从其名称应显而易见。从代码中可以很明显地看出来。为什么要这样做,使用了哪些隐含假设,为了理解算法需要阅读哪些论文,等等-应该放在注释中。
SK-logic

11
我认为以下许多答复是故意误解您的问题。注释您的代码没有错。如果您觉得需要写一个解释性注释,那么您需要这样做。
Tony Ennis

Answers:


408

用外行的话来说:

  • 什么不妥的意见本身。错误的是编写需要这些注释的代码,或者假定只要您以通俗易懂的英语来解释它,就可以编写复杂的代码。
  • 更改代码时,注释不会自动更新。这就是为什么注释常常与代码不同步的原因。
  • 注释不会使代码更易于测试。
  • 道歉还不错。您所做的事情要求道歉(编写不易理解的代码)是不好的。
  • 能够编写简单代码来解决复杂问题的程序员比编写复杂代码然后写一个长注释解释其代码功能的程序员更好。

底线:

自我解释是件好事,不需要这样做会更好。


91
当好的评论可以在更少的时间内完成工作时,通常不可能证明花费雇主的钱来重写代码是不言自明的。尽职的程序员必须每次都使用自己的判断。
aecolley 2014年

34
@aecolley从头开始编写自解释代码会更好。
TulainsCórdova2014年

127
有时,不言自明的代码不足以解决当今的硬件和软件问题。商业逻辑是众所周知的……曲折。具有完善的软件解决方案的问题子集比在经济上有用的解决问题集要小得多。
Scott Leadley 2014年

62
@rwong:相反,我经常发现自己在业务逻辑中写了更多评论,因为重要的是要确切显示代码如何符合规定的要求:“这是防止我们所有人在任何情况下都因电汇欺诈而入狱的行。刑法典”。如果这只是一种算法,那么,如果绝对必要,程序员可以从头开始解决这个问题。对于业务逻辑,您需要在同一时间同时在同一房间内聘请律师和客户。我的“常识”可能与普通应用程序程序员不在同一个领域;-)
史蒂夫·杰索普

29
@ user61852除了对于刚刚编写了该代码并花费了最后一个$句号的您来说,不言自明的可能对五年后必须维护或编辑它的您而言不言自明,更不用说所有可能不是您的人可能不得不去看它。“不言自明”是定义的模糊圣杯。
2014年

110

导致代码复杂或混乱的原因有很多。在最常见的原因,最好通过重构代码,使其少混乱,不加入任何形式的意见解决。

但是,在某些情况下,精心选择的评论是最佳选择。

  • 如果算法本身很复杂且令人困惑,则不仅是其实现(这种方法在数学期刊上得到了广泛的记载,后来被称为Mbogo的算法),那么您在实现的一开始就发表了评论,请阅读类似于“这是Mbogo的用于重新修饰小部件的算法,最初在这里描述:[论文的URL]。此实现包含Alice和Carol的改进[另一篇论文的URL]。” 不要试图比这更详细。如果有人需要更多细节,他们可能需要阅读整篇论文。

  • 如果您采取了一些可以用某种特殊记法表示为一两行的东西,并将其扩展为命令性代码的一大类,那么将这些一两行特殊记法放在函数上方的注释中是一种不错的方法告诉读者应该怎么做。这是一个例外“但如果有什么评论失控与代码同步”的说法,因为专业符号可能是容易找到比代码中的bug。(如果您用英语写了规范,则是另一种方式。)一个很好的例子在这里:https : //dxr.mozilla.org/mozilla-central/source/layout/style/nsCSSScanner.cpp#1057 ...

    /**
     * Scan a unicode-range token.  These match the regular expression
     *
     *     u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
     *
     * However, some such tokens are "invalid".  There are three valid forms:
     *
     *     u+[0-9a-f]{x}              1 <= x <= 6
     *     u+[0-9a-f]{x}\?{y}         1 <= x+y <= 6
     *     u+[0-9a-f]{x}-[0-9a-f]{y}  1 <= x <= 6, 1 <= y <= 6
    
  • 如果代码总体上是简单明了的,但是包含一两个看起来过于复杂,不必要或完全错误的东西,但由于某种原因必须采用这种方式,那么您应在看起来可疑的部分上方放置一个注释,其中请说明原因。这是一个简单的示例,其中唯一需要说明的是常量为什么具有特定值。

    /* s1*s2 <= SIZE_MAX if s1 < K and s2 < K, where K = sqrt(SIZE_MAX+1) */
    const size_t MUL_NO_OVERFLOW = ((size_t)1) << (sizeof(size_t) * 4);
    if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
        nmemb > 0 && SIZE_MAX / nmemb < size)
      abort();
    

25
这是太可恶了,4应该是CHAR_BIT / 2;-)
史蒂夫·杰索普

@SteveJessop:有什么能排除实现CHAR_BITS为16且sizeof(size_t)为2,但是size_t的最大值为2 ^ 20 [size_t包含12个填充位]的实现吗?
supercat 2014年

2
@supercat我在C99中看不到任何明显排除它的东西,这意味着该示例技术上是错误的。它恰好取自OpenBSD的(略有修改的版本)reallocarray,并且OpenBSD通常不相信迎合 ABI中未发生的可能性。
zwol 2014年

3
@Zack:如果代码是围绕POSIX假设设计,采用CHAR_BITS可能给人的印象是,代码可能比8其他值正常工作
supercat

2
@Zack:为使精确宽度的无符号类型有用,将需要独立于的大小定义其语义int。就其本身而言,uint32_t x,y,z;的含义(x-y) > z取决于的大小int。此外,一种用于编写健壮代码的语言应允许程序员区分一种类型,即预期计算将超出该类型的范围并应进行静默包装;另一种则应将超出该类型范围的计算捕获,而另一种则需要计算预计不会超出该类型的范围,但是...
supercat

61

那么用注释解释复杂的代码怎么了?

这不是对与错的问题,而是Wikipedia文章中定义的“最佳实践” :

最佳实践是一种方法或技术,该方法或技术始终如一地显示出优于其他方式所达到的结果,并且被用作基准。

因此,最佳实践是首先尝试改进代码,并在不可能的情况下使用英语。

这不是法律,但是找到需要重构的注释代码比需要注释的重构代码更为常见,最佳实践反映了这一点。


42
+1表示“找到需要重构的注释代码比需要注释的重构代码更为常见”
Brandon 2014年

7
好的,但是评论的频率是多少: //This code seriously needs a refactor
Erik Reppen 2014年

2
当然,任何未经严格科学研究支持的所谓最佳实践都只是一种意见。
Blrfl 2015年

54

美好的一天,精心制作,结构良好且可读的代码将无法正常工作。否则它将无法正常工作。否则会出现无法使用且需要调整的特殊情况。

到那时,您将需要做一些可以改变事情的事情,这样它才能正常工作。尤其是在存在性能问题的情况下,而且经常在所使用的库,API,Web服务,Gem或操作系统之一运行不正常的情况下,您最终可能会提出不建议的建议必不可少,但反直觉或不明显。

如果您没有任何注释来解释为什么选择这种方法,那么很有可能将来有人(甚至有人可能是您)查看代码,然后看看如何将其“修复”为更易读,更优雅且无意间撤消了您的修复,因为它看起来不像是修复。

如果每个人都总是编写完美的代码,那么很显然,看起来不完美的代码是在现实世界中一些棘手的干预下工作的,但事实并非如此。大多数程序员经常编写令人困惑或有些混乱的代码,因此当我们遇到这种情况时,很自然地倾向于整理它。每当我阅读自己写的旧代码时,我都发誓过去的自己是一个白痴

因此,我不认为注释是对不良代码的道歉,而只是作为您为什么不做显而易见的事情的解释。拥有后,// The standard approach doesn't work against the 64 bit version of the Frobosticate Library将来的开发人员(包括您将来的自己)可以注意该部分代码,并对该库进行测试。当然,您也可以将注释放入源代码管理的提交中,但是人们只会在出现问题后才查看注释。他们将在更改代码时阅读代码注释。

告诉我们我们应该始终在理论上编写完美代码的人并不总是在现实环境中具有丰富的编程经验的人。有时您需要编写性能达到一定水平的代码,有时需要与不完善的系统进行互操作。这并不意味着您不能以优雅且写得很好的方式执行此操作,但是非显而易见的解决方案需要说明。

当我为业余项目编写代码时,我知道没人会读过,但我仍会注释一些令我感到困惑的部分-例如,任何3D几何都涉及我并不完全熟悉的数学-因为我知道什么时候回来在六个月内,我将完全忘记该怎么做。这不是对错误代码的道歉,而是对个人限制的承认。通过不加注释,我要做的就是在将来为自己创造更多的工作。如果我现在能避免的话,我不希望我的未来自我不必要地重新学习一些东西。那可能有什么价值?


5
@Christian是吗?当然,第一行引用了该语句,但据我所知,它稍宽一些。
glenatron 2014年

9
“每当我阅读以前编写的代码时,我都发誓过去的自己是一个白痴。” 在我发展职业生涯的四年中,我发现这种情况发生在每当我看到超过6个月左右的东西时。
2014年

6
在许多情况下,最有用和最有用的历史信息与被考虑但被决定反对的事物有关。在许多情况下,某人选择X来做某事,而另一些Y似乎更好。在某些情况下,Y将“几乎”比X更好地工作,但结果却存在一些无法解决的问题。如果y为避免因这些问题,这些知识可以帮助防止其他人浪费他们对不成功的尝试时间来实施办法Y.
supercat

4
在日常工作中,我也经常使用进度注释,但长期来看,这些注释就不会出现,但是放入TODO注释或一小段以提醒我下一步要做什么可能会很有用早上提醒。
glenatron 2014年

1
@Lilienthal,我认为最后一段不限于个人项目,他说:“ ...我仍然对我感到困惑的部分发表评论。”
2015年

29

注释的需要与代码的抽象级别成反比。

例如,对于大多数实际目的,汇编语言是不带注释的,难以理解。这是一个小程序的摘录,该程序计算并打印了斐波那契数列的各项

main:   
; initializes the two numbers and the counter.  Note that this assumes
; that the counter and num1 and num2 areas are contiguous!
;
    mov ax,'00'                     ; initialize to all ASCII zeroes
    mov di,counter                  ; including the counter
    mov cx,digits+cntDigits/2       ; two bytes at a time
    cld                             ; initialize from low to high memory
    rep stosw                       ; write the data
    inc ax                          ; make sure ASCII zero is in al
    mov [num1 + digits - 1],al      ; last digit is one
    mov [num2 + digits - 1],al      ; 
    mov [counter + cntDigits - 1],al

    jmp .bottom         ; done with initialization, so begin

.top
    ; add num1 to num2
    mov di,num1+digits-1
    mov si,num2+digits-1
    mov cx,digits       ; 
    call    AddNumbers  ; num2 += num1
    mov bp,num2         ;
    call    PrintLine   ;
    dec dword [term]    ; decrement loop counter
    jz  .done           ;

    ; add num2 to num1
    mov di,num2+digits-1
    mov si,num1+digits-1
    mov cx,digits       ;
    call    AddNumbers  ; num1 += num2
.bottom
    mov bp,num1         ;
    call    PrintLine   ;
    dec dword [term]    ; decrement loop counter
    jnz .top            ;
.done
    call    CRLF        ; finish off with CRLF
    mov ax,4c00h        ; terminate
    int 21h             ;

即使有评论,也很复杂。

现代示例:正则表达式通常是非常低的抽象结构(小写字母,数字0、1、2,换行等)。他们可能需要样本形式的评论(IIRC的鲍勃·马丁(Bob Martin)确实承认这一点)。这是一个正则表达式,(我认为)应与HTTP(S)和FTP URL匹配:

^(((ht|f)tp(s?))\://)?(www.|[a-zA-Z].)[a-zA-Z0-9\-\.]+\.(com|edu|gov|m
+il|net|org|biz|info|name|museum|us|ca|uk)(\:[0-9]+)*(/($|[a-zA-Z0-9\.
+\,\;\?\'\\\+&amp;%\$#\=~_\-]+))*$

随着语言逐步发展到抽象层次结构,程序员能够使用令人回味的抽象(变量名,函数名,类名,模块名,接口,回调等)来提供内置文档。忽略利用此优势,并在其上使用注释在纸上是很懒惰的,这对维护者是不利的,也是不尊重的。

我想到的是在C数字食谱翻译逐字大多以数字食谱在C ++中,我推断开始为数字食谱(在FORTAN),所有的变量aaabccc,等通过每个版本维护。该算法可能是正确的,但是它们没有利用所提供语言的抽象性。他们让我失望。Dobbs博士的文章样本-快速傅立叶变换

void four1(double* data, unsigned long nn)
{
    unsigned long n, mmax, m, j, istep, i;
    double wtemp, wr, wpr, wpi, wi, theta;
    double tempr, tempi;

    // reverse-binary reindexing
    n = nn<<1;
    j=1;
    for (i=1; i<n; i+=2) {
        if (j>i) {
            swap(data[j-1], data[i-1]);
            swap(data[j], data[i]);
        }
        m = nn;
        while (m>=2 && j>m) {
            j -= m;
            m >>= 1;
        }
        j += m;
    };

    // here begins the Danielson-Lanczos section
    mmax=2;
    while (n>mmax) {
        istep = mmax<<1;
        theta = -(2*M_PI/mmax);
        wtemp = sin(0.5*theta);
        wpr = -2.0*wtemp*wtemp;
        wpi = sin(theta);
        wr = 1.0;
        wi = 0.0;
        for (m=1; m < mmax; m += 2) {
            for (i=m; i <= n; i += istep) {
                j=i+mmax;
                tempr = wr*data[j-1] - wi*data[j];
                tempi = wr * data[j] + wi*data[j-1];

                data[j-1] = data[i-1] - tempr;
                data[j] = data[i] - tempi;
                data[i-1] += tempr;
                data[i] += tempi;
            }
            wtemp=wr;
            wr += wr*wpr - wi*wpi;
            wi += wi*wpr + wtemp*wpi;
        }
        mmax=istep;
    }
}

作为抽象的特殊情况,每种语言都有用于某些常见任务的惯用语/规范代码片段(在C中删除动态链接列表),无论它们看起来如何,都不应记录在案。程序员应该学习这些习语,因为它们是语言的非正式部分。

因此,要解决的问题是:必须避免使用从低级构建块构建的非惯用代码进行注释。而且这比实际情况要少WAAAAY。


1
真的没有人应该用汇编语言来写这样的一行:dec dword [term] ; decrement loop counter。另一方面,您的汇编语言示例所缺少的是在每个“代码段”之前加一个注释,以解释下一个代码块的作用。在这种情况下,注释通常等效于伪代码中的一行,例如;clear the screen,其后是清除屏幕实际需要的7行。
Scott Whitlock 2014年

1
是的,在装配体示例中我会考虑一些不必要的注释,但公平地说,它很能代表“良好”的装配体风格。即使只有一两行的段落作为序幕,代码也仍然很难遵循。我比FFT示例更了解ASM示例。我在研究生院用C ++编写了一个FFT,它看起来并不像这样,但是后来我们使用了STL,迭代器,函子等许多方法调用。不像单片函数那样快,但是更容易阅读。我将尝试添加它以与NRinC ++示例形成对比。
克里斯蒂安H

你是说^(((ht|f)tps?)\:\/\/)?(www\.)*[a-zA-Z0-9\-\.]+\.(com|edu|gov|mil|net|org|biz|info|name|museum|us|ca|uk)(\:[0-9]+)*(\/($|[a-zA-Z0-9\.\,\;\?\'\\\+&%\$#\=~_\-]+))*$吗 注意数字地址。
izabera 2014年

我的观点或多或少是这样的:有些东西是从非常低的抽象水平构建的,不容易阅读或验证。注释(并且不要太离题,TESTS)可能是有用的,而不是有害的。同时,不使用可用的更高级别的抽象(:alpha::num:可用),即使使用良好的注释,也比使用更高级别的抽象更难理解。
克里斯蒂安

3
+1:"The need for comments is inversely proportional to the abstraction level of the code." 几乎可以总结所有的信息。
Gerrat 2014年

21

我不认为代码中的注释有什么问题。在我看来,注释在某种程度上是不好的,这是由于某些程序员将事情做得太过分了。这个行业有很多潮流,特别是对于极端观点。在此过程中的某个地方,注释的代码等同于错误的代码,我不确定为什么。

注释确实存在问题-您需要在更新它们所引用的代码时使它们保持更新,这种情况很少发生。Wiki或其他内容是更详尽的代码参考文档。您的代码应可读,无需注释。应该在版本控制或修订说明中描述所做的代码更改。

但是,以上所有方法均不能使注释的使用无效。我们没有生活在理想的世界中,因此,无论出于何种原因上述任何一项都不可行时,我宁愿留下一些评论。


18

我认为您对他的话读得太多了。您的投诉分为两个不同部分:

解释(1)复杂算法或(2)冗长而费解的带有描述性注释的代码有什么问题?

(1)是不可避免的。我认为马丁不会不同意你的看法。如果您正在编写类似快速反平方根之类的内容,则将需要一些注释,即使这仅仅是“邪恶的浮点位级黑客攻击”。除非使用DFS或二进制搜索之类的简单方法,否则阅读您的代码的人不太可能具有使用该算法的经验,因此,我认为注释中应至少提及其含义。

但是,大多数代码不是(1)。您很少会编写一款软件,除了手动滚动互斥实现,模糊的线性代数运算(缺乏库支持)以及新颖的算法(只有您公司的研究小组知道)之外,什么都不是。大多数代码由库/框架/ API调用,IO,样板和单元测试组成。

这就是马丁在谈论的那种代码。他用本章开头的Kernighan和Plaugher的语录回答了您的问题:

不要注释错误的代码-重写它。

如果您的代码中包含冗长而复杂的部分,则您可能无法保持代码干净。解决此问题的最佳方法不是在文件顶部写一段长段落的注释,以帮助将来的开发人员弄清楚它的位置。最好的解决方案是重写它。

这正是马丁所说的:

注释的正确使用是为了弥补我们无法用代码表达自己的意见注释始终是失败。我们必须拥有它们,因为我们不能总是想出没有它们的表达方式,但是它们的使用并不是值得庆祝的原因。

这是您的(2)。Martin同意,冗长而复杂的代码确实需要注释-但他将代码的责任归咎于编写该代码的程序员,而不是一个含糊不清的想法,即“我们都知道那还不够”。他认为:

清晰,表达力强,注释少的代码远胜于混乱且复杂,注释很多的代码。与其花费时间来编写解释已造成的混乱的评论,不如将其用于清理混乱。


3
如果与我一起工作的开发人员只是写了“邪恶的浮点位级黑客”来解释快速的平方根算法-他们会与我交谈。只要他们提及更有用的地方,我都会很高兴。
2014年

8
我以一种方式不同意-解释坏事是如何工作的评论要快得多。给定一些可能不会再被触及的代码(我猜是大多数代码),那么注释是比大重构更好的业务解决方案,它通常会引入bug(因为杀死依赖于bug的修复程序仍然是bug)。我们无法获得可以完全理解的代码的完美世界。
gbjbaanb 2014年

2
@trysis哈哈,是的,但是在一个程序员负责而不是商人的世界里,他们永远也不会出货,因为他们永远在不断地重构不断完善的代码库上镀金,以求完美。
gbjbaanb 2014年

4
@PatrickCollins几乎我在网络上阅读的所有内容都是关于第一次正确完成操作。几乎没有人想写有关纠正混乱的文章!物理学家说“给了一个完美的领域……” Comp。科学家说“给了一个未开发的领域……”
gbjbaanb

2
最好的解决方案是在无限的时间内重写它。但考虑到其他人的代码库,典型的公司截止日期和实际情况;有时最好的办法是发表评论,添加一个TODO:重构并将其重构到下一个版本中;该修复程序需要在昨天完成。有关重构的所有理想主义讨论的全部内容是,它并不能说明事情在工作场所中的实际工作方式;有时会有更高的优先级和足够快的截止日期,这将抢先解决遗留劣质代码。就是这样。
hsanders 2014年

8

解释复杂的算法或带有描述性注释的冗长而复杂的代码段有什么问题?

没什么。记录您的工作是一个好习惯。

就是说,您在这里有一个错误的二分法:编写干净的代码与编写记录的代码-两者并不矛盾。

您应该关注的是将复杂的代码简化和抽象为更简单的代码,而不是认为“只要有注释就可以使用复杂的代码”。

理想情况下,您的代码应该简单记录在案。

这样,无需其他开发人员(包括您自己)逐行阅读整个算法来弄清楚算法的作用,他们只需阅读您用普通英语编写的友好描述性注释即可。

真正。这就是为什么在文档中应解释所有公共API算法的原因。

因此,在编写了用部分人类可读的编程语言编写的复杂代码之后,为什么不添加友好而易懂的英语的描述性简明注释来解释代码的操作呢?

理想情况下,编写一段复杂的代码后,您应该(而不是详尽的清单):

  • 将其视为草稿(即计划对其进行重写)
  • 形式化算法入口点/接口/角色/等(分析和优化接口,形式化抽象,文档前提条件,后置条件和副作用以及文档错误情况)。
  • 编写测试
  • 清理和重构

这些步骤都不是微不足道的操作(即每个步骤都可能需要几个小时),并且执行这些操作的回报不是立即的。因此,这些步骤(几乎)总是被折衷(通过开发人员偷工减料,经理偷工减料,截止日期,市场限制/其他现实情况,缺乏经验等)。

某些算法很复杂。因此在逐行阅读它们时很难理解。

您永远不必依靠阅读实现来弄清楚API的功能。执行此操作时,您将基于实现(而不是接口)来实现客户端代码,这意味着模块耦合陷入地狱,您可能会在编写的每行新代码中引入未记录的依赖关系。已经增加了技术债务。

用一些关于它的一般操作的注释来解释一个复杂的算法真的很糟糕吗?

不-那很好。但是,仅添加几行注释是不够的。

用注释解释复杂的代码有什么问题?

如果可以避免的话,您不应使用复杂的代码。

为了避免复杂的代码,请对接口进行形式化,在API设计上花费的钱要比在实现上花费的钱多8倍(Stepanov建议与实现相比,至少要花10倍的钱在接口上),然后在开发项目时应了解您正在创建一个项目,而不仅仅是编写一些算法。

一个项目涉及API文档,功能文档,代码/质量度量,项目管理等。这些过程都不是一次性完成的快速步骤(所有过程都需要时间,需要深思熟虑和计划,并且都需要您定期回到它们并用细节进行修改/完善)。


3
“您永远不必依靠阅读实现来弄清楚API的功能。” 有时,您承诺使用的上游会对您造成影响。我有一个特别令人不满意的项目,上面写着“以下丑陋的希思·罗宾逊代码存在,因为尽管供应商声称,simpleAPI()在此硬件上无法正常工作”,但注释不一。
pjc50 2014年

6

无需其他开发人员(包括您自己)逐行阅读整个算法来弄清楚它的作用,他们只需阅读您用普通英语编写的友好描述性注释即可。

我认为这是对“评论”的轻微滥用。如果程序员想读取某些内容而不是整个算法,那么功能文档就是针对此内容。好的,因此功能文档实际上可能出现在源代码中的注释中(也许是通过doc工具提取的),但是就语法而言,尽管就语法而言,它是注释,但对于您的编译器,您应该将它们视为具有不同目的的独立事物。我认为“评论应少”不一定意味着“文件应少”,甚至“版权声明不多”!

该函数中的注释以及代码供他人阅读。因此,如果您的代码中有几行难以理解,并且无法使其易于理解,那么注释对于读者用作这些行的占位符很有用。当读者试图获得一般要旨时,这可能非常有用,但是存在两个问题:

  • 注释不一定是正确的,而代码可以完成它的工作。因此,读者对此表示赞同,这并不理想。
  • 读者还不了解代码本身,因此,直到以后再回来阅读它们时,他们仍然没有资格修改或重新使用它。在这种情况下,他们在阅读什么?

有例外,但是大多数读者将需要了解代码本身。应该写评论来帮助而不是取代它,这就是为什么通常建议您评论应说“为什么这么做”的原因。知道接下来几行代码的动机的读者更有机会了解他们的工作和方式。


5
一个有用的注释地方:在科学代码中,您通常可以进行非常复杂的计算,其中涉及许多变量。出于程序员的理智,将变量名的名称保持简短是有意义的,因此您可以查看数学,而不是名称。但这确实使读者难以理解。因此,简短地描述正在发生的事情(或者更好的是,对期刊文章或类似文章中的方程式进行引用)可能会很有帮助。
naught101 '09

1
@ naught101:是的,尤其是因为您所指的论文也可能使用了单字母变量名。通常,如果使用相同的名称,通常会发现代码确实遵循本文,但这与代码的目标不言自明(而是由论文进行解释)相矛盾。在这种情况下,定义了每个名称的注释(说出其实际含义)将代替有意义的名称。
史蒂夫·杰索普

1
当我在搜索代码中的特定内容时(该特定情况在哪里处理?),我不想阅读和理解代码段,只是发现它毕竟不是地方。我需要在一行中总结下一段所做的评论。这样,我将快速找到与问题相关的代码部分,并跳过不感兴趣的细节。
Florian F

1
@FlorianF:传统的回答是,变量和函数名称应大致指示代码的含义,因此让您略过肯定与您所寻找内容无关的内容。我同意您的观点,这种方法并不总是能够成功,但是我不同意如此,以至于我认为需要注释所有代码以帮助搜索或略读。但是您是对的,在这种情况下,有人正在阅读您的代码(某种程度上)并且合法地不需要理解它。
史蒂夫·杰索普

2
@Snowman People可以使用变量名来实现。我已经看到了其中变量listOfApples包含香蕉列表的代码。有人复制了处理苹果列表的代码,并将其改编为香蕉,而无需更改变量名称。
Florian F

5

通常,我们必须做复杂的事情。将它们记录下来以供将来理解当然是正确的。有时,此文档的正确位置是代码,可以在其中使文档与代码保持最新。但是绝对值得考虑单独的文档。这也可以更容易地呈现给其他人,包括图表,彩色图片等。那么评论就是:

// This code implements the algorithm described in requirements document 239.

甚至只是

void doPRD239Algorithm() { ...

当然,人们很高兴能与命名的功能MatchStringKnuthMorrisPrattencryptAESpartitionBSP。更晦涩的名字值得在评论中解释。您还可以添加书目数据和指向从中实现算法的论文的链接。

如果算法复杂,新颖且不明显,那么即使仅用于公司内部流通,也绝对值得一份文档。如果您担心文档丢失,请将其签入源代码管理。

还有另一类代码,它不是算法而是官僚。您需要为另一个系统设置参数,或与其他人的错误进行互操作:

/* Configure the beam controller and turn on the laser.
The sequence is timing-critical and this code must run with interrupts disabled.
Note that the constant 0xef45ab87 differs from the vendor documentation; the vendor
is wrong in this case.
Some of these operations write the same value multiple times. Do not attempt
to optimise this code by removing seemingly redundant operations.
*/

2
我反对在函数/方法的内部算法之后命名,在大多数情况下,所使用的方法应该是内部关注的问题,无论如何都应将所使用的方法记录在函数的顶部,但不要称之为doPRD239Algorithm告诉我在无需查找算法的情况下,该功能一无所获,其原因MatchStringKnuthMorrisPrattencryptAES工作是从对功能的描述开始,然后对方法进行描述。
scragar's

5

我忘了,我读它,但有应该出现在你的代码是什么,应该出现什么样的评论之间的锐利和清晰的线条。

我相信您应该评论自己的意图,而不是算法。即评论你的意思,而不是评论你的意思

例如:

// The getter.
public <V> V get(final K key, Class<V> type) {
  // Has it run yet?
  Future<Object> f = multitons.get(key);
  if (f == null) {
    // No! Make the task that runs it.
    FutureTask<Object> ft = new FutureTask<Object>(
            new Callable() {

              public Object call() throws Exception {
                // Only do the create when called to do so.
                return key.create();
              }

            });
    // Only put if not there.
    f = multitons.putIfAbsent(key, ft);
    if (f == null) {
      // We replaced null so we successfully put. We were first!
      f = ft;
      // Initiate the task.
      ft.run();
    }
  }
  try {
    /**
     * If code gets here and hangs due to f.status = 0 (FutureTask.NEW)
     * then you are trying to get from your Multiton in your creator.
     *
     * Cannot check for that without unnecessarily complex code.
     *
     * Perhaps could use get with timeout.
     */
    // Cast here to force the right type.
    return (V) f.get();
  } catch (Exception ex) {
    // Hide exceptions without discarding them.
    throw Throwables.asRuntimeException(ex);
  }
}

这里没有尝试说明每个步骤执行的操作,它只说明应该执行的操作。

PS:我找到了我所指的来源- 编码恐怖:代码告诉你如何,评论告诉你为什么


8
第一条评论:它运行了吗?运行了吗?其他评论也一样。对于不知道代码做什么的人来说,这是没有用的。
gnasher729 2014年

1
@ gnasher729-从上下文中删除几乎所有注释都将是无用的-此代码演示了添加注释的意图和意图,而不是试图描述。对不起,它对您没有任何帮助。
OldCurmudgeon 2014年

2
该代码的维护者将没有上下文。弄清楚代码的功能并不是特别困难,但是注释没有帮助。如果您写评论,请花些时间并集中精力写评论。
gnasher729 2014年

顺便说一句-“ 是否运行”注释指,Future并且指示get()进行检查后null检测是否Future已经运行-正确地记录了意图而不是过程
OldCurmudgeon 2014年

1
@OldCurmudgeon:您的答复与我的想法非常接近,我将仅添加此评论作为您的观点的一个例子。尽管不需要注释来解释干净的代码,但是注释却可以很好地解释为什么编码要单单完成。以我有限的经验,注释通常对于解释代码所处理的数据集的特性或代码要执行的业务规则很有用。如果该错误是由于关于数据的假设错误而发生的,则添加注释代码以修复错误的示例就是一个很好的例子。
兰德尔·斯图尔特

4

但是我们都知道这还不够。

真?从何时起?

在大多数情况下,设计良好并带有好名字的代码绰绰有余。反对使用注释的论点是众所周知的,并已记录在案(如您所指)。

但这是准则(与其他准则一样)。在极少数情况下(以我的经验,大约每2年一次),如果将其重构为较小的清晰功能(由于性能或内聚需求),情况会变得更糟,然后继续进行下去-进行冗长的评论,以说明事物的实质这样做(以及为什么您违反最佳做法)。


7
我知道这还不够。
Florian F

2
从何时起?显然,您已经知道答案了。“在大多数情况下,具有良好名称的精心设计的代码绰绰有余。” 因此,在少数情况下可能还不够,这正是问问者所要求的。
Ellesedil 2014年

3
我曾经试图破译其他人的密码,我希望每两年添加一次以上的评论。
Ogre Psalm33'9

@ OgrePsalm33-他们有小的方法并且使用好名字吗?不管注释如何,错误的代码都是不好的。
Telastyn 2014年

2
@Telastyn不幸的是,在大型代码库上工作时,“小的”方法和“好的”名称对于每个开发人员都是主观的(因此,这是一个很好的注释)。开发人员编写Flarbigan图形处理算法代码已有7年的时间,他和其他类似的开发人员都可以编写出清晰明了的内容,但是对于过去四年来开发Perbian网格基础结构代码的新手来说却是个谜。然后,两周后,Flarbigan专家退出了。
Ogre Psalm33'9

2

代码的主要目的是命令计算机执行某项操作,因此,好的注释永远不能替代好的代码,因为注释无法执行。

也就是说,源代码中的注释是其他程序员(包括您自己)的一种文档形式。如果注释所涉及的抽象问题超出了代码在每个步骤中所做的工作,那么您做的要好于平均水平。该抽象级别随所使用的工具而异。汇编语言例程附带的注释通常比“ APL”具有较低的“抽象”级别A←0⋄A⊣{2⊤⍵:1+3×⍵⋄⍵÷2}⍣{⍺=A+←1}⎕。我认为这可能值得对要解决的问题进行评论,嗯?


2

如果该代码是微不足道的,则不需要解释性注释。如果代码不平凡,则解释性注释很可能也是不平凡的。

现在,非平凡的自然语言的麻烦在于,我们中的许多人都不善于阅读或编写它。我相信您的书面交流能力非常出色,但是对书面语言了解较少的人可能会误解您的语言。

如果您尽力编写不会被误解的自然语言,那么您最终会得到诸如法律文件之类的东西(众所周知,这些东西比代码更冗长,更难以理解)。

代码应该是您逻辑的最简洁的描述,关于代码的含义应该没有太多争论,因为您的编译器和平台拥有最终决定权。

就我个人而言,我不会说你永远不要写评论。只有这样,您才应该考虑为什么代码需要注释,以及如何解决该注释。这似乎是这里答案的共同主题。


当我不同意“一个人可以更快地理解英语,而他/她可以理解具有相同含义的代码(只要操作不琐碎)”时,我的想法正是总是不那么模棱两可,更简洁。
stephenbayer 2014年

0

还没有提到的一点是,有时在某些语言将特定语法用于多种目的的情况下,准确地注释一段代码是有帮助的。例如,假设所有变量均为类型float,请考虑:

f1 = (float)(f2+f3); // Force result to be rounded to single precision
f4 = f1-f2;

显式强制转换为floatto的作用float是强制将结果四舍五入到单精度。因此,可以将注释视为只是说出代码的作用。另一方面,将该代码与:

thing.someFloatProperty = (float)(f2*0.1); // Divide by ten

在这里,强制转换的目的是防止编译器以最有效的方式进行精确计算(f2 / 10)[比乘以0.1f更精确,并且在大多数机器上,比除以10.0f更快速]。

如果没有评论,则正在审查以前的代码的人可能会认为错误地添加了强制类型转换,认为该类型是防止编译器发出嘎嘎声的必要,并且不需要。实际上,强制转换的目的是严格按照语言规范的说明进行操作:强制将计算结果四舍五入为单精度,即使是在四舍五入比将结果保持更高的精度更昂贵的机器上也是如此。鉴于强制转换float可以具有多种不同的含义和目的,使用注释指定在特定情况下要使用的含义可以帮助弄清楚实际含义与意图是一致的。


我不确定J.Random Programmer在第二个示例中会意识到,将常量写入0.1是有充分的理由的,而不是因为原始程序员忘记键入'f'。
David K

尤其是在调试过程中,您永远不会以为有充分的理由就已经做了任何事情。
gnasher729 2014年

@DavidK:我的第二个示例代码的目的是将其与第一部分代码进行对比。在第二段代码中,程序员的意图可能是拥有someFloatProperty最准确的表示形式f2/10因此,第二种转换的主要目的仅仅是使代码编译。但是,在第一个示例中,由于操作数已经是,它的正常用途(将一个编译时类型更改为另一种)显然不需要进行强制转换float。该注释可以清楚地表明演员表次要目的(四舍五入)。
超级猫2014年

我同意以下观点,即您无需(float)在第二个示例中对演员表进行任何评论。问题是关于字面常量0.1。您(在文本的下一段中)解释了为什么我们要这样写0.1:“它比乘以0.1f更准确。” 我建议这些是应在评论中使用的词。
David K

@DavidK:如果我知道0.1f的不精确度是不可接受的,那么我肯定会添加评论,如果我知道精度损失是可以接受的并且0.1f实际上比0.1快的话,我会使用0.1f 。如果我不知道这两个事实中的哪一个是正确的,那么我的编码习惯将是double用于常量或中间计算,其值可能无法表示为float[尽管在需要烦人的显式double-to-float强制转换,懒惰的语言中可能会推动使用float常量而不是为了速度,而是为了减少烦恼]。
supercat

-1

解释代码作用的注释是一种重复形式。如果您更改代码,然后忘记更新注释,则可能引起混乱。我并不是说不要使用它们,只是明智地使用它们。我赞成Bob叔叔的格言:“仅评论代码不能说的内容”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.