[转载]——To 注释 or not 注释, that is a question

“程序里的注释是多好还是少好”,“一个合格的程序员是否应该多写注释”。我参与到这个话题是因为《优秀的程序 vs. 糟糕的程序》这篇资讯译文。去看了一下原文,作者显然是蹲坑时无聊想出来几句打油诗,说的都是业界早有定论的,虚不拉几的东西,例如什么“优秀的程序容易维护,糟糕的程序很难维护”云云。根本无意在这种打油诗里说些有争议的话题。没想到被翻译发表在iteye里,由于不押韵,看起来倒好像是几条最高指导原则一样。更没想到一句“优秀的程序不需要加以说明;糟糕的程序需要大量注释。”惹起了诸多讨论。令我惊诧的是,还有这么多的程序员对这个我一直以为早有定论的东西持不同意见,聊得不亦乐乎。那与其在别人的地方歪楼,坏了人家打油诗的意境,倒不如在我自己的地盘上写篇东西详细讨论一下。 

PS. 那句的原话是:Good programming is self-explanatory. Bad Programming requires explanation。直译的话应该是:优秀的程序能自我解释,糟糕的程序的需要(额外的)解释(自然包括用大量注释来解释)。恐怕是因为在中文里explanation和interpreting(与“编译”所对应的“解释”)都译成“解释”,在IT文章里怕有歧义,结果把注释(comment)给扯进来了。真是躺枪呀,本来是没它什么事的…… 

先表明一下个人立场,我的观点是,如果真正想项目容易维护,那么在公共API的位置上,要多写文档性的注释(其实应该说是“注释形式的文档”),而在内部实现上,要少写注释,让程序自我表达。而在程序内部,只有以下几种注释是有价值和可维护的: 

1. TODO和FIXME 

2. 在非常规的做法上,或者考虑过多种处理方式而最终选择其中一种的地方,记下为什么不采用其他似乎更好的方式,说明已经考虑过这种方案了。(最典型就是在吞掉异常的地方写清楚为什么要吞掉) 

3. 在被注释掉的代码上说明这段代码的功能,以及为什么要注释掉。(被注释掉的代码没有语法高亮,不方便直接阅读。而且不在测试覆盖) 

简单来说,内部实现上的注释,主要应该记录“没做”和“决定了不去做”这类根本没有具体代码与之相对应的内容。 

除此之外,出现在内部主线实现上的注释都有各种副作用(强调一下,是个人观点),注释得越详细,文采越好,副作用越大,越影响维护。而且,内部注释的绝大部分功能,在当今的技术条件下,有更好的替代方案。与其使用具有副作用的注释,不如转变习惯,改用没有副作用的方式。 

下面展开来聊一聊,在没有特别说明的情况下,“注释”均指内部实现上的注释,不包括在API接口上,类似JavaDoc这种“用注释的格式编写的文档”。 

注释存在的问题 

第一个问题:写注释的机会成本 

做管理的人都清楚,一件事情该不该去做,除了考虑直接的成本和收益,还要考虑机会成本。也就是如果把做这一件事的资源用来做其他事情,收益会不会更大。 

相信你也猜到我想说什么了。Ok,如果你说你把本来用来嘿咻的时间用来写注释了,那请跳过本节。但如果你是在工作时间里埋头写注释,那就应该认真考虑一下这个问题了。 

喜欢写注释的人经常会幻想自己写注释是因为自己比别人更勤奋,更负责,更有团队意识,更有牺牲精神。而别人不写注释是因为他们更懒散,更自私。而实际情况是,他们有空闲写注释,是因为他们把自己的产出量跟新手们看齐后才腾出的工作时间。而这些时间,他们本来可以用来编写新功能,或者把代码重构成自描述的,或者写写真正的文档或者教程。 

