我正在与新同事讨论有关评论的问题。我们俩都喜欢Clean Code,我非常满意以下事实:应该避免内联代码注释,并且应该使用类和方法名称来表示它们的作用。

但是,我非常喜欢添加小类摘要,以试图解释类的目的和实际代表什么,主要是为了使其易于维护单一职责原则模式。我也习惯在方法中添加单行摘要,以说明该方法应该执行的操作。一个典型的例子是简单的方法

public Product GetById(int productId) {...}


我要添加以下方法摘要

/// <summary>
/// Retrieves a product by its id, returns null if no product was found.
/// </summary


I相信应该记录该方法返回null的事实。想要调用方法的开发人员不必打开我的代码即可查看该方法是否返回null或引发异常。有时它是接口的一部分,所以开发人员甚至都不知道正在运行的底层代码?

但是,我的同事认为这些注释是“代码异味”,而“注释总是失败”(罗伯特·C·马丁)。

是否可以表达和交流这些类型的知识而无需添加注释?由于我是Robert C. Martin的忠实拥护者,所以我有些困惑。摘要与注释相同,因此总是失败吗?

这不是在线注释的问题。

评论

罗伯特·马丁(Robert Martin)说:“评论总是失败”?好吧,那么他是个边缘极端主义者,应该带一点盐吃。 (是的,我知道他为修辞目的而写这样的文字是为了传达他的信息。我的意思是,你也应该如此。)

鲍勃叔叔的书应该装一袋1公斤盐...

如果您关注的是罗伯特·马丁(Robert Martin),则空案例的文档应该是测试。也就是说,您应该进行测试以显示在这种情况下该方法可以返回null。另外,由于这是Java,因此@Nullable注释也比注释要好。

@Bjorn我拥有“清洁代码”的副本,并且已经阅读了涵盖多次的内容。是的,鲍伯叔叔更喜欢代码能够自我记录,但是在书中自己的代码中有多个注释示例。关键是,如果您觉得自己不得不写评论,请尽力更改代码而不是添加评论,但不要完全禁止评论(甚至是内嵌评论)。

该方法应称为TryGetById,并应删除注释。

#1 楼

正如其他人所说,API文档注释和嵌入式注释之间是有区别的。从我的角度来看,主要区别在于,在代码旁边读取了嵌入式注释,而在您注释的内容的签名旁边读取了文档注释。

鉴于此,我们可以申请相同的DRY原理。评论是否与签名相同?让我们看一下您的示例:


按其ID检索产品


这部分只是重复我们从名称GetById加上的内容返回类型Product。这也提出了一个问题,即“获取”和“检索”之间的区别是什么,以及在该区别上带有什么代码与注释。因此,这是不必要的,并且有些混乱。如果有的话,它会妨碍实际有用的注释的第二部分:


如果没有找到产品,则返回null。


啊!我们绝对不能仅仅从签名中就知道这一点,它提供了有用的信息。


现在,请进一步。当人们谈论代码的味道时,问题不在于代码本身是否需要注释,而在于注释是否表明可以更好地编写代码以表达注释中的信息。这就是“代码气味”的含义-并不意味着“不要这样做!”,它的意思是“如果您这样做,可能表明存在问题”。

因此,如果您的同事告诉您有关null的注释是一种代码味道,您应该简单地问他们:“好吧,那我该如何表达呢?”如果他们有可行的答案,那么您已经学到了一些东西。如果不是这样,可能会杀死他们的投诉。


关于这种特殊情况,通常众所周知,空问题是一个困难的问题。原因是代码库中堆满了保护子句,为什么空检查是代码契约的普遍前提,为什么空的存在被称为“十亿美元的错误”。没有太多可行的选择。不过,在C#中发现的一种流行的用法是Try...约定:

public bool TryGetById(int productId, out Product product);


在其他语言中,使用类型可能是惯用的(通常称为OptionalMaybe)表示可能存在或不存在的结果:

public Optional<Product> GetById(int productId);


所以,从某种程度上来说,这种反评论立场已经使我们陷入困境:关于此评论是否代表一种气味,以及对我们可能有什么替代方案,我们的想法最少。

