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

我的问题如下:

解释有什么问题

这样,不需要其他开发人员(包括您自己)一行一行地阅读整个算法来弄清楚它的作用,而无需编写说明的注释? ,他们就可以阅读您用普通英语撰写的友好描述性评论。

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

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

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

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

解释一个复杂的算法,并对其通用操作加几行注释,真的很不好吗?用注释解释复杂的代码有什么问题?

评论

如果那是令人费解的,请尝试将其重构为较小的碎片。

从理论上讲,理论与实践之间没有区别。实际上,有。

@mattnz:更直接地,在编写注释时,您会沉迷于此代码解决的问题。下次访问时,您遇到的问题将更少。

函数或方法的“作用”从其名称应显而易见。它的执行方式从其代码中显而易见。为什么要这样做,使用了哪些隐含假设,为了理解算法需要阅读哪些论文,等等-应该在注释中。

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

#1 楼

用外行的话来说:评论本身没有错。错误的是编写需要这些注释的代码,或者假设只要您以通俗易懂的英语来解释它,就可以编写混乱的代码。
注释在更改代码时不会自动更新。这就是为什么注释经常与代码不同步的原因。注释不会使代码更易于测试。
道歉还不错。您所做的工作需要道歉(编写不易理解的代码)是很糟糕的。
能够编写简单代码来解决复杂问题的程序员要比编写复杂代码然后编写代码的程序员更好。长注释解释他的代码的作用。

底线:

说明自己是好的,不需要这样做会更好。

评论


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

–aecolley
2014年9月1日下午1:27

@aecolley从头开始编写自解释代码会更好。

–图兰斯·科尔多瓦(TulainsCórdova)
2014年9月1日,凌晨1:30

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

–斯科特·利德利(Scott Leadley)
2014年9月1日在2:16



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

–史蒂夫·杰索普(Steve Jessop)
2014年9月1日9:17



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

– Shadur
2014年9月1日上午9:38

#2 楼

有很多原因使代码变得复杂或混乱。可以通过重构代码以减少混乱,而不是添加任何注释来解决最常见的原因。
但是,在某些情况下,选择正确的注释是最佳选择。 br />

如果算法本身很复杂且令人困惑,那么不仅是其实现(这种方法已在数学期刊上写成,后来被称为Mbogo的算法),然后您把在实现的开始时发表评论,内容类似于“这是Mbogo的用于重新装饰小部件的算法,最初在此处描述:[论文的URL]。此实现包含Alice和Carol的改进(另一篇论文的URL)。”不要试图比这更详细。如果有人需要更多详细信息,则可能需要阅读整篇论文。

如果您采取了一些可以用一种特殊的表示法写成一两行的东西,并将其扩展为命令式的大范围代码,将一两行专用符号放在函数上方的注释中,是一种告诉读者应该做什么的好方法。这是“但是如果注释与代码不同步该怎么办”参数的一个例外,因为专用符号可能比代码更容易发现错误。 (如果您用英语写了规范,这是另一种方法。)一个很好的示例在这里: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();




评论


太可惜了,4应该是CHAR_BIT / 2 ;-)

–史蒂夫·杰索普(Steve Jessop)
2014年9月1日上午9:06

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

–超级猫
2014年9月2日下午13:29

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

– zwol
2014年9月2日下午13:37

@Zack:如果代码是根据POSIX假设设计的,则使用CHAR_BITS可能会给人以为该代码可以使用8以外的值。

–超级猫
2014年9月2日13:45在

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

–超级猫
2014年9月3日于16:04

#3 楼


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


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


最佳实践是一种方法或技术,该方法或技术始终显示出
优于其他方法获得的结果,并被用作基准。
br />

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

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

评论


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

–布兰登
2014年9月1日,下午1:42

好的,但是该注释的频率是://此代码严重需要重构

–埃里克·雷彭(Erik Reppen)
2014年9月5日上午11:21

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


2015年2月7日在12:57



#4 楼

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

此时,您将需要进行一些更改,以使其正常工作。尤其是在存在性能问题的情况下,而且经常在所使用的库,API,Web服务,Gem或操作系统之一运行不正常的情况下,您最终可能会提出不建议的建议