退一步来说,就算你能说服管理者把你“写注释”当成预算内的资源消耗(或者那个管理者就是你自己),那内部注释还有另一个机会成本的问题:既然你已经耗用了大量时间去写注释,自然应该把收益最大化,也就是,这段注释的时效应该尽量长(关于注释的时效问题下面会谈到),应该能被更多人看到。那么,这段注释就应该放在接口API上,成为API文档。这样就能一定程度上保证这段注释更容易被重视,被维护,被其他人看到。 

你或许要问,那我的注释就是针对一段内部代码的,放在接口文档上岂不是成了实现细节?这个问题很好解决,把段代码抽取出来,形成一个独立的方法,再给这个方法写API文档。 

那岂不是写注释侵入了我的程序结构吗,我犯不着为每一个写注释的地方都抽取一个独立方法呀。恰恰相反,既然你能为一段内部代码写一段注释来描述其功能。那说明这段代码的相对来说功能是独立的。你不把它抽取出来,才是懒散的表现。 

在这里举一个真实的例子,这是我之前在论坛上和某位朋友讨论(好吧,争论)的时候他贴出来作为论据的。在他看来,他写的这段注释自然堪称经典。而在我看来,也是颇为经典————的反面教材。(为了避免扩大争论,就不给出链接了,怕被断章取义的朋友可以私信找我要链接。如果那位跟我讨论朋友路过看到,也可以要求我贴出原文链接) 

 

先不考虑他写这段注释的时间可以干多少其他活。我个人的看法,这段注释本身的第一个问题就是,写得这么工整,却放在了方法内部,谁能看得到。只有两种人有机会看到这段注释,一是没事干通读代码熟悉系统的新人,但在复杂的代码库里,他刚好跑到这里来看到这段注释的机会有多少?二是排错时跟踪到这里的人,但这时对他真正有意义的信息(可能)是currentRowIndex为什么在这里为null,在原本的设计意图里,它在这里能否允许为null,而不是这种关于使用方法的描述。 

第二个问题:注释的准确性和信任危机 

对于前面一个问题,不少人的回答是,如果我耗费一个小时的时间来写注释,能省下别人数十个小时的时间,何乐而不为? 

首先,要省别人或自己的时间,写注释并不是是唯一的方法,也不是最好的方法(这个后面会谈)。而只不过是最方便,和最没有任何其他附加价值的方法(强调一下,API文档注释不在此列)。没有其他附加价值有什么好处?好处是它最能满足我们具有牺牲精神的浪漫情怀,最能说明我们有多重视程序的可读性。“你看我为了你们这帮小子看代码容易点,白白耗了多少时间来写注释。你们不好好读我的代码对得住我吗?” 

其次,虽然一个拥有详尽,准确注释的代码库显然更容易理解。但前提是,这些注释都是准确的。准确的意思是:注释的内容应该能正确说明出它看上去所要注释的代码某方面的特性 

(“它看上去所要注释的代码”?太绕口了吧。嗯,我就见过在在同一个文件里,应该有两个以上的人修改过,有的注释是写在要注释的代码前面一行的,有的注释是写在要注释的代码后面一行的。虽然它们都在准确地注释它们所要注释的代码,但对任意一种阅读习惯的读者来说,看上去总有一部分注释和代码是不匹配的。) 

注释有可能不准确吗?太有可能了!首先,从写注释的那一刻开始,注释就受到程序员对代码的理解、他的语言文字表达能力,以及他当时的精神状态影响。关精神状态什么事?在疲劳的时候,错别字你总写过吧,把“或”跟“和”搞混总试过吧,写漏“不”字你总试过吧。其次,即使代码在创建的那一刻是准确的,随着时间的推移,它也有很大机会变得不准确,我称这为“注释的时效性”。如果不维护,在发展变化的代码库里,注释会慢慢失效。 

注释失效是很容易发生的,常见的情景有: 

1. 代码被更改了,但没有更改注释。很多人想象不到这有多轻易就能发生:代码太简单,不用看注释就弄懂了,根本就没留意有注释;注释太简单,改完代码就忘了;注释太隆重,不敢改(回想一下前面的那个例子,这是另一个程序员给未来阅读他的类的人的一封信,我改了要不要署名?会不会有法律问题?);注释太长,不想改;心情不好,不改(反正也没人监督)。 