相对于原始签名,我们是否真的应该更喜欢这些替代方案是另一回事,但我们至少可以选择表达通过代码而不是注释,当找不到产品时会发生什么。您应该与您的同事讨论他们认为哪种选择更好,为什么更好,并希望帮助他们超越关于评论的笼统教条。

评论


或与Linq等效的Try ...,... OrDefault,如果该子句将导致空结果,则返回default(T)。

–巴特·范·尼罗普
2015年6月4日上午10:29

非常感谢您对内联代码注释和文档注释以及给出的示例之间的区别:)

–雷切尔(Rachel)
2015年6月4日15:41

函数的可能返回值应通过其签名来证明。 TryGetValue模式是在C#中执行此操作的一种合理方法,但是大多数功能语言都具有更好的表示缺失值的方法。在这里阅读更多

– AlexFoxGill
15年4月4日在16:40

@BenAaronson:如果希望拥有一个可以支持协方差的通用接口,则可以对任何类型T使用T TryGetValue(params,ref bool success)或T TryGetValue(params),对于表示类约束的对象,可以使用null表示失败。类型T,但是返回布尔值的TryGetXX模式与协方差不兼容。

–超级猫
2015年6月4日在17:07

在Java 8中,您可以返回Optional 来指示该方法可能没有返回Product。

– Wim Deblauwe
2015年6月5日下午6:18

#2 楼

罗伯特·C·马丁(Robert C. Martin)的引用是出于上下文。以下是带有更多上下文的引文:


没有什么比放置适当的注释那么有用了。
琐碎的教条式注释只会使模块混乱。没有什么能像传播谎言和错误信息的粗鲁的评论那样具有破坏性了。

评论不像迅达的名单。他们不是“纯粹的好人”。
的确,评论充其量是必要的邪恶。如果我们的编程语言具有足够的表达力,或者我们有能力巧妙地运用这​​些语言表达我们的意图,那么我们就不需要太多注释了,也许根本就不需要。

注释的正确用法是为了弥补我们未能在代码中表达自己的意图。请注意,我使用了失败一词。我是真的
评论总是失败。我们必须拥有它们,因为我们不能
总是想出没有它们的表达方式,但它们的使用
并不是值得庆祝的原因。
因此,当您发现自己在一个确定您需要写
注释的位置,仔细考虑一下,看看是否没有办法转过这些表并用代码表达自己。每次在代码中表达自己时,都应该轻拍一下。每次
发表评论时,您都应该做个鬼脸,并感到自己表达能力的失败。
摘自Clean Code:《敏捷软件技巧手册》

如何将此报价简化为“注释始终是失败”,这是一个很好的示例,说明了某些人如何从上下文中获取明智的报价并将其变为现实变成愚蠢的教条。


API文档(如javadoc)应该记录API,以便用户无需阅读源代码即可使用它。因此,在这种情况下,文档应说明该方法的作用。现在,您可以说“通过其ID检索产品”是多余的,因为它已经由方法名称指示,但是可能要返回null的信息对于记录文档绝对重要,因为这一点一点都不明显。 />
如果要避免注释的必要性,则必须通过使API更明确地消除根本的问题(使用null作为有效的返回值)。例如,您可以返回某种Option<Product>类型,因此类型签名本身可以清楚地传达在未找到产品的情况下将返回什么。

但是在任何情况下,记录API都是不现实的仅通过方法名称和类型签名即可。将文档注释用于用户应该知道的所有其他非显而易见的信息。以BCL中DateTime.AddMonths()的API文档为例:


AddMonths方法计算得出的月份和年份,并考虑了br年和一个月中的天数。 ,然后
调整生成的DateTime对象的日期部分。如果
结果日不是结果月份中的有效日,则使用结果月中的最后一个
有效日。例如,3月31日+ 1
月= 4月30日。结果DateTime
对象中的时间部分与此实例相同。


您无法仅使用方法名称和签名来表示!当然,您的班级文档可能不需要这种详细程度,仅是示例。


内联注释也不错。

不良注释不好。例如,仅说明可以从代码中轻松看到的注释,经典示例为:

// increment x by one
x++;


这些注释解释了一些可以通过重命名变量或方法或通过重组代码来使事情变得清晰的东西,这是代码的味道:

// data1 is the collection of tasks which failed during execution
var data1 = getData1();


这些是Martin反对的评论。此注释是未能编写清晰代码的征兆-在这种情况下,对变量和方法使用不言自明的名称。注释本身当然不是问题,问题是我们需要注释才能理解代码。

但是注释应用于解释从代码中不明显的所有内容,例如为什么以某种非显而易见的方式编写代码:

// need to reset foo before calling bar due to a bug in the foo component.
foo.reset()
foo.bar();


注释解释了一段过于复杂的代码也是一种气味,但解决方法不是禁止发表评论,解决方法就是修复代码!实际上,确实发生了混乱的代码(希望只是暂时的直到重构),但是没有普通的开发人员第一次编写完美的干净代码。当卷积的代码发生时,写一个注释来解释它的作用比不写注释要好得多。此注释还将使以后的重构更加容易。

有时代码不可避免地很复杂。它可能是复杂的算法,或者出于性能原因可能是牺牲清晰度的代码。再次需要注释。

评论


在某些情况下,您的代码也只能处理复杂的情况,而没有简单的代码可以处理。

– gnasher729
15年6月4日在9:02

好点,漱口。当您必须优化某些代码以提高性能时,这似乎经常发生。

–雅克B
2015年6月4日,9:07

如果使用x ++的注释,例如“将x递增1,如果是UINT32_MAX,则换行”,则可能会很好。任何了解语言规范的人都知道,增加uint32_t会自动换行,但是如果没有注释,可能不知道这种自动换行是否是所实现算法的预期部分。

–超级猫
2015年6月4日17:10



@ l0b0:希望你在开玩笑!

–雅克B
2015年6月4日23:48

@ l0b0没有诸如临时代码之类的东西。该代码永远不会被重构,因为企业对结果感到满意,并且不会批准资金来修复它。从现在起的五年后,一些初级开发人员将看到此代码,因为您已将其替换为Bugtrocity v9,所以您甚至都不再使用WizBug 4.0,因此“ Bug123”对他而言毫无意义。他现在认为这应该是永久性代码,并在整个职业生涯中成为一名糟糕的开发人员。想想孩子们。不要写临时代码。

– corsiKa
2015年6月5日在17:44

#3 楼

注释代码和记录代码之间是有区别的。




需要注释来稍后维护代码,即更改代码本身。

评论确实确实有问题。极端的说法是,它们始终表示问题,无论是在您的代码内(代码太难理解)还是在语言内(语言无法足够表达;例如,该方法从不返回null的事实可能是)通过C#中的代码契约表达,但是无法通过PHP等代码表达。


需要文档才能使用对象(类,接口)您开发的。目标读者是不同的:我们在这里谈论的不是维护您的代码并对其进行更改的人员,而是几乎不需要使用它的人员。

因为代码足够清晰而删除文档太疯狂了,因为此处的文档专门用于使类和接口成为可能,而不必阅读数千行代码。



评论


