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