2. 注释连同代码一起被复制然后更改了代码,但没有改注释。觉得自己是核心架构师,写的核心代码万年不变的朋友,你是逃不掉的…… 

3. 注释的上下文被更改了,注释的含义发生了变化。这个比较阴,我记得是遇到过,但一时又想不起来具体场景,想起了再补上。成因是,自然语言的表达是连贯性的,上下文之间有时需要结合来理解才能清晰。有可能在改代码时,虽然程序员很负责,同时修改或删除了代码对应的注释,但却无意间引起了后文意义的改变或者形成歧义。 

现在不妨回头看看上面的例子,如果你没有被一堆的文字吓住,认真去看看注释内容,你会发现,里面提到的变量名称在紧跟着的代码上都找不到!注释中说的是某个Map,下面的代码却是个List。他说“下面这行代码中”,而下面紧跟的那行代码跟他说的东西一点关系都没有。对于这个,我没什么好说的了,只能在这里保证,绝对是原文截图,而不是为了写这篇文章杜撰的。 

好吧,注释失效就失效了,它总算发过光发过热了。失效的注释放在那里,很快就会被人删掉或者改正的。如果你这样想,就大错特错了。除非你的团队里都是初出茅庐的愣头青,否则在团队环境中保住脑袋的首要原则就是:不要随便改动你不知道是干什么的东西,特别是当这个东西跟你手上的任务无关时。而团队环境的第一定律是:如果一个你不知道干什么的东西又看上去没有跟任何其它东西发生直接关联,你永远也弄不清它是干什么的(好吧,也不是不可能,只不过很难)。试想一下,你在排错的过程中会不会留意到一条跟前后左右代码都不相关的注释。如果你不幸认真阅读了它,并且被它误导了,等你认真阅读了代码,你得有多大自信和责任心才敢改动它:首先,在复杂的系统里,你怎么敢肯定你现在的理解就是正确的。其次,你怎么能确定别人看起来时你的说法会比原来的说法更清楚。最重要的是,如果你不改,没人会怪你,如果你把它改对了,没人会激励你(除非你改完后自己到处说,然后别人说声,嗯,不错),如果你把它改错了又误导了其它人,提交历史记录上写的就是你的名字。这个厉害关系有点脑子的人都会算。 

PS. 每次我说这种话,就肯定会有人跳出来说:最恨就是你这种不负责任的人。再讨论几个回合之后,这人就会说出来:我之所以觉得你这种人这么讨厌,是因为我上一个公司/生意/项目就是被你这种人搞砸的!好吧,我的观点是,你之所以搞砸,就是因为你的管理方式脆弱到只要有一个员工不负责,整个公司/生意/项目就会垮掉。你想让它活的长点,至少应该让它能适应10%的员工不如你想象中负责的情况吧。 

总之,跟代码不同,注释没有编译器语法检查,没有单元测试,没有QA把关。你唯一的指望,就是团队里有一个表达能力超强,工作清闲,且不怕得罪人的热血骨干来维护。 

好吧,就让失效注释留在那里。有什么问题?想象一下有A,B,C三个人,A只说真话,B只说假话,C有时说真话,有时说假话,而且你不知道他哪一句是真的,你觉得谁最讨厌?就我来说,最讨厌的是C!A自然是好兄弟,至于B我就当他的话是耳边风,谈不上讨厌。但C,如果你不相信他的话,浪费了额外的时间,结果证明他说的是真的,人人都来说你没事找事,再给你扣个看不起人的帽子。如果你相信他的话,不知什么时候就被阴一下,别人照样说你笨。你去骂他,他给你来一句“我也就是想帮帮你而已”,你就自行判断他这句话的真假吧。一堆夹杂着失效和有效注释的代码,就是这位C先生。别人我不好说,至少我对这种人,只好假定他每一句都是假的,对他说的所有话都不轻信。这就出现了一个很无奈的矛盾:所有与代码对应的注释,我先要理解这段代码,才能确定这段注释是准确的,而这段注释的唯一作用,就是帮助我理解这段代码。 