是的,但至少马丁的观点是,在现代开发实践中,测试是文档,而不是代码本身。假设正在使用BDD风格的测试系统对代码进行测试,例如specflow,测试本身是对该方法行为的直接可读描述(“当使用有效产品的ID调用GetById时,给出了产品数据库,则当调用GetById时返回了相应的Product对象。产品ID无效,然后返回null”或类似的内容。

–法律
2015年6月7日在2:45

#4 楼

好吧,您的同事似乎在读书,接受他们所说的话,并运用他学到的东西而没有思考,也没有考虑任何上下文。

您对函数的作用的注释应该这样,您可以丢弃实现代码,阅读函数的注释,并且可以编写实现的替换,而不会出错。

如果注释不告诉我是否引发异常或是否返回nil,则无法执行此操作。此外,如果注释没有告诉您是否抛出异常或是否返回nil,则无论在何处调用该函数,都必须确保代码正确运行(无论是否抛出异常或返回nil)。

所以你的同事是完全错误的。继续阅读所有书籍,但要自己考虑。

PS。我看到您的行“有时它是接口的一部分,所以您甚至都不知道正在运行的代码。”即使使用虚函数,您也不知道正在运行的代码。更糟糕的是,如果您编写了一个抽象类,甚至没有任何代码!因此,如果您有一个带有抽象函数的抽象类,则添加到抽象函数中的注释是具体类的实现者必须指导它们的唯一事情。这些注释也可能是唯一可以指导该类用户的内容,例如,如果您拥有的只是一个抽象类和一个返回具体实现的工厂,但是您从未看到该实现的任何源代码。 (当然,我不应该查看一种实现的源代码)。

评论


我十年没有评论代码了。评论很肿,很垃圾。这些天没有人评论代码。我们专注于结构良好且命名良好的代码,小型模块,去耦等。这使您的代码可读而不是注释。测试可确保如果丢弃代码,则不会出错,不会注释。测试告诉您如何使用编写的代码,如何调用它们以及为什么它们首先存在。您太老了,我的朋友需要学习测试和清除代码。

–PositiveGuy
17年6月17日在19:03

#5 楼

有两种类型的注释可供考虑-那些对代码可见的注释以及用于生成文档的注释。

Bob叔叔所指的注释类型仅对人们可见。与代码。他提倡的是一种DRY。对于正在查看源代码的人员,源代码应该是他们所需的文档。即使在人们可以访问源代码的情况下,注释也不总是不好的。有时,算法很复杂,或者您需要了解为什么采用非显而易见的方法,以便其他人在尝试修复错误或添加新功能时最终不会破坏您的代码。

所描述的注释是API文档。这些是使用您的实现的人员可以看到的,但是可能无法访问您的源代码。即使他们确实有权访问您的源代码,他们也可能正在其他模块上工作,而不在查看您的源代码。这些人会发现在编写代码时在IDE中提供此文档很有用。

评论


老实说,我从来没有想到过将DRY应用于代码+注释,但这是完全合理的。有点像@JacquesB的答案中的“增量X”示例。

–user22815
2015年6月4日15:04

#6 楼

评论的价值以其提供的信息的价值减去阅读和/或忽略它所需的精力来衡量。因此,如果我们分析评论

/// <summary>
/// Retrieves a product by its id, returns null if no product was found.
/// </summary>


关于价值和成本,我们会看到三件事:


Retrieves a product by its id重复了名称该函数表示,因此成本就是没有价值。应该将其删除。
returns null if no product was found是非常有价值的信息。这可能会减少其他编码人员必须查看功能实现的时间。我敢肯定,它节省的阅读成本要比它自己介绍的阅读成本高。应该保留。

线路

/// <summary>
/// </summary>


不携带任何信息。对于评论的读者来说,它们是纯成本。如果您的文档生成器需要它们,则可能是合理的,但在这种情况下,您可能应该考虑使用其他文档生成器。

这就是使用文档生成器是一个有争议的想法的原因:它们通常需要许多其他注释,这些注释不带任何信息或重复明显的内容,只是为了使文档更美观。



我在任何其他答案:

即使不是理解/使用代码所必需的注释也可能非常有价值。这是一个这样的示例:

//XXX: The obvious way to do this would have been ...
//     However, since we need this functionality primarily for ...
//     doing this the non-obvious way of ...
//     gives us the advantage of ...


可能很多文本,完全不需要理解/使用代码。但是,它解释了代码看起来像以前一样的原因。这将使人们停止查看代码,不知道为什么它没有采用明显的方式,并开始重构代码,直到他们意识到为什么首先编写这样的代码。即使读者足够聪明,不能直接跳转到重构,他们仍然需要弄清楚为什么代码看起来像以前一样,然后才意识到最好保持现状。该评论实际上可以节省工作时间。因此,该值高于成本。

同样,注释可以传达代码的意图,而不仅仅是代码的工作原理。他们可以描绘通常在代码本身的细节中迷失的全局。因此,您赞成班级评论是正确的。如果他们解释类的意图,类与其他类的交互作用,如何使用等,则我最看重类注释。可惜,我不是这类注释的主要作者...

评论


哇-更改文档生成器,因为它需要几行额外的html行才能进行解析?不要吧。

– corsiKa
2015年6月5日17:45

@corsiKa YMMV,但是我更喜欢一个文档生成器,它将注释的成本降低到最低。当然,我也宁愿阅读写得很好的头文件,也不愿阅读与实际代码不同步的doxygen文档。但是,正如我所说,YMMV。

–cmaster-恢复莫妮卡
2015年6月5日18:06

即使方法的名称很好地描述了其目的,使用自然语言重复该目的也可以使阅读它的人更容易将其与随后的所有警告联系起来。重述名称中描述的内容将足够简短,即使值很小,成本也将很低。因此,我不同意您文章的第一部分。但是,第二部分为+1。被评估和拒绝的替代方法的文档可能非常有价值,但是鉴于应得到的重视,此类信息很少。

–超级猫
2015年6月5日在20:54

GetById提出了一个问题,即什么是id,以及从何处获取什么。文档注释应允许开发环境显示这些问题的答案。除非在模块doc注释的其他地方进行了说明,否则它还是一个可以告诉您为什么仍会获得ID的地方。

–氢化物
2015年6月8日在5:48



注释,干净的代码(自我描述),TDD(经常获得反馈并经常获得设计反馈)和测试(给您信心和文档行为)规则!测试人员测试。这里没有人在谈论这个。醒来

–PositiveGuy
17年6月17日在19:04



#7 楼

未注释的代码是错误的代码。一个普遍的(如果不是普遍的)神话是,代码的读取方式与英语类似。它必须被解释,除了最琐碎的代码之外,任何其他代码都需要花费时间和精力。另外,每个人在阅读和编写任何给定语言方面都有不同程度的能力。作者和读者的编码风格和能力之间的差异是准确解释的强大障碍。您可以从代码的实现中得出作者的意图,这也是一个神话。以我的经验,添加额外的评论很少是错误的。

Robert Martin等。认为它是“难闻的气味”,因为可能是代码已更改而注释未更改。我说这是一件好事(就像在家用燃气中添加“难闻的气味”以提醒用户泄漏一样)。阅读注释为您解释实际代码提供了一个有用的起点。如果它们匹配,那么您将对代码有更大的信心。如果它们不同,则您已检测到警告气味,需要进一步调查。解决“难闻的气味”的方法不是去除异味,而是密封泄漏。

评论


假设您是一位聪明的作者,而文字是为了让聪明的读者受益;您使用常识来划定界限。显然,就目前而言,该示例是愚蠢的,但这并不是说没有充分的理由来说明为什么此时代码包含i ++。

–文斯·奥沙利文(Vince O'Sullivan)
2015年6月4日13:39



i ++; //调整leap年。

–文斯·奥沙利文(Vince O'Sullivan)
2015年6月4日在13:54



“ Robert Martin等人将其视为“难闻的气味”,因为可能是代码已更改而注释未更改。”那只是气味的一部分。最糟糕的气味来自于这样的想法,即程序员决定不尝试以更具描述性的方式编写代码,而是选择在注释中“密封泄漏”。他的断言是,应该在代码本身(或类似内容)中使用“ adjustForLeapYears()”方法,而不是打“ //为leap年调整”注释。该文档采用行使the年逻辑的测试形式。

–埃里克·金(Eric King)
2015年6月4日15:39

我会考虑添加一个方法调用,以用注释掉掉多余的代码来替换一行代码,尤其是因为该方法的名称实际上只是标记了一段代码的注释,而不能保证更好的文档准确性。 (如果该行出现在两个或多个位置,那么引入方法调用当然是正确的解决方案。)

–文斯·奥沙利文(Vince O'Sullivan)
15年4月4日在18:10

@Jay原因是使您的抽象明确(例如通过引入方法)是规则。不这样做是因为例外,因为您可能会得到只有一行的方法。让我解释一下真正的任意规则:“使用编程语言的结构(通常是方法和类)引入抽象,除非该抽象的实现可以表示为一行代码,在这种情况下,通过添加自然语言来指定抽象一条评论。”

–埃里克
2015年6月5日15:18



#8 楼

在某些语言中(例如F#),整个注释/文档实际上可以在方法签名中表示。这是因为在F#中,除非明确允许,否则通常不允许null。

F#(以及其他几种功能语言)的共同点是,您使用选项类型Option<T>代替了null。可以是NoneSome(T)。然后,该语言将理解这一点,并在尝试使用这两种情况时迫使您在两种情况下进行匹配(或警告您是否匹配)。

因此,例如在F#中,您可以使用一个签名看起来像这样

val GetProductById: int -> Option<Product>


然后这将是一个函数,它接受一个参数(一个int),然后返回一个乘积,或者返回值None。

然后可以像这样使用它

let product = GetProduct 42
match product with
| None -> printfn "No product found!"
| Some p -> DoThingWithProduct p


如果两种情况都不匹配,则会收到编译器警告。因此,那里不可能获得空引用异常(除非您当然忽略编译器警告),并且仅通过查看函数签名即可知道所需的一切。

当然,这需要您的语言是通过这种方式设计的-许多通用语言(例如C#,Java,C ++等)都没有。因此,在您当前的情况下,这可能对您没有帮助。但是,很高兴知道有多种语言可以让您以静态类型的方式表达这种信息,而无需诉诸评论等。:)

#9 楼

这里有一些很好的答案,我不想重复他们说的话。但是,让我添加一些评论。 (不是双关语。)

聪明的人发表了许多关于软件开发和许多其他主题的陈述,我想这是非常好的建议,但在上下文中理解却很愚蠢。人们不应该脱离上下文,在不适当的情况下使用代码,也不要选择荒唐的极端做法。

代码应该是自我记录的想法就是这样一个极好的想法。但是在现实生活中,这种想法的实用性受到限制。

有一个要注意的地方是,该语言可能无法提供功能来清楚,简洁地记录需要记录的内容。随着计算机语言的改进,这已成为越来越少的问题。但是我不认为它已经完全消失了。早在我写汇编程序的时候,添加诸如“总价=非应税项目的价格加应税项目的价格加应税项目的价格加应税项目的价格*税率”这样的注释是很有意义的。在任何给定时刻,寄存器中的确切内容不一定是显而易见的。采取了许多步骤来进行简单的操作。等等,但是,如果您使用的是现代语言,那么这样的注释将只是重新声明一行代码。

当我看到诸如“ x = x + 7; //将7加到x”之类的评论时,我总是很生气。就像,哇,谢谢,如果我忘了加号意味着什么可能很有帮助。我可能真正感到困惑的地方是知道什么是“ x”或为什么在这个特定的时间需要在其上加上7。通过给“ x”赋予更有意义的名称并使用符号常量而不是7,可以使此代码自动记录文档。就像您编写“ total_price = total_price + MEMBERSHIP_FEE;”一样,那么可能根本不需要注释。 。

听起来不错,一个函数名称应该确切告诉您一个函数的功能,这样任何其他注释都将是多余的。我曾经写过一个函数,该函数检查项目编号是否在我们的数据库项目表中,并返回true或false,并称其为“ ValidateItemNumber”,这让我很回忆。看起来像一个得体的名字。然后其他人出现并修改了该功能,还为商品创建了订单并更新了数据库,但从未更改名称。现在这个名字很让人误解。听起来它做了一件小事情,但实际上它做得更多。后来有人甚至拿出了有关验证商品编号的部分,并在其他地方进行了修改,但仍然没有更改名称。因此,即使函数现在与验证项目编号无关,它仍被称为ValidateItemNumber。但是在实践中,如果没有函数名,通常不可能完全描述函数的功能函数名称与组成该函数的代码一样长。这个名称是否会告诉我们确切地对所有参数执行哪些验证?在异常情况下会发生什么?阐明所有可能的歧义?在某些时候,名称会变得很长,以至于变得混乱。我会接受String BuildFullName(String firstname,String lastname)作为体面的函数签名。即使不能说明名称是“ first first”还是“ last,first”或其他变体,但是如果名称的一个或两个部分为空白,并且对合并长度和如果超出该限制会如何处理,以及可能会询问许多其他详细问题。