有哪些评论示例可以告诉您原因而不是原因或方式?[关闭]


78

首先,在这个问题上,我想避免争论源代码注释是好是坏。我只是想更清楚地了解人们在谈论可以告诉您原因,原因或方式的评论时的意思。

我们经常看到诸如“注释应该告诉您为什么;代码本身应该告诉您如何”之类的准则。很容易就抽象的观点达成一致。但是,人们通常会像教条一样掉下来,离开房间而无需进一步解释。我已经看到它在许多不同的地方和环境中使用过,看起来人们可以就流行语达成共识,但是他们似乎完全是在谈论不同的事情。

因此,回到问题所在:如果评论可以告诉您原因,那么我们在说什么呢?这就是为什么这段代码首先存在的原因吗?这是片段代码应该做什么?如果有人可以给出清楚的解释,然后添加一些好的示例,我将不胜感激(确实不需要坏的示例,但是可以随意添加它们以进行对比)。

关于评论是好是坏,有很多问题,但是没有人能解决一个具体问题,即什么是可以告诉您为什么的好的评论示例。


36
有时,最好的评论说明了为什么不是。我曾经遇到过一些看起来很容易简化的复杂代码。该评论解释了为什么在这种特定情况下无法进行明显的简化(因为原始开发人员已经尝试过)。
丹·皮切尔曼

6
There are many questions on whether comments are good or bad, but no one that addresses the specific question of what are good examples of comments that tell you WHY. 如果每个人都提供有效的示例,那么它们都是正确的答案。本网站的格式是为了简化问答过程,在此过程中并非所有答案都相等。
大卫·卡钦斯基

好点,@ david-kaczynski。你有什么建议?
里克

1
在我脑海中,我想不出一种表达问题的方法,以使单个示例或通用策略可以成为“最佳”答案。p.se中有一个聊天部分:chat.stackexchange.com/rooms/21/the-whiteboard,但实际上可能会有一个更好的论坛来回答您的问题。平心而论,您的问题似乎在这里得到了社区的积极响应,因此可能不必担心。我可以找到有用评论示例的最佳建议是浏览流行的公共git存储库。
大卫·卡钦斯基

Answers:


62

最常见和最独特的示例是有关各种解决方法的注释。例如这个:

https://github.com/git/git/blob/master/compat/fopen.c

/*
 *  The order of the following two lines is important.
 *
 *  FREAD_READS_DIRECTORIES is undefined before including git-compat-util.h
 *  to avoid the redefinition of fopen within git-compat-util.h. This is
 *  necessary since fopen is a macro on some platforms which may be set
 *  based on compiler options. For example, on AIX fopen is set to fopen64
 *  when _LARGE_FILES is defined. The previous technique of merely undefining
 *  fopen after including git-compat-util.h is inadequate in this case.
 */
#undef FREAD_READS_DIRECTORIES
#include "../git-compat-util.h"

您一定会在Git和Linux资源中找到更多示例;两个项目都尝试遵循此规则。

我还建议对提交日志更严格地遵循此规则。对于代码注释,您可能会修复代码,但是忘记更新注释。借助普通项目中的大量代码,可以保证它迟早会发生。另一方面,提交日志与特定更改有关,可以使用版本控制系统的“注释” /“责备”功能进行调用。同样,Git和Linux也有一些很好的例子。

看一下这个提交。(此处未复制,太长了)。它有四个段落几乎占据了整个页面(有点过屏),描述了到底是什么错误以及为什么是错误的,然后继续修改了所有高达六行的内容。他们使用这样的注释有两个目的:

  1. 所有提交的更改都将进行审核,而提交日志则是必须向审核者解释更改的内容。
  2. 发现错误后,将使用“ pickaxe”或“ blame”来检索相关日志,以避免恢复为更早的错误行为。

(注意:我最多花了10分钟随机浏览git repo来提出这两个示例,因此在那找到更多示例肯定很容易)


29

一条注释,告诉您为什么解释代码背后的原因 -例如:

// We need to sync the values if the temp <doodad> GUID matches one of the active <doodad>'s
// GUID, as the temp <doodad> has the most recent values according to the server and said 
// values might have changed since we added the <doodad>. We want a user to be able to <foo> 
// the <doodad> whenever, which means those values must be accurate.
for (doodad in doodads) {
    if ([doodad guid] == [tempDoodad guid]) {
        [doodad updateFromDoodad:tempDoodad];
        break;
    }
}

一条注释,告诉您如何解释代码的作用。

// Loop through our <doodads> and check for a GUID match. If it matches, copy the new values
// on the <doodad> that matches 
for (doodad in doodads) {
    if ([doodad guid] == [tempDoodad guid]) {
        [doodad updateFromDoodad:tempDoodad];
        break;
    }
}

区别在于维护人员可以查看第一个,然后说:“哦,所以这可能已经过时了!” 在第二种情况下,所说的维护者有一条注释,它不会告诉您代码本身没有揭示的任何内容(假设变量名很好)。

这是一个现实的示例,其中为什么注释,我在一些iOS代码中研究了我们需要获取网关地址(或对此的合理猜测)的地方。我本来可以留下说“初始化接收套接字”之类的评论,但是那只会告诉维护者(或将来给我)发生了什么,而不是为什么我必须做这个奇怪的尝试才能在网关中获取网关地址。第一名。

/*
 We're going to do something really hacky here and use a custom partial
 implementation of traceroute to get our gateway IP address.

 [rant removed - irrelevant to the point]

 There's no good way to get at the gateway address of an iDevice
 right now. So, we have two options (per https://devforums.apple.com/message/644915#644915 ):
 1. Get at and parse the routing table (like netstat -rn, or route -n)
 2. Do a traceroute and grab the IP address for the first hop

 As far as I can tell, the former requires <sys/route.h> from the Mac OS X
 header files, which doesn't seem like a good idea to me. Also, there's a
 thread on the Apple Developer forums that seems to imply that header isn't
 in iOS for a reason (https://devforums.apple.com/message/774731#774731 ).

 So when we send our request with a TTL of one it will survive a single hop
 to the router and return, triumphant, with the router's IP address!

 Viva la kludge!

 PS: Original source was the below SO question, but I've modded it since then.
 http://stackoverflow.com/questions/14304581/hops-tracing-ttl-reciveform-on-ios/14304923#14304923
 */

// Default to using Google's DNS address. We used to try checking www.google.com
// if reachability reported we had internet, but that could still hang on routers
// that had no internet connectivity - not sure why.
const char *ip_addr = [kGoogleDNS UTF8String]; // Must be const to avoid undefined behavior
struct sockaddr_in destination,fromAddr;
int recv_sock;
int send_sock;

// ... more code follows

4
第一个示例过于冗长,包括许多“如何”。它应该说只是“从temp <doodad>更新<doodads>,以便用户可以随时安全地<foo>进行它”。其余的从此或代码中隐含起来很简单。最后一个例子的前四段中的“童话介绍”也是毫无意义的。我要离开“万岁啦!”;很有趣,到最后。但是开始之前,在获得真正的解释之前,必须先挖掘很多单词。
2013年

@JanHudec根据您的反馈进行调整。看看对不对?
thegrinner 2013年

15
关于第二个示例的好处之一是,它不仅解释了为什么代码以特定的方式工作,而且还解释了为什么没有采用其他合理的替代方法。这使代码更具可维护性,因为下一个阅读代码并认为“为什么我不能只解析路由表?”的人。可以阅读评论。此外,确实提出了合理理由来更改代码的人将更有信心这样做是安全的。否则,维护人员会担心在导致混乱的(未知)场景中任何更改都会失败。
布莱恩

18

我想以Jeff Atwood在他的博客文章“代码告诉你如何,评论告诉你为什么”中的引用开始回答:

最好的评论就是您不需要的评论

他还指出:

您首先应该努力使您的代码尽可能地简单易懂,而不必依赖注释。仅在无法简化代码的地方,才应开始添加注释。

我完全同意,在这一点上,我必须补充一点,在开始使代码尽可能简单之前,我先使代码工作,然后开始重构。因此,在重构之前的第一次运行中,请添加注释为什么很有帮助