换句话说,内部注释存在“短板效应”,整个系统里所有注释的可信性受到其中最不可信的那部分注释的限制

你会问,那照这么说,前面所说的几种所谓有价值的注释也有这个问题,API上的注释文档也有这个问题呀。严格来说,是的,但相对来说没那么严重。那几种注释都不对应具体的执行代码,不存在矛盾的问题。而你不阅读他们,就无法开始工作。一旦开始工作,就可以直接把它们删除掉,很容易作为规定来推行。唯一可能出现矛盾的地方,就是有人把缺的东西补进去了,但没有删除注释。这种情况危害较小,因为很少有人不经过项目组统筹规划,看到程序里写着TODO就傻乎乎的DO起来。 

至于API文档,代码行为如果与文档不对应,很快就会有人发现,而且责任很清晰,就是改代码的人的责任,他有压力去主动维护。最重要是,这些API文档会形成即时代码提示,具有不可替代的特殊功能。 

还有人会说,你杞人忧天而已,我可以选择直接相信注释,就算失效,大不了浪费一点时间,只要系统里有效注释足够多,平均下来,注释还是为我节省了大量时间。可惜,事实上,在排错过程中,失效注释带来的损失往往不止一点,要知道,真正的排错并不是找到离故障位置最近的最后一个出错的地方,然后把它调整正确这么简单(这样做往往是通过引入一个新的错误来修正之前的一个错误)。 而是需要推导原本的设计意图,在整条逻辑链最早出现错误的位置进行修正。在推导逻辑链过程中,是一环扣一环的,后面的所有推导,都是基于前面推导所形成的结论。如果前面的结论错了,那么后面的一切推导都是无用功,幸运的话会进入死胡同,倒霉的话就找到一个似是而非的地方,改动之后把当前问题解决了,但对其他分支却引入了新的问题。而且,人的工作情绪是很容易受到返工打击的,以我来说,如果推导一直正确的话,我可以一直排查一整天。但是如果在推导过程中发现前面某一步犯了个低级错误把整个逻辑链推倒重来的话,两个小时就会让我疲惫不堪。 

不妨想象一下,你家电视机不亮了,你去排查,当然首先就是检查唯一的电源插座,然后你看到插座上贴着一张纸:24小时连续不间断无故障供电。于是你就开始拆机壳,把东西拆完楞是没找到问题。直到天黑,你找来一个本来亮着的台灯插在原本电视机的插座上,这时你发现台灯不亮。你现在是什么心情。别忘了,你的任务是让电视机亮起来。找出问题出在插座上是不够的,这是唯一的插座,你现在应该开始拆插座了。哦,别忘了先把电视机装起来。 

第三个问题:歧义 

如果说,失效的注释会导致时间的浪费,那么歧义的注释就甚至可能导致直接的损失了。关于我们日常所用的自然语言有多容易产生歧义就不多说了。在这里借用温伯格老先生在《你的灯亮着吗》中举的一个例子: 

