如何编写注释
如何编写注释
注释的目的是确保系统的结构和行为对读者来说是显而易见的,因此他们可以快速找到所需的信息,并有信心对其进行修改,以对系统进行修改。这些信息中的某些信息可以以对读者来说显而易见的方式表示在代码中,但是有大量信息无法从代码中轻易推导出。注释将填写此信息。
当遵循注释应描述代码中不明显的内容的规则时
注释应该描述代码中不明显的内容
编写注释的原因是,使用编程语言编写的语句无法捕获编写代码时开发人员想到的所有重要信息。注释记录了这些信息,以便后来的开发人员可以轻松地理解和修改代码。注释的指导原则是,注释应描述代码中不明显的内容。
从代码中看不到很多事情。有时,底层细节并不明显。例如,当一对索引描述一个范围时,由索引给出的元素是在范围之内还是之外并不明显。有时不清楚为什么需要代码,或者为什么要以特定方式实现代码。有时,开发人员遵循一些规则,例如“总是在
注释的最重要原因之一是抽象,其中包括许多从代码中看不到的信息。抽象的思想是提供一种思考问题的简单方法,但是代码是如此详细,以至于仅通过阅读代码就很难看到抽象。注释可以提供一个更简单,更高级的视图(“调用此方法后,网络流量将被限制为每秒
选择约定
编写注释的第一步是确定注释的约定,例如您要注释的内容和注释的格式。如果您正在使用存在文档编译工具的语言进行编程,例如
约定有两个目的。首先,它们确保一致性,这使得注释更易于阅读和理解。其次,它们有助于确保您实际编写评论。如果您不清楚要发表的评论以及发表评论的方式,那么很容易最终根本不发表评论。大多数注释属于以下类别之一:
- 接口:在模块声明(例如类,数据结构,函数或方法)之前的注释块。注释描述模块的接口。对于一个类,注释描述了该类提供的整体抽象。对于方法或函数,注释描述其整体行为,其参数和返回值(如果有
) ,其生成的任何副作用或异常,以及调用者在调用该方法之前必须满足的任何其他要求。 - 数据结构成员:数据结构中字段声明旁边的注释,例如类的实例变量或静态变量。
- 实现注释:方法或函数代码内部的注释,它描述代码在内部的工作方式。
- 跨模块注释:描述跨模块边界的依赖项的注释。
最重要的注释是前两个类别中的注释。每个类都应有一个接口注释,每个类变量应有一个注释,每个方法都应有一个接口注释。有时,变量或方法的声明是如此明显,以至于在注释中没有添加任何有用的东西(
不要重复代码
不幸的是,许多注释并不是特别有用。最常见的原因是注释重复了代码:可以轻松地从注释旁边的代码中推断出注释中的所有信息。这是最近研究论文中出现的代码示例:
ptr_copy = get_copy(obj) # Get pointer copy
if is_unlocked(ptr_copy): # Is obj free?
return obj # return current obj
if is_copy(ptr_copy): # Already a copy?
return obj # return obj
thread_id = get_thread_id(ptr_copy)
if thread_id == ctx.thread_id: # Locked by current ctx
return ptr_copy # Return copy
这些注释中没有任何有用的信息,但“ Locked by”注释除外,该注释暗示了有关线程的某些信息可能在代码中并不明显。请注意,这些注释的详细程度与代码大致相同:每行代码有一个注释,用于描述该行。这样的注释很少有用。以下是重复代码的注释的更多示例:
// Add a horizontal scroll bar
hScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
add(hScrollBar, BorderLayout.SOUTH);
// Add a vertical scroll bar
vScrollBar = new JScrollBar(JScrollBar.VERTICAL);
add(vScrollBar, BorderLayout.EAST);
// Initialize the caret-position related values
caretX = 0;
caretY = 0;
caretMemX = null;
这些注释均未提供任何价值。对于前两个注释,代码已经很清楚了,它实际上不需要注释。在第三种情况下,注释可能有用,但是当前注释没有提供足够的细节来提供帮助。编写注释后,请问自己以下问题:从未看过代码的人能否仅通过查看注释旁边的代码来编写注释?如果答案是肯定的(如上述示例所示
另一个常见的错误是在注释中使用与要记录的实体名称相同的词:
/*
* Obtain a normalized resource name from REQ.
*/
private static String[] getNormalizedResourceNames(
HTTPRequest req) ...
/*
* Downcast PARAMETER to TYPE.
*/
private static Object downCastParameter(String parameter, String type) ...
/*
* The horizontal padding of each line in the text.
*/
private static final int textHorizontalPadding = 4;
这些注释只是从方法或变量名中提取单词,或者从参数名称和类型中添加几个单词,然后将它们组成一个句子。例如,第二个注释中唯一不在代码中的是单词“ to”!再说一次,这些注释可以仅通过查看声明来编写,而无需任何了解变量的方法。结果,它们没有价值。如果注释旁边的代码中的注释信息已经很明显,则注释无济于事。这样的一个例子是,当注释使用与所描述事物名称相同的单词时。
同时,注释中缺少一些重要信息:例如,什么是“标准化资源名称”,以及
/*
* The amount of blank space to leave on the left and
* right sides of each line of text, in pixels.
*/
private static final int textHorizontalPadding = 4;
该注释提供了从声明本身不明显的其他信息,例如单位(像素)以及填充适用于每行两边的事实。如果读者不熟悉该术语,则注释将解释什么是填充,而不是使用术语“填充”。
低级注释可提高精度
现在您知道了不应该做的事情,让我们讨论应该在注释中添加哪些信息。注释通过提供不同详细程度的信息来增强代码。一些注释提供了比代码更低,更详细的信息。这些注释通过阐明代码的确切含 义来增加精度。其他注释提供了比代码更高,更抽象的信息。这些注释提供了直觉,例如代码背后的推理,或者更简单,更抽象的代码思考方式。与代码处于同一级别的注释可能会重复该代码。本节将更详细地讨论下层方法,而下一节将讨论上层方法。
在注释变量声明(例如类实例变量,方法参数和返回值)时,精度最有用。变量声明中的名称和类型通常不是很精确。注释可以填写缺少的详细信息,例如:
- 此变量的单位是什么?
- 边界条件是包容性还是排他性?
- 如果允许使用空值,则意味着什么?
- 如果变量引用了最终必须释放或关闭的资源,那么谁负责释放或关闭该资源?
- 是否存在某些对于变量始终不变的属性(不变量
) ,例如“此列表始终包含至少一个条目”?
通过检查使用该变量的所有代码,可以潜在地了解某些信息。但是,这很耗时且容易出错。声明的注释应清晰,完整,以免不必要。当我说声明的注释应描述代码中不明显的内容时
// Current offset in resp Buffer
uint32_t offset;
// Contains all line-widths inside the document and
// number of appearances.
private TreeMap<Integer, Integer> lineWidths;
在第一个示例中,尚不清楚“当前”的含义。在第二个示例中,尚不清楚
// Position in this buffer of the first object that hasn't
// been returned to the client.
uint32_t offset;
// Holds statistics about line lengths of the form <length, count>
// where length is the number of characters in a line (including
// the newline), and count is the number of lines with
// exactly that many characters. If there are no lines with
// a particular length, then there is no entry for that length.
private TreeMap<Integer, Integer> numLinesWithLength;
第二个声明使用一个较长的名称来传达更多信息。它还将“宽度”更改为“长度”,因为该术语更可能使人们认为单位是字符而不是像素。请注意,第二条注释不仅记录了每个条目的详细信息,还记录了缺少条目的含义。在记录变量时,请考虑名词而不是动词。换句话说,关注变量代表什么,而不是如何操纵变量。考虑以下注释:
/* FOLLOWER VARIABLE: indicator variable that allows the Receiver and the
* PeriodicTasks thread to communicate about whether a heartbeat has been
* received within the follower's election timeout window.
* Toggled to TRUE when a valid heartbeat is received.
* Toggled to FALSE when the election timeout window is reset. */
private boolean receivedValidHeartbeat;
高级注释可增强直觉
注释可以增加代码的第二种方法是提供直觉。这些注释是在比代码更高的级别上编写的。它们忽略了细节,并帮助读者理解了代码的整体意图和结构。此方法通常用于方法内部的注释以及接口注释。例如,考虑以下代码:
// If there is a LOADING readRpc using the same session
// as PKHash pointed to by assignPos, and the last PKHash
// in that readRPC is smaller than current assigning
// PKHash, then we put assigning PKHash into that readRPC.
int readActiveRpcId = RPC_ID_NOT_ASSIGNED;
for (int i = 0; i < NUM_READ_RPC; i++) {
if (session == readRpc[i].session
&& readRpc[i].status == LOADING
&& readRpc[i].maxPos < assignPos
&& readRpc[i].numHashes < MAX_PKHASHES_PERRPC) {
readActiveRpcId = i;
break;
}
}
该注释太底层和太详细。一方面,它部分重复了代码
// Try to append the current key hash onto an existing
// RPC to the desired server that hasn't been sent yet.
此注释不包含任何详细信息。相反,它在更高级别上描述了代码的整体功能。有了这些高级信息,读者就可以解释代码中几乎发生的所有事情:循环必须遍历所有现有的远程过程调用(RPC
高级别的注释比低级别的注释更难编写,因为您必须以不同的方式考虑代码。问问自己:这段代码要做什么?您能以何种最简单方式来解释代码中的所有内容?这段代码最重要的是什么?工程师往往非常注重细节。我们喜欢细节,善于管理其中的许多细节;这对于成为一名优秀的工程师至关重要。但是,优秀的软件设计师也可以从细节退后一步,从更高层次考虑系统。这意味着要确定系统的哪些方面最重要,并且能够忽略底层细节,仅根据系统的最基本特征来考虑系统。这是抽象的本质(找到一种思考复杂实体的简单方法
这是另一个代码示例,具有较高层次的注释:
if (numProcessedPKHashes < readRpc[i].numHashes) {
// Some of the key hashes couldn't be looked up in
// this request (either because they aren't stored
// on the server, the server crashed, or there
// wasn't enough space in the response message).
// Mark the unprocessed hashes so they will get
// reassigned to new RPCs.
for (size_t p = removePos; p < insertPos; p++) {
if (activeRpcId[p] == i) {
if (numProcessedPKHashes > 0) {
numProcessedPKHashes--;
} else {
if (p < assignPos)
assignPos = p;
activeRpcId[p] = RPC_ID_NOT_ASSIGNED;
}
}
}
}
此注释有两件事。第二句话提供了代码功能的抽象描述。第一句话是不同的:它以高级的方式解释了为什么执行代码
接口文档
注释最重要的作用之一就是定义抽象。抽象是实体的简化视图,它保留了基本信息,但省略了可以安全忽略的细节。代码不适合描述抽象;它的级别太低,它包含实现细节,这些细节在抽象中不应该看到。描述抽象的唯一方法是使用注释。如果您想要呈现良好抽象的代码,则必须用注释记录这些抽象。
记录抽象的第一步是将接口注释与实现注释分开。接口注释提供了使用类或方法时需要知道的信息。他们定义了抽象。实现注释描述了类或方法如何在内部工作以实现抽象。区分这两种注释很重要,这样接口的用户就不会暴露于实现细节。此外,这两种形式最好有所不同。如果接口注释也必须描述实现,则该类或方法很浅。这意味着撰写注释的行为可以提供有关设计质量的线索。
类的接口注释提供了该类提供的抽象的高级描述,例如:
/**
* This class implements a simple server-side interface to the HTTP
* protocol: by using this class, an application can receive HTTP
* requests, process them, and return responses. Each instance of
* this class corresponds to a particular socket used to receive
* requests. The current implementation is single-threaded and
* processes one request at a time.
*/
public class Http {...}
该注释描述了类的整体功能,没有任何实现细节,甚至没有特定方法的细节。它还描述了该类的每个实例代表什么。最后,注释描述了该类的限制(它不支持从多个线程的并发访问
- 注释通常以一两个句子开头,描述调用者感知到的方法的行为。这是更高层次的抽象。
- 注释必须描述每个参数和返回值(如果有
) 。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。 - 如果该方法有任何副作用,则必须在接口注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。例如,如果该方法将一个值添加到内部数据结构中,可以通过将来的方法调用来检索该值,则这是副作用。写入文件系统也是一个副作用。
- 方法的接口注释必须描述该方法可能产生的任何异常。
- 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二进制搜索方法,必须对要搜索的列表进行排序
) 。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。
这是从
/**
* Copy a range of bytes from a buffer to an external location.
*
* \param offset
* Index within the buffer of the first byte to copy.
* \param length
* Number of bytes to copy.
* \param dest
* Where to copy the bytes: must have room for at least
* length bytes.
*
* \return
* The return value is the actual number of bytes copied,
* which may be less than length if the requested range of
* bytes extends past the end of the buffer. 0 is returned
* if there is no overlap between the requested range and
* the actual buffer.
*/
uint32_t
Buffer::copy(uint32_t offset, uint32_t length, void* dest)
...
此注释的语法(例如
对于更扩展的示例,让我们考虑一个称为
query = new IndexLookup(table, index, key1, key2);
while (true) {
object = query.getNext();
if (object == NULL) {
break;
}
... process object ...
}
应用程序首先构造一个类型为
现在,让我们考虑该类的接口注释中需要包含哪些信息。对于下面给出的每条信息,问自己一个开发人员是否需要知道该信息才能使用该类:
IndexLookup 类发送给包含索引和对象的服务器的消息格式。- 用于确定特定对象是否在所需范围内的比较功能(使用整数,浮点数或字符串进行比较吗
? ) 。 - 用于在服务器上存储索引的数据结构。
IndexLookup 是否同时向多个服务器发出多个请求。- 处理服务器崩溃的机制。
这是
/*
* This class implements the client side framework for index range
* lookups. It manages a single LookupIndexKeys RPC and multiple
* IndexedRead RPCs. Client side just includes "IndexLookup.h" in
* its header to use IndexLookup class. Several parameters can be set
* in the config below:
* - The number of concurrent indexedRead RPCs
* - The max number of PKHashes a indexedRead RPC can hold at a time
* - The size of the active PKHashes
*
* To use IndexLookup, the client creates an object of this class by
* providing all necessary information. After construction of
* IndexLookup, client can call getNext() function to move to next
* available object. If getNext() returns NULL, it means we reached
* the last object. Client can use getKey, getKeyLength, getValue,
* and getValueLength to get object data of current object.
*/
class IndexLookup {
...
private:
/// Max number of concurrent indexedRead RPCs
static const uint8_t NUM_READ_RPC = 10;
/// Max number of PKHashes that can be sent in one
/// indexedRead RPC
static const uint32_t MAX_PKHASHES_PERRPC = 256;
/// Max number of PKHashes that activeHashes can
/// hold at once.
static const size_t MAX_NUM_PK = (1 << LG_BUFFER_SIZE);
}
在进一步阅读之前,请先查看您是否可以使用此注释确定问题所在。这是我发现的问题:
- 第一段的大部分与实现有关,而不是接口。举一个例子,用户不需要知道用于与服务器通信的特定远程过程调用的名称。在第一段的后半部分中提到的配置参数都是所有私有变量,它们仅与类的维护者相关,而与类的用户无关。所有这些实现信息都应从注释中省略。
- 该评论还包括一些显而易见的事情。例如,不需要告诉用户包括
IndexLookup.h :任何编写C ++ 代码的人都可以猜测这是必要的。另外, “通过提供所有必要的信息”一词毫无意义,因此可以省略。
对此类的简短注释就足够了(并且更可取
/*
* This class is used by client applications to make range queries
* using indexes. Each instance represents a single range query.
*
* To start a range query, a client creates an instance of this
* class. The client can then call getNext() to retrieve the objects
* in the desired range. For each object returned by getNext(), the
* caller can invoke getKey(), getKeyLength(), getValue(), and
* getValueLength() to get information about that object.
*/
此注释的最后一段不是严格必需的,因为它主要针对单个方法复制了注释中的信息。但是,在类文档中提供示例来说明其方法如何协同工作可能会有所帮助,特别是对于使用模式不明显的深层类尤其如此。注意,新注释未提及
当接口文档(例如方法的文档)描述了不需要使用要记录的事物的实现详细信息时,就会出现此红色标记。现在考虑以下代码,该代码显示
/**
* Check if the next object is RESULT_READY. This function is
* implemented in a DCFT module, each execution of isReady() tries
* to make small progress, and getNext() invokes isReady() in a
* while loop, until isReady() returns true.
*
* isReady() is implemented in a rule-based approach. We check
* different rules by following a particular order, and perform
* certain actions if some rule is satisfied.
*
* \return
* True means the next Object is available. Otherwise, return
* false.
*/
bool IndexLookup::isReady() { ... }
再一次,本文档中的大多数内容,例如对
/*
* Indicates whether an indexed read has made enough progress for
* getNext to return immediately without blocking. In addition, this
* method does most of the real work for indexed reads, so it must
* be invoked (either directly, or indirectly by calling getNext) in
* order for the indexed read to make progress.
*
* \return
* True means that the next invocation of getNext will not block
* (at least one object is available to return, or the end of the
* lookup has been reached); false means getNext may block.
*/
此注释版本提供了有关“就绪”含义的更精确信息,并且提供了重要信息,如果要继续进行索引检索,则必须最终调用此方法。
实现注释:什么以及为什么,而不是如何
实现注释是出现在方法内部的注释,以帮助读者了解它们在内部的工作方式。大多数方法是如此简短,简单,以至于它们不需要任何实现注释:有了代码和接口注释,就很容易弄清楚方法的工作原理。实现注释的主要目的是帮助读者理解代码在做什么(而不是代码如何工作
// Phase 1: Scan active RPCs to see if any have completed.
对于循环,在循环前加一个注释来描述每次迭代中发生的事情是有帮助的:
// Each iteration of the following loop extracts one request from
// the request message, increments the corresponding object, and
// appends a response to the response message.
请注意,此注释如何更抽象和直观地描述循环。它没有详细介绍如何从请求消息中提取请求或对象如何递增。仅对于更长或更复杂的循环才需要循环注释,在这种情况下,循环的作用可能并不明显。许多循环足够短且简单,以至于其行为已经很明显。除了描述代码在做什么之外,实现注释还有助于解释原因。如果代码中有些棘手的方面从阅读中看不出来,则应将它们记录下来。例如,如果一个错误修复程序需要添加目的不是很明显的代码,请添加注释以说明为什么需要该代码。对于错误修复,其中有写得很好的错误报告来描述问题,该注释可以引用错误跟踪数据库中的问题,而不是重复其所有详细信息(“修复
对于更长的方法,为一些最重要的局部变量写注释会很有帮助。但是,如果大多数局部变量具有好名字,则不需要文档。如果变量的所有用法在几行之内都是可见的,则通常无需注释即可轻松理解变量的用途。在这种情况下,可以让读者阅读代码来弄清楚变量的含义。但是,如果在大量代码中使用了该变量,则应考虑添加注释以描述该变量。在记录变量时,应关注变量表示的内容,而不是代码中如何对其进行操作。
跨模块设计决策
在理想环境中,每个重要的设计决策都将封装在一个类中。不幸的是,真实的系统不可避免地最终会影响到多个类的设计决策。例如,网络协议的设计将影响发送方和接收方,并且它们可以在不同的地方实现。跨模块决策通常是复杂而微妙的,并且会导致许多错误,因此,为它们提供良好的文档至关重要。
跨模块文档的最大挑战是找到一个放置它的位置,以便开发人员自然地发现它。有时,放置此类文档的中心位置很明显。例如,
typedef enum Status {
STATUS_OK = 0,
STATUS_UNKNOWN_TABLET = 1,
STATUS_WRONG_VERSION = 2,
...
STATUS_INDEX_DOESNT_EXIST = 29,
STATUS_INVALID_PARAMETER = 30,
STATUS_MAX_VALUE = 30,
// Note: if you add a new status value you must make the following
// additional updates:
// (1) Modify STATUS_MAX_VALUE to have a value equal to the
// largest defined status value, and make sure its definition
// is the last one in the list. STATUS_MAX_VALUE is used
// primarily for testing.
// (2) Add new entries in the tables "messages" and "symbols" in
// Status.cc.
// (3) Add a new exception class to ClientException.h
// (4) Add a new "case" to ClientException::throwException to map
// from the status value to a status-specific ClientException
// subclass.
// (5) In the Java bindings, add a static class for the exception
// to ClientException.java
// (6) Add a case for the status of the exception to throw the
// exception in ClientException.java
// (7) Add the exception to the Status enum in Status.java, making
// sure the status is in the correct position corresponding to
// its status code.
}
新状态值将添加到现有列表的末尾,因此注释也将放置在最有可能出现的末尾。不幸的是,在许多情况下,并没有一个明显的中心位置来放置跨模块文档。
我最近一直在尝试一种方法,该方法将跨模块问题记录在一个名为
...
Zombies
-------
A zombie is a server that is considered dead by the rest of the
cluster; any data stored on the server has been recovered and will
be managed by other servers. However, if a zombie is not actually
dead (e.g., it was just disconnected from the other servers for a
while) two forms of inconsistency can arise:
* A zombie server must not serve read requests once replacement servers have taken over; otherwise it may return stale data that does not reflect writes accepted by the replacement servers.
* The zombie server must not accept write requests once replacement servers have begun replaying its log during recovery; if it does, these writes may be lost (the new values may not be stored on the replacement servers and thus will not be returned by reads).
RAMCloud uses two techniques to neutralize zombies. First,
...
然后,在与这些问题之一相关的任何代码段中,都有一条简短的注释引用了
// See "Zombies" in designNotes.
使用这种方法,文档只有一个副本,因此开发人员在需要时可以相对容易地找到它。但是,这样做的缺点是,文档离它依赖的任何代码段都不近,因此随着系统的发展,可能难以保持最新。