如果您没有评论来解释为什么选择这种方法,那么将来某人很有可能(以及有人甚至可能是您)会看一下代码,看看如何将其“修复”为更具可读性和优雅的内容,并且无意中撤消了您的修复,因为它看起来不像是修复。

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

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

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

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

评论


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

–旋转加速器
2014年9月1日下午13:11

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

–肯
2014年9月4日在18:18

在许多情况下,最有用和最有用的历史信息与被考虑但被决定反对的事物有关。在许多情况下,某人选择X来做某事,而其他Y似乎更好。在某些情况下,Y将“几乎”比X更好地工作,但结果却存在一些无法解决的问题。如果由于这些问题而避免使用Y,则此类知识可以帮助防止其他人将时间浪费在实施方法Y的失败尝试上。

–超级猫
2014年9月4日19:39

在日常工作中,我也经常使用在进行中的注释-从长远来看它们并不存在,但是放入TODO注释或一小段以提醒我下一步要做什么可能是有用的提醒早晨。

–旋转加速器
2014年9月5日在9:08

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

–通配符
15年11月30日在18:38

#5 楼

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

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

 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。

评论


真的没有人应该用汇编语言来写这样的一行:dec dword [term];递减循环计数器。另一方面,您的汇编语言示例缺少的是在每个“代码段”之前加一个注释,以解释下一个代码块的作用。在这种情况下,注释通常等效于伪代码中的一行,例如;清除屏幕,然后是清除屏幕实际需要的7行。

–斯科特·惠特洛克
2014年9月2日下午16:57

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

– Kristian H
2014年9月2日于17:09