引用
谈到容易误解的词语、放错了地方的逗号或者一个表意不明确的语法,以及由它们造成的一万、十万、一百万或者你所能想象的足够多钞票的损失,任何一个计算机程序员都能提供十几个例子。 
例如,一段程序注释上写着, 
“异常信息也会出现在文件 XYZ 中。”The exception information will be in the XYZ file, too.) 
某个程序员理解为, 
“这条异常信息还会在文件 XYZ 的另一个地方出现。”(Another place the exception information appears is the XYZ file.) 
于是他认为这条异常信息在别的地方还有备份,因此他觉得他的程序中没有必要保留它。 
事实上,写这条注释的人的意思是,“XYZ 文件中有一种信息也属于异常信息。”Another type of information that appears in the XYZ file is the exception information.) 
这句话并不表示这条信息在别的地方有备份,并且事实上,并没有什么备份。结果,这条宝贵的、不能复原的信息就这样丢失了。在人们发现对这句注释有不同的理解之前,这条丢失了的异常信息造成的损失已经达到 50 万美元了――对于一个因为考虑不周全而加的“也”字来说,这损失未免太惨重了。 
50 万美元就这样飞了,一定会有人掉脑袋。但是,应该砍谁的头呢?写注释的那个人?还是程序员?大多数英语课堂上会把写注释的家伙推上断头台。而教人解决问题的老师则会把这个程序员的脖子放在断头台上。有没有人喜欢不流血的方法? 
我们可以告诫那些写注释的人,对于问题表述来说清晰好懂是多么的重要,直到他们被这废话的海洋淹死。我们也可以敦促问题解决者们阅读的时候更加仔细,然后他们都会变成瞎子。按照以往的经验,这些都没什么用。不管人们多么真诚地去努力,单靠增加投入精力的数量是不够的。你永远都不能确信这里的每个人对于同一个词都和你有相同的理解。 



在书中,温伯格只是提出了问题,没有给出具体解决的办法。这种事情,甚至连责任该落到谁头上都是公说公有理,婆说婆有理。唯一可以肯定的是,无论最后责任归到谁头上,那人都是一肚子的冤枉。那我们不妨想想,为了避免自己受这种冤枉气,我们该怎么办?至少我个人得出的结论是,如果我是写程序的那个家伙,我就干脆不要写那条注释。如果我是读程序那个家伙,我就干脆不要管那条注释。 

第四个问题:救生圈效应 

这是我自己临时想出来的一个说法,什么是救生圈效应呢。很多大人都知道,如果要教小孩子学游泳,最简单有效的办法是直接把他扔到水里,虽然在最开始的时候,你要时刻注意着他,在必要时才扶一把。但等他呛几口水后,很快就自然学会狗扒式了。这时再去教什么蛙式自由式就简单得多了。但如果你一开始给他一个救生圈,虽然你就算一直不管他也能自己游个十几米,但他永远学不会自己游。而最大的问题是,并不是任何时候你都能找到救生圈。 

那些觉得在程序里写满注释然后交给新手,幻想自己在帮助新手进步的朋友,有没有想过这些注释就是救生圈?而那些你教导出来的程序员总会有一天在论坛上发帖骂娘:我刚入职的某某公司的代码里TMD没有一行注释!在我听起来,这种抱怨就像一个快溺水的人在喊海里怎么没有救生圈,这能怪海吗? 

内部注释在心理上的救生圈效应则更为明显,是一个非常好的推卸责任的皮球。写程序人觉得程序本身不太清晰,没关系,我的注释写清楚了。出了问题,那是修改的人没好好维护我的注释,读程序人没好好理解我的注释。读程序的人觉得程序不太理解,没关系,直接相信注释就行了,出了问题,那是有一条不知道是谁写的注释TMD骗了老子,只能怪老子倒霉了。 

假如你是项目经理,有A和B两个人让你选。A写的代码是自描述的,结构清晰,但没有一行内部注释。B写的代码一团浆糊,但注释详细,代码连着注释看尚算能懂。排错时,如果是带有完善准确注释的代码,A比B慢一倍(实际上可未必),如果是没有注释或注释有错的代码,A的速度不变,B比A慢几倍,甚至完全卡住(临急抱佛脚看代码的结果)。你会选谁? 



前面说了那么多注释的问题,但除了注释,还有没有其他能帮助我们理解程序,又没有上述种种问题的方式呢。如果到了最后,注释是唯一的选择,那就没什么好说的了。下面就开始聊聊内部注释的几个种类,和它们的替代方案。不过今天夜了,且听下回分解吧。

posted @ 2013-08-05 17:03  果粒遇到前端  阅读(171)  评论(0编辑  收藏  举报