Redis是如何寫(xiě)代碼注釋的?
許多人認(rèn)為,如果代碼寫(xiě)得足夠扎實(shí),注釋就沒(méi)什么用了。在他們看來(lái),當(dāng)一切都設(shè)計(jì)妥當(dāng)時(shí),代碼本身會(huì)記錄其作用,因此代碼注釋是多余的。我對(duì)此持不同意見(jiàn),主要出于兩個(gè)原因:
1、許多注釋并未起到解釋代碼的作用。
2、注釋使讀者不必憑空想象太多細(xì)枝末節(jié),幫助讀者降低認(rèn)知負(fù)擔(dān)。
注釋的分類(lèi)
我的工作始于隨機(jī)地閱讀Redis源代碼,以檢查注釋是否以及為什么在不同的上下文中起作用。我很快發(fā)現(xiàn),注釋的作用來(lái)源于多方面:它們?cè)诠δ?,編程風(fēng)格,長(zhǎng)度和更新頻率方面往往非常不同。我最終轉(zhuǎn)向了注釋分類(lèi)。
在研究期間,我確定了九種注釋類(lèi)別:
* 函數(shù)注釋 Function comments
* 設(shè)計(jì)注釋 Design comments
* 原因注釋 Why comments
* 教學(xué)注釋 Teacher comments
* 清單注釋 Checklist comments
* 引導(dǎo)注釋 Guide comments
* 瑣碎注釋 Trivial comments
* (代碼)負(fù)債注釋 Debt comments
* 備份注釋 Backup comments
在我看來(lái),前六個(gè)主要是非常積極的注釋形式,而***三個(gè)有點(diǎn)值得懷疑。在接下來(lái)的部分中,我將使用Redis源代碼中的示例分析每種注釋類(lèi)型。
函數(shù)注釋
函數(shù)注釋的目標(biāo)是防止讀者直接閱讀代碼。
在閱讀注釋之后,讀者應(yīng)該可以將一些代碼視為應(yīng)遵守某些規(guī)則的黑箱子。通常情況下,函數(shù)注釋位于函數(shù)定義的頂部。
rax.c:
- / * 在當(dāng)前節(jié)點(diǎn)的子樹(shù)中尋找***的key。
- 如果內(nèi)存不足返回0,否則 返回1. * /
- int raxSeekGreatest(raxIterator * it){
- ...
- }
函數(shù)注釋實(shí)際上是一種內(nèi)聯(lián)API文檔。如果函數(shù)注釋編寫(xiě)得好,那么用戶(hù)在大多數(shù)時(shí)候能跳回到她正在閱讀的內(nèi)容(如閱讀調(diào)用此類(lèi)API的代碼),而無(wú)需閱讀函數(shù)(function),類(lèi)(class),宏(macro)等的實(shí)現(xiàn)過(guò)程。
在所有注釋類(lèi)型中,函數(shù)注釋被整個(gè)編程界廣泛接受和需要。要分析的唯一一點(diǎn)是:在代碼內(nèi)部放置以API參考文檔為主的注釋是否是件好事。
對(duì)我來(lái)說(shuō)答案很簡(jiǎn)單:我希望API文檔與代碼完全匹配。隨著代碼的更改,文檔也得到更改。出于這個(gè)原因,我們將函數(shù)注釋用作函數(shù)或其他元素的序言,使API文檔接近代碼,完成三個(gè)任務(wù):
* 隨著代碼的更改,我們可以輕松更改文檔,API參考也不會(huì)有過(guò)時(shí)的風(fēng)險(xiǎn)。
* 這種方法使得更改者(理應(yīng)是最清楚更改目的的人)在***限度上成為API文檔的更改者。
* 讀者能通過(guò)閱讀代碼直接找到函數(shù)或方法(method)的文檔,以便閱讀代碼的讀者只關(guān)注代碼,而不是代碼和文檔之間的上下文切換。
設(shè)計(jì)注釋
“函數(shù)注釋”通常位于函數(shù)的開(kāi)頭,而設(shè)計(jì)注釋通常位于文件的開(kāi)頭。
設(shè)計(jì)注釋一般說(shuō)明了給定代碼片段使用某些算法、技術(shù)、技巧和具體實(shí)現(xiàn)的方式和原因,對(duì)代碼中實(shí)現(xiàn)的內(nèi)容進(jìn)行了更高級(jí)別的概述。在這樣的背景下,閱讀代碼會(huì)更簡(jiǎn)單一些。
bio.c
- *設(shè)計(jì)
- * ------
- *
- *設(shè)計(jì)很簡(jiǎn)單,我們用一個(gè)結(jié)構(gòu)代表要執(zhí)行的一項(xiàng) Job
- *每種Job類(lèi)型有不同的線程和Job隊(duì)列。
- *每個(gè)線程都在等待隊(duì)列中的新Job,并按照順序處理
- *每個(gè)Job。
- ...
原因注釋
原因注釋解釋了代碼執(zhí)行某些操作的原因——即使代碼執(zhí)行的操作非常明確。請(qǐng)看以下來(lái)自Redis replication的代碼 的示例。
replication.c:
- if(idle> server.repl_backlog_time_limit){
- /* 當(dāng)我們釋放 backlog時(shí),我們總是使用新的
- * replication ID并清除ID2。這是
- * 因?yàn)樵跊](méi)有backlog時(shí),master_repl_offset
- * 未更新,但我們?nèi)詴?huì)保留我們的
- * replication ID,由此導(dǎo)致以下問(wèn)題:
- *
- * 1.我們是一個(gè)主實(shí)例(master instance)。
- * 2.我們的副本成為主服務(wù)器(Master)。repl-id-2將會(huì)
- * 與我們的repl-id相同。
- * 3.我們作為主服務(wù)器,收到了一些更新命令,但不會(huì)
- * 增加master_repl_offset。
- * 4.稍后我們將變成副本,連接到新的
- * 主服務(wù)器,它將接受我們第二個(gè)副本ID的
- * PSYNC請(qǐng)求,但會(huì)有數(shù)據(jù)不一致的情況
- * 因?yàn)槲覀兘邮芰藢?xiě)命令。* /
- changeReplicationId();
- clearReplicationId2();
- freeReplicationBacklog();
- serverLog(LL_NOTICE,
- "Replication backlog freed after %d seconds "
- "without connected replicas.",
- (int) server.repl_backlog_time_limit);
- }
如果我只檢查函數(shù)調(diào)用,就沒(méi)什么需要糾結(jié)的:如果超時(shí)了就更改主replication ID,清除輔助ID,***釋放replication backlog。
教學(xué)注釋
教學(xué)注釋不會(huì)試圖解釋代碼本身或我們應(yīng)該注意的某些副作用。教學(xué)注釋教授的是代碼運(yùn)行的“領(lǐng)域”(例如數(shù)學(xué),計(jì)算機(jī)圖形學(xué),網(wǎng)絡(luò)系統(tǒng),統(tǒng)計(jì),復(fù)雜的數(shù)據(jù)結(jié)構(gòu)等),這些信息可能超出了讀者的認(rèn)知范圍,或者細(xì)節(jié)多到難以回憶。
版本5中的LOLWUT命令需要在屏幕上顯示旋轉(zhuǎn)的方塊。為了做到這一點(diǎn),它使用了一些基本的三角函數(shù):盡管涉及的數(shù)學(xué)內(nèi)容很簡(jiǎn)單,但許多閱讀Redis源代碼的程序員可能沒(méi)有任何數(shù)學(xué)背景知識(shí),因此函數(shù)頂部的注釋解釋了該函數(shù)的原理。
- /*
- * 繪制一個(gè)以指定的x,y坐標(biāo)為中心的正方形
- * 旋轉(zhuǎn)角度和大小已定。為了寫(xiě)出旋轉(zhuǎn)方塊的代碼,我們使用了
- * 參數(shù)方程:
- *
- * x = sin(k)
- * y = cos(k)
- *
- * 繪制一個(gè)圓(0-2*PI)。然后,如果我們從45度
- * 開(kāi)始,即k = PI / 4,以此作為***個(gè)點(diǎn),然后我們發(fā)現(xiàn)
- * 其他三個(gè)點(diǎn)的K值以PI / 2(90度)遞增,于是我們得到
- * 了構(gòu)成一個(gè)圓的點(diǎn)。為了旋轉(zhuǎn)方塊,我們從
- * k = PI / 4 + rotation_angle開(kāi)始,然后我們就完事兒了。
- * ......
- * /
注釋不包含任何與函數(shù)本身的代碼,或其副作用,或與函數(shù)相關(guān)的技術(shù)細(xì)節(jié)等內(nèi)容。注釋描述的部分僅限于函數(shù)內(nèi)部使用以達(dá)到給定目標(biāo)的數(shù)學(xué)概念。
清單注釋
這是一個(gè)非常常見(jiàn)且奇怪的問(wèn)題:有時(shí)由于語(yǔ)言限制,設(shè)計(jì)問(wèn)題,或者僅僅因?yàn)橄到y(tǒng)內(nèi)部固有的復(fù)雜性,我們無(wú)法將某個(gè)概念或界面集中在一個(gè)代碼片段中,因此代碼中有一些部分能提醒你在代碼的某個(gè)部分做某件事。一般概念是:
- / * 警告:如果你在此處添加類(lèi)型ID,請(qǐng)務(wù)必修改
- * getTypeNameByID()函數(shù)。* /
在一個(gè)***世界中,我們永遠(yuǎn)不需要添加這類(lèi)注釋;但在實(shí)踐中有時(shí)沒(méi)法省略這一步。
在這種情況下,防御性注釋有時(shí)能起作用:如果你修改了某節(jié)代碼,它會(huì)提醒你修改代碼的其他相關(guān)部分。具體而言,清單注釋會(huì)發(fā)揮以下一種作用(或者兩種兼而有之):
* 告訴你在修改某些內(nèi)容時(shí)要執(zhí)行的一系列操作。
* 警告你應(yīng)該如何進(jìn)行某些更改。
引導(dǎo)注釋
我濫用引導(dǎo)注釋到這種程度:Redis中的大多數(shù)注釋都是引導(dǎo)注釋。然而,引導(dǎo)注釋正是大多數(shù)人認(rèn)知中那類(lèi)完全無(wú)用的注釋?zhuān)?/p>
* 他們沒(méi)有說(shuō)明代碼中不甚明了的內(nèi)容。
* 指導(dǎo)注釋不提供有關(guān)設(shè)計(jì)方面的提示。
引導(dǎo)注釋只做了一件事:他們照顧了讀者的需求,在讀者處理源代碼中的內(nèi)容時(shí)提供明確的劃分(division)和節(jié)奏(rhythm),并介紹接下來(lái)需要閱讀的內(nèi)容。
rax.c
- / *調(diào)用節(jié)點(diǎn)回調(diào)(如果有的話),如果回調(diào)返回true
- *則替換節(jié)點(diǎn)指標(biāo)* /
- if (it->node_cb && it->node_cb(&it->node))
- memcpy(cp,&it->node,sizeof(it->node));
- /*對(duì)于“下一步”,每次找到一個(gè)鍵就停止
- *一次,因?yàn)橄啾容^后面子節(jié)點(diǎn)分支中的內(nèi)容
- *鍵本身字典序較小。* /
- if (it->node->iskey) {
- it->data = raxGetData(it->node);
- return 1;
- }
Redis內(nèi)“實(shí)際上”充滿(mǎn)了引導(dǎo)注釋?zhuān)曰旧夏愦蜷_(kāi)的每個(gè)文件都會(huì)包含很多引導(dǎo)注釋。為什么要費(fèi)這個(gè)力氣呢?在這篇博客文章中所分析的所有注釋類(lèi)型中,這絕對(duì)是最主觀的一種。我并不覺(jué)得沒(méi)有引導(dǎo)注釋的代碼就不是好代碼。但我堅(jiān)信,如果人們認(rèn)為Redis代碼是可讀的,部分原因就在于其中的引導(dǎo)注釋。
引導(dǎo)注釋還有一些別的用處。因?yàn)樗鼈兠鞔_地將代碼劃分為獨(dú)立的部分,所以我們能在合適的位置插入新代碼,而不是隨便加在其他代碼后面。在代碼附近設(shè)置相關(guān)語(yǔ)句能大大提高可讀性。
引導(dǎo)注釋能簡(jiǎn)要地告訴讀者函數(shù)將要執(zhí)行什么操作,所以如果你只對(duì)大框架感興趣,則無(wú)需回過(guò)頭去閱讀函數(shù)。
瑣碎注釋
引導(dǎo)注釋是非常主觀的工具。不管你喜不喜歡,我反正超愛(ài)引導(dǎo)注釋。
然而,引導(dǎo)注釋可能會(huì)退化為極其糟糕的注釋?zhuān)核苋菀鬃兂?ldquo;瑣碎注釋”(trivial comment)。
瑣碎注釋這種引導(dǎo)注釋所帶來(lái)的認(rèn)知負(fù)荷和僅閱讀相關(guān)代碼比起來(lái)相差無(wú)幾,甚至可能更高。以下這種瑣碎注釋正是許多書(shū)籍規(guī)勸你避免的。
array_len ++; / *增加數(shù)組的長(zhǎng)度。* /
因此,如果你寫(xiě)引導(dǎo)注釋的話,請(qǐng)避免寫(xiě)瑣碎注釋。
(代碼)負(fù)債注釋
負(fù)債注釋是源代碼內(nèi)部硬編碼的技術(shù)債務(wù)語(yǔ)句:
- entries -= to_delete;
- marked_deleted += to_delete;
- if (entries + marked_deleted > 10 && marked_deleted > entries/2) {
- / * TODO:執(zhí)行垃圾收集操作。* /
- }
FIXME,TODO,XXX,“這是一個(gè)黑客”,這些都是負(fù)債注釋。總的來(lái)說(shuō)這些注釋不算好,我試圖避免使用它們,但看起來(lái)不太可能。有時(shí)候,比起永遠(yuǎn)忘記一個(gè)問(wèn)題,我更喜歡在源代碼中放置一個(gè)節(jié)點(diǎn)。程序員至少應(yīng)該定期查看這些注釋?zhuān)纯词欠窨梢阅芨倪M(jìn)一下表述,或者這些問(wèn)題是否已不再相關(guān)或可以立即解決。
備份注釋
備份注釋是開(kāi)發(fā)人員對(duì)某些代碼塊的舊版本甚至是整個(gè)函數(shù)做出的注釋?zhuān)驗(yàn)樗?她對(duì)新版本中運(yùn)行的更改放不下心。令人費(fèi)解的是,現(xiàn)在有了Git,人們卻還在使用這類(lèi)注釋。我想人們對(duì)于丟失代碼片段有一種不安全感,過(guò)去提交代碼時(shí),使用備份注釋會(huì)顯得更加理智可靠。
但源代碼并不是用來(lái)備份的。如果你需要保存舊版本的函數(shù)或代碼,說(shuō)明你的工作尚未完成,也無(wú)法提交。要么確保新函數(shù)比過(guò)去的更好,要么只在開(kāi)發(fā)樹(shù)(development tree)中使用它,直到你確定為止。
備份注釋是我分類(lèi)中的***一項(xiàng)。我們來(lái)做個(gè)總結(jié)。
總結(jié)
注釋是和未來(lái)的代碼讀者聊天,讀者們還能在Twitter上評(píng)價(jià)你的注釋。所以在這個(gè)過(guò)程中,你真心地在審視自己所注釋的內(nèi)容是否“能讓人接受”,看自己寫(xiě)得是否足夠體面、足夠好。如果不是,你就勤勤懇懇地再做一遍,拿出更好的注釋來(lái)。
你可能認(rèn)為編寫(xiě)注釋不是個(gè)高端工作。畢竟你“會(huì)寫(xiě)代碼”!但請(qǐng)考慮這一點(diǎn):代碼是一組語(yǔ)句和函數(shù)調(diào)用(或者你做的其他編程范例也一樣)。如果代碼寫(xiě)得不好,這些語(yǔ)句就沒(méi)有多大意義。注釋常常要求你進(jìn)行一些設(shè)計(jì)過(guò)程,并從更深層次來(lái)理解你正在編寫(xiě)的代碼。
最重要的是,為了寫(xiě)出好的注釋?zhuān)惚仨毰囵B(yǎng)自己的寫(xiě)作能力。這種寫(xiě)作技巧能幫你更好地編寫(xiě)電子郵件、文案、設(shè)計(jì)文檔、博客文章和提交文件。
我寫(xiě)代碼是因?yàn)槲移惹邢胍c他人溝通交流、分享想法。注釋能夠?yàn)榇a提供幫助,把作者的心血表現(xiàn)出來(lái)。說(shuō)到底,我喜歡寫(xiě)注釋?zhuān)拖裎蚁矚g寫(xiě)代碼一樣。
注:英文原文太長(zhǎng),翻譯后有刪減。