您是说^((((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年9月4日于13:51

或多或少,我的意思是:有些东西是基于非常低级的抽象构建的,不容易阅读或验证。注释(并且不要太离题,TESTS)可能是有用的,而不是有害的。同时,不使用可用的高层抽象(:alpha :: num:如果可用),即使使用良好的注释,也比使用高层抽象更难理解。

– Kristian H
2014年9月4日15:18

+1:“注释的需要与代码的抽象级别成反比。”几乎总结了所有内容。

– Gerrat
2014年9月4日在21:16



#6 楼

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

注释确实存在问题-您需要在更新它们所引用的代码时使它们保持更新,这很可能会发生太不频繁了。 Wiki或其他内容是更详尽的有关您的代码文档的资源。您的代码应可读,无需注释。应该在版本控制或修订说明中描述所做的代码更改。

以上内容均不能使注释的使用无效。我们没有生活在理想的世界中,因此,如果以上任何一项由于任何原因而失败,我都希望能提出一些意见。

#7 楼

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


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

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

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

这是Martin所说的那种代码。他用本章开头的Kernighan和Plaugher的语录解决了您的问题:


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


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

这正是马丁所说的:


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

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



清晰,表现力强的代码很少带有注释,而凌乱而复杂的代码却带有大量注释。与其花时间写评论来解释你的烂摊子,不如花时间清理那堆烂摊子。


评论


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

–迈克尔·安德森(Michael Anderson)
2014年9月1日,下午3:56

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

– gbjbaanb
2014年9月1日下午7:42

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

– gbjbaanb
2014年9月1日14:26在

@PatrickCollins几乎我在网络上阅读的所有内容都是关于第一次正确执行。几乎没有人想写有关解决混乱问题的文章!物理学家说“给了一个完美的领域...”科学家说“给了一个绿地发展...”

– gbjbaanb
2014年9月2日在7:26

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

– hsanders
2014年9月4日在18:17

#8 楼


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


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

那是您在这里有一个错误的二分法:编写简洁的代码与编写记录的代码-两者没有对立。

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

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

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


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

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


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


考虑一个草案(即计划对其进行重写)
使算法入口点/接口/角色/等正式化(分析和优化接口,形式化抽象,文档前提条件,后置条件和约束效果和文档错误案例)。
编写测试
清理和重构

这些步骤都不是一件容易的事(即每个步骤可能要花费几个小时),这样做的好处是他们不是立即的。因此,这些步骤(几乎)总是会受到损害(通过开发人员偷工减料,经理偷工减料,截止日期,市场限制/其他现实情况,缺乏经验等)。


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


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


用一个简单的注释来解释一个复杂的算法真的很不好吗?


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


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


您不应该这样做的事实

要避免复杂的代码,请对接口进行形式化,在API设计上的投入要比在实现上花费的〜多八倍(Stepanov建议至少花费10倍,在界面上(与实现相比),然后在知道自己正在创建项目的情况下开始开发项目,而不仅仅是编写一些算法。

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

评论


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

– pjc50
2014年9月1日14:56

#9 楼


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


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

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


注释不一定是正确的,而代码却可以做到这一点做。因此,读者正在接受您的要求,这是不理想的。
读者还不了解代码本身,因此,直到以后再使用它,他们仍然没有资格修改或重新编写代码。用它。在这种情况下,他们在阅读它是做什么的?

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

评论


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

–naught101
2014年9月1日8:50

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

–史蒂夫·杰索普(Steve Jessop)
2014年9月1日8:56



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

– Florian F
2014年9月1日上午11:57

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

–史蒂夫·杰索普(Steve Jessop)
2014年9月1日12:01



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

– Florian F
2014年9月1日19:42



#10 楼

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

// 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.
*/


评论


我反对在函数/方法的内部算法之后命名,大多数时候,所使用的方法应该是内部关注的问题,绝不应该用所使用的方法记录函数的顶部,但是不要将其称为doPRD239Algorithm我对函数的任何了解都无需查找算法,MatchStringKnuthMorrisPratt和cryptoAES起作用的原因是,它们首先描述其功能,然后再描述方法。

– scragar
2014年9月2日14:35

#11 楼

我忘记了我在哪里读的书,但是应该在代码中显示的内容和应作为注释显示的内容之间存在清晰清晰的界线。

我相信您应该注释自己的意图,而不是算法。即注释一下您打算做什么,而不是您做什么。例如,

// 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:我找到了我所指的来源-编码恐怖:代码告诉你如何,注释告诉你为什么

评论


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

– gnasher729
2014年9月1日14:48在

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

–OldCurmudgeon
2014年9月1日下午14:55

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

– gnasher729
2014年9月1日在22:51

顺便说一句-“是否运行”注释指的是Future,并指出get()和对null的检查将检测Future是否已经运行-正确地记录了意图而不是过程。

–OldCurmudgeon
2014年9月5日在9:54

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

–语法垃圾
17-2-14在18:37



#12 楼


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


真的吗?从什么时候开始?

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

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

评论


我知道这还不够。

– Florian F
2014年9月1日上午11:47

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

– Ellesedil
2014年9月2日于20:01

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

–食人魔诗篇33
2014年9月4日20:15在

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

– Telastyn
2014年9月4日在20:26

@Telastyn不幸的是,在大型代码库上工作时,“小的”方法和“好的”名称对每个开发人员都是主观的(因此,这是一个很好的注释)。编写Flarbigan图形处理算法代码长达7年的开发人员可以对他和类似的开发人员完全清楚地写出一些东西,但对于过去4年开发Perbian网格基础结构代码的新手来说却是个谜。然后,两周后,Flarbigan专家退出了。

–食人魔诗篇33
2014年9月4日在21:04



#13 楼

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

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

#14 楼

如果代码很简单,则不需要说明性注释。如果代码是不平凡的,那么解释性注释也很可能也是不平凡的。

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

如果您努力地编写不会被误解的自然语言,您最终会失败带有法律文件之类的东西(众所周知,这些东西比代码更冗长,更难理解)。

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

我个人不会说您永远不要写注释。只有这样,您才应该考虑为什么代码需要注释,以及如何解决。这似乎是此处答案的常见主题。

评论


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

– stephenbayer
2014年9月2日在16:49

#15 楼

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

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


float强制转换为float的作用是强制将结果四舍五入到单精度;因此,该注释可以被视为只是说出代码的作用。另一方面,将代码与以下代码进行比较:这里,强制转换的目的是防止编译器以最有效的方式准确计算( f2 / 10)[比乘以0.1f更精确,并且在大多数计算机上,比乘以10.0f更准确]。

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

评论


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

– David K
2014年9月7日15:45

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

– gnasher729
2014年9月8日上午10:16

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

–超级猫
2014年9月8日15:29

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

– David K
2014年9月8日在16:54



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

–超级猫
2014年9月8日于17:07

#16 楼

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