例如,如果在解析数据时使用带有2个二维哈希表的3个嵌套循环来填充工作日表,则很容易失去对某人甚至您自己所做的事情的跟踪,如果您不看几个星期而突然重构。

[loop1]6oclock -> [loop2]Monday -> [loop3]stage 1 to 4
         -> tuesday-> stage 1 to 4
         ...
         -> Saturday -> stage 1 to 4
    7oclock -> Monday-> stage 1 to 4
        ....etc.

上半部是重构之前3个嵌套循环如何工作的示例。
同时解释一些分支条件可以帮助人们更好地理解代码,并了解过程中的想法:

// added a zero before the actual day in order for the days always to be 2 digits long.
if( actualDayFuture < 10 ) 
{ 
     actualDayFuture = padIfSingleDigitDate(actualDayFuture); 
}

即使是简单明了的代码也可以很好地与注释配合。只是为了让同事,甚至您自己在维护软件时,使事情变得更明显,更清楚或更容易理解。

当然,xp声明有可以自我解释的代码,但是一行注释是否有害?

我还发现此博客中的以下规则非常有帮助:

  • 写作之前先了解材料
  • 写得好像你的听众是四年级学生
  • 考虑一下读者可能如何曲解你

任何必须重新使用自己的代码,别人或什至是遗留代码的人都知道,这可能令人头疼。因此,为什么不懒惰或试图成为一个超级程序员而不评论任何东西或很少发表评论,为什么不让自己或一些可怜的bugger(这些人必须维护您的代码)通过遵循引用的规则使将来的生活变得容易得多。

同样,在回顾过程中也会对许多编程决定产生怀疑,而且即使某些代码部分对于程序的运行至关重要,由于在使用了多年的代码后发现了一个重大错误,因此并不总是清楚为什么编写了某些部分。因此,为了不让大家对tl; dr 感到厌烦,请以acmqueue的最后引号结尾

事先,清晰而广泛的文档是创建可以生存和适应的软件的关键要素。记录高标准将减少开发时间,改善工作并改善底线。很难从任何技术中获得更多要求。


8
在您的第二个示例中,可以通过重构完全消除注释:actualDayFuture = padIfSingleDigitDate(actualDayFuture); 这是微不足道的,但更可靠的示例将从此方法中受益。
克里斯·库德莫

4
我也将条件转移到方法中。再次说明,这并非微不足道,但它使我完全不必考虑填充逻辑。不过,我不会替换您原来的示例,因为这是对问题的更好回答。它只是一个旁注,探讨了其他替代方法。
克里斯·库德莫

1
广告“确定xp声明有可以自我解释的代码,但是一行注释会受到伤害吗?”:注释很好,但是也存在过分注释的危险。注释的每一行都是人们在更改代码时可能忘记更新的内容。
2013年

1
更好的说法是“最好的评论是无需评论”。不需要(但仍要写)的注释不是好的注释。
卡兹(Kaz)2013年

1
有趣的是所引用的代码int directionCode = (x > oldX) ? DIRECTIONCODE_RIGHT : (x > oldX) ? DIRECTIONCODE_LEFT : DIRECTIONCODE_NONE;是错误的。当然应该... (x < oldX) ? DIRECTIONCODE_LEFT : DIRECTIONCODE_NONE;。好的注释建议-错误的代码。
chux 2015年

8

我倾向于将注释减少为对某些功能/代码进行更彻底解释的参考,或者解释为什么选择某种编程方式。

考虑到其他具有类似技能的程序员正在使用或阅读您的代码,因此,如果您使用不同于预期的方式来实现某些目标,则必须进行注释。因此,您可以在评论中解释为什么选择这种方式。

例如,如果您可以在Android设备上使用两个不同的传感器,但其中一个不适合您的需求,则可以在注释中解释为什么选择了另一个。

因此,“为什么”应该为您做出的选择提供理由


5
参考是一个很好的例子。//此方法使用furshclingeheimer算法对foobit进行ronsterize。参见http:// ...
克里斯·库德莫

8

注释应该告诉您代码没有什么内容,不一定要由WHYHOWWHAT来定义。如果您的名字很好,并且函数描述清楚,那么代码很可能会告诉您发生了什么。例如:

List<LightMap> maps = makeLightmaps(receivingModels);
TrianglePartitioner partition = new Octree(castingTriangles);
List<Photon> photons = firePhotons(lights, partition);

if (photons.Count > 0)
{
      PhotonPartitioner photonMap = new KDTree(photons);
      gatherPhotons(maps, photonMap, partition, lights);
}

此代码确实不需要注释。函数和类型名称使它易于理解。

但是,有时有时候很难或不可能真正制作出像上面这样的流利代码。例如,下一个代码段用于查找球体上的统计随机点。数学是相当不透明的,因此带有解释链接的注释可以帮助告诉它如何工作。这可以被包裹在一个函数来告诉什么它无需评论,如果需要超过一次,否则链接的标题也有助于在该部门。

double randomA = localGenerator.NextDouble();
double randomB = localGenerator.NextDouble();

//http://mathworld.wolfram.com/SpherePointPicking.html
double theta = 2 * Math.PI * randomA;
double phi = Math.Acos(2 * randomB - 1);

Vector3 randomDirection = new Vector3(Settings.ambientRayLength * (float)(Math.Cos(theta) * Math.Sin(phi)),
                                      Settings.ambientRayLength * (float)(Math.Sin(theta) * Math.Sin(phi)),
                                      Settings.ambientRayLength * (float)Math.Cos(phi));

注释何时告诉您代码没有什么的另一个示例是用于解释决策。在下一个示例中,代码不会在线程代码段内锁定非线程局部变量。这是有原因的,评论解释了为什么。没有评论,它可能被认为是一个错误,或者甚至没有被注意到。

Random random = new Random();
Parallel.For(0, maxPhotons, delegate(int photonIndex, ParallelLoopState state)
{
    ...
    //I don't actually care if this random number is unique between threads, threadsafty is not that big of a deal
    //  in this case and locking the random object could cause a lot of lock contention
    while (random.NextDouble() > reflectProbability)
    {
        ...
    }
    ...
}

也许可以改进一下,为什么不首先在并行循环内部创建随机对象。如果没有理由,它也可能使某人出现,并意识到整个想法很愚蠢,是重构的好地方。


在注释之前WriteText而不是注释中将代码描述为不需要注释是否合理//

1
正如我在回答中所说的,即使没有打印语句,也不需要注释,但是为了使其更清楚,我对其进行了编辑以删除打印语句。
2012年

5

认识不同种类的“为什么”可能会有所帮助-最值得注意的是:

  • 如果简化,似乎过于复杂的代码的原因将不起作用(例如,可能需要看似多余的类型转换,以确保代码在某些极端情况下可以工作)。

  • 某些看起来很危险的简单操作实际上是安全的原因(例如,“我们的提取数据例程将报告最后一个虚拟项目比其他项目少,而后面的项目则更大;应该排序的任何项目)在此之前,按照一致的升序或降序排列,其后将至少再有一个(可能是虚拟的)项目”)。

在许多情况下,代码一部分中的第二种类型的注释可能与另一种类型中的第一种类型的注释“匹配”(例如,“虽然看起来该操作序列可以简化,但Fitz例程依赖于直到Bandersnatch被鞭打后,Wongle才开始摇晃。”)


2

别忘了,如果您正在编写程序,那么您不仅在随机输入内容,而且还在执行它,因为您拥有所需模型,无论是在正式文档中还是在脑海中。头脑中的东西与计算机中的软件/数据一样真实(并且可能包含错误)。

读取您的代码的人可能没有头脑中的那个模型,因此注释可以用来告诉他们什么是模型以及代码与模型之间的关系。我认为这就是“为什么”的意思。当然,使代码本身尽可能自解释是很好的,但这并不总是足够好。例:

// transform the x,y point location to the nearest hexagonal cell location
ix1 = (int)floor(0.5 + x + y/2);
iy1 = (int)floor(0.5 + y);

最重要的是,模型会随时间变化,这些变化必须转移到代码中。因此,注释不仅需要说“为什么”代码中包含某些内容,而且同样重要的是如何响应预期的模型更改来进行更改。例:

// to change to square cell locations, remove the "+ y/2" in the above code

我认为有时忽略评论的目的。


2
问题是要举例。您能否添加示例以使此答案更有用?
布莱恩·奥克利

2
第一段代码看起来像一个向我解释“什么”的经典示例。并不是说这是一个不好的评论,但我认为它不能回答OP的问题。

@乔恩:如果没有评论,读者可以看到正在发生的事情,但不知道为什么。
Mike Dunlavey

1
@MikeDunlavey:我不同意。我仍然不知道-您为什么想要最近的六边形单元格位置?获取此位置的目的是什么?如果删除这两行会不会有任何影响?

2

并非我的所有评论都是“为什么”类型的,但很多都是。
这些是一个(Delphi)源文件中的示例:

// For easier access to the custom properties:

function GetPrivate: Integer;   // It's an integer field in the external program so let's treat it like that here

// The below properties depend on the ones above or are calculated fields.
// They are kept up-to-date in the OnEventModified event of the TTSynchronizerStorage
// or in the ClientDataSet.OnCalcFields of the TcxDBSchedulerStorage.DataSource.DataSet
property IsModified       : Boolean   read GetIsModified   write SetIsModified;
property IsCatTT          : Boolean   read GetIsCatTT      write SetIsCatTT;
property IsSynced         : Boolean   read GetIsSynced     write SetIsSynced;

lLeftPos := pos(' - [',ASubject); // Were subject and [shiftnaam:act,project,cust] concatenated with a dash?

// Things that were added behing the ] we will append to the subject:

// In the storage the custom value must also be set for:
Self.SetCustomFieldValueByname(cCustFldIsCatTT,Result);

// When we show the custom fields in a grid, the Getters are not executed,
// because the DevEx code does not know about our class helpers.
// So we have two keep both properties synchronized ourselves:

// lNewMasterEvent was set to usUpdated, overwrite because we added:
if ARepair then
  lNewMasterEvent.CustUpdateStatus := usRecreated

// The source occurrence date may have bee changed. Using GetOriginalDate we can retrieve the original date,
// then use that for creating a target occurrence (and update its date):

lNewTTOccurrence.CustSyncEntryID := cSyncEntryID0;    // Backward compatibility with old sync methode

// Single event became recurring or vice versa; replace entire event

// In contradiction to CopySingleEventToTimeTell, CopyMasterEventToTimeTell does not have a ANewStatus parameter
// because master events are always added.

请注意,为什么注释通常会在要执行注释的代码之前(因此以冒号结尾)。

我做了一些有评论仅解释什么正在发生的事情,例如,当一个进程具有逻辑分组(和代码不重构显示自动将该)许多步骤,我会喜欢评论:

// Step 1. Initialization

1

我理解WHY是您以可能奇怪或不合逻辑的方式执行某项操作的原因,因为在给定的情况下需要这样做。该如何可以在代码本身,可以看出无论是多么奇怪,即使代码是没有“感觉”。在什么可能是最好的类/函数文档的开头说。这样一来,您就可以添加WHY了,您可以在其中解释HOW和WHAT中未包括的任何内容,以及由于无法控制的原因而需要采取的特殊方法。

当然,在独角兽和彩虹之地之外并非总是如此。

怎么样:

foreach($critters as $creature) {
   $creature->dance();
}

什么:

/* Dancing creatures v1.0
 * 
 * The purpose of this is to make all your critters do the funky dance.
 */

foreach($critters as $creature) {
  $creature->dance();
}

为什么:

// We had to store the items in an array of objects because of _____ (reason)
foreach($critters as $creature) {
   $creature->dance();
}

5
这如何回答所提问题?
蚊蚋

1
引用OP的话:“那么,回到问题:如果评论应该告诉您为什么,我们在说什么呢?”,我回答了这个问题:谈论的原因是存在该原因的原因。给定一段代码。
Juha Untinen

1
这个问题要问几次实例。您能否在此答案中添加示例以使其更有用?
布莱恩·奥克利

1
我认为这些评论中的任何一条实际上都没有帮助。如果您的函数签名是critters.dance(),则注释仅重复显而易见的内容,并且“我们无法使其与我们尝试的任何其他方式一起使用”完全是无用的。同样,说“我们将为每个对象调用该方法”是在重复非常清楚地讲代码。
布伦丹·朗

1

我学会了总是在C ++头文件中写注释(因为尽管名称总是提供一个很好的提示,但并不总是清楚函数的作用),尤其是当您将API传递给其他开发人员或使用诸如doxygen之类的自动文档工具时。

所以对我来说,典型的评论看起来像

/*** Functionname
/*   What happens here
/*  [in] Params
/*  [out] params
/*** 

我唯一使用“为什么要注释”的东西是难以理解的,有时甚至是程序员难以理解的东西,例如“请勿触摸!因为...”或“如果行被删除,程序将崩溃”。

解决方法,黑客行为和怪异行为在我眼中符合WHY标准...

一个很好甚至有趣的例子就是这种“变通方法”,用于某些名叫理查德(Richard)的人编写的混乱代码,其他人将其包裹起来并解释了为什么在评论中... https://stackoverflow.com/a/184673/979785

不幸的是,有很多次,您因为无法触摸原图而被迫包装公牛****,或者是因为“一直以来都是这样”,或者您无权访问...或者,好吧,没有时间来修复原始文档,而实际上并没有资格获得开销。


7
除了问题是关于评论,而不是文档。它们实际上是不同的东西(documentation标记令人遗憾,但仍然不适用于该问题)。
托马斯

很好的事实是,在我的母语注释和文档注释中可以互换使用,因此对于标签,我认为它也适用于此问题。那真的是拒绝投票的原因吗?
2013年

2
该问题询问了几次为什么要发表评论的示例,但是您包括的唯一示例是“ 什么”评论。人们忽略了示例的答案可能会被您的示例误导。您能否举一个为什么要发表评论的例子?
布莱恩·奥克利

尽管我说代码中很少有WHY,但我举了两个例子:EDITED ...这是一个链接,绝对符合WHY的条件
AnyOneElse 2013年

@AnyOneElse我没有投票。在我到达之前就在那里。
托马斯

0

代码应指定执行计划。这样,程序跟随者(或编译器)可以弄清楚该做什么以及如何去做。什么被分解为程序跟随者可以遵循的步骤。原始步骤是方法。

编码人员的意图是另一回事。在简单,清晰,直接的代码中,意图很明显。任何相当熟练的人类阅读者都可以通过阅读代码来达到一段代码的目的。大多数代码应如下所示。

有时,意图和计划之间的关系是模糊的。该代码揭示了内容和方式,而不是原因。那时,揭示意图的评论是值得的。程序员的意图是为什么。


3
这个问题要问几个例子。您可以在答案中添加示例以使其更有用吗?
布莱恩·奥克利

0

现在,针对一个复杂且有点费解的数据模型,这个问题已经遍历了存储过程和视图。

我们已经(大量)组合了诸如“ x.account不为空并且x.address在(从联邦快递公司选择地址)然后x.account否则y.account结束的情况下的情况”之类的选择,并且尽管没有时间,但生产率还是可以预期的全部读取所有源代码。这个例子有点合理,但仍然难以理解。

这些注释解释了为什么在fedex中使用x,如果不是,则使用y,这说明了整个系统,当我们阅读了足够多的内容时,便开始理解它。而且这过于简化了,有成千上万的类似陈述。我对2007年的那种善良的开发人员谁为什么把那些为什么放进去深深地热情。

因此,是的,复杂的卷积数据模型和毛茸茸的面纱以及具有多个有效命名路径的存储过程,请热爱Gd,告诉我们原因。


0

我只是写了这个评论;这是一个具体的示例,解释了为什么一行代码就是它,特别是为什么我更改了它。

该方法检查存储的数据,并评估在一端当前一天到另一端开始日期是否完成。

// In principal, this should be ">=", as we may have data up to the account start
// date but not complete for that day; in practice, 98% of the time if we have
// data for the start date it *is* complete, and requerying it would be a waste
// of time.
while (endDate > accountStartDate)
    ...

您可能会猜到,大于运算符等于或大于或等于。该评论解释了为什么旧值有意义,而新值更好。如果将来有人对此进行研究,他们会看到“>”的使用不是疏忽,而是优化。然后,他们可以根据当时的需要更改或保留它。

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.