你寫註釋嗎?寫你就輸了

本文最初發佈於Level Up Coding官方博客,經原作者授權由InfoQ中文站翻譯並分享。

我並不是提倡不寫代碼註釋,只是建議不要過於依賴註釋,這樣可以使代碼更乾淨、更有表現力,這也能提高開發人員的水平。我自己也在尋求編寫更簡潔的代碼,我盡力不編寫糟糕的註釋,並在可能時重構代碼。

這篇文章的標題可能會讓你情緒激動,但請先耐心聽我說完。在適當的位置寫下適當的註釋可能非常有用,但是沒有什麼比無用的註釋更讓代碼混亂了。在某些情況下,我敢說,註釋可以彌補我們在代碼中沒有完全表達出來的意思。因此,寫註釋不值得讚美,而是應該停下來問問自己,是否有更好的方式可以用代碼來表達自己。

帶有少量註釋的清晰而富於表現力的代碼,要比帶有大量註釋的混亂而複雜的代碼好得多。如果你已經把代碼弄得一團糟,不要花時間寫註釋來解釋,而是要花時間梳理代碼。如果每次寫註釋的時候,你都冥思苦想,覺得自己的表達能力不足,那麼最終你就會寫出簡潔明瞭的代碼,完全沒有必要寫註釋。鼓勵自己用代碼表達。

爲什麼對註釋如此不屑?

因爲它們會說謊,還會把代碼弄得亂七八糟。雖說並非總是如此,也並非有意如此,但卻經常如此。糟糕的代碼和帶有大量註釋的代碼之間有很高的相關性。註釋存在的時間越久就越容易偏離它們所描述的代碼——在某些情況下,它們可能是完全錯誤的。實際上,隨着代碼庫和團隊的增長,維護註釋成了不可能的事情。

註釋不同於《辛德勒的名單》。它們不是“純善的”。事實上,註釋充其量是一種必要的惡。——Robert C.Martin

當談論關於註釋的話題時,很重要的一點是,我們要看一下什麼是恰當的註釋,什麼是糟糕的註釋,這樣我們才能學會寫更好的註釋,或者完全避免註釋。

恰當的註釋

並不是所有的註釋都是不好的——有些註釋實際上非常必要。

出於法律目的的註釋

有時候,你可能需要出於法律目的編寫特定的註釋,比如開源項目的創作許可。一些現代化的IDE和文本編輯器會自動將它們摺疊起來,保持工作區的整潔。

魔術表達式

如果你有一個複雜的SQL或正則表達式,它以神奇的方式做了一些令人興奮的事,那麼請務必註釋,以便讓讀者更容易理解,因爲我們都不是Regex忍者。

// 匹配電子郵件地址的正則表達式
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
// 注意:添加一個富於表現力的函數名,註釋就變得沒有必要了
function validateEmail(email) { 
    var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}

說明意圖

在某些情況下,註釋有助於解釋決策或特定解決方案背後的意圖。例如,測試套件中的一條註釋告訴我們添加這行代碼是爲了降低死鎖的機率。

for x in range(1, 500):
  
  # 這是運行多個並行測試時預防死鎖的最好方法
  time.sleep(0.5)
  runTest(x)

結果預警

用註釋說明代碼可能會產生嚴重的或可怕的後果,甚至鼓勵這樣做。在本例中,開發人員讓讀者知道,當與回調函數一起使用時,QT函數不是線程安全的。一般來說,如果一條註釋可以避免某個人在編程時陷入絕望,那麼它就是有用的。

"""
  許多Qt函數都不是線程安全的。如果你使用回調函數,
  即使你在所有繪製調用代碼的周圍都加上鎖,你也會遇到段錯誤,
  因爲Qt的主事件循環仍在運行,並且使用了沒加鎖的資源。
"""
from multiprocessing.pool import ThreadPool
import sys
from threading import Lock
import time
from PyQt5 import QtCore, QtWidgets
class Task(QtCore.QObject):
    updated = QtCore.pyqtSignal(int, int)
    ...............
    ...............

TODO註釋

這些註釋可以幫助我們標記那些我們認爲應該做,但是由於某些原因沒有做到的事情。它可能會提醒你刪除廢棄的特性,或者請求其他人查看某個問題。它可能是要求其他人想一個更好的名字,或者是提醒他們根據計劃事件做出修改。

請記住,TODO註釋不是在系統中留下糟糕代碼的藉口。本質上,每一行代碼都是一種負擔——最安全、最快的代碼是根本沒有代碼。

現在,大多數優秀的IDE都提供了特殊的指令和特性來定位所有的TODO註釋,所以不太可能漏掉它們。儘管如此,你也不希望代碼中到處都是TODO。所以要經常瀏覽一下,刪除那些你能刪除的。

糟糕的註釋

這個清單比較長,但在本節中,我們將看到一些更爲老生常談而又隨處可見的註釋。

明知故問的註釋

有些註釋的意思顯而易見,即它們沒有增加任何實際的價值,而且大多是噪音。

下面是一個開源項目的代碼片段,其中包含大量明知故問型的註釋,這些註釋使代碼變得混亂而晦澀。它們所提供的信息並不比代碼本身多,而且在某些情況下,閱讀註釋的時間甚至比閱讀代碼長。

/**
* 與該容器相關的集羣
*/
protected Cluster cluster = null;
/**
* 人類可讀的容器名 
*/
protected String name = null;
/**
* 該容器的父容器
*/
protected Container parent = null;
/**
* 創建一個Loader配置父類加載器
*/
protected ClassLoader parentClassLoader = null;

不清不楚的註釋

如果你寫註釋是爲了符合公司規定,或者你只是覺得有必要添加一些註釋,那麼你在註釋時就不會進行適當的思考。所以,如果你真的寫了一條註釋,花點時間讓它對閱讀它的人有所幫助。

def load_config():
  try:
    do_useful_stuff()
  except Exception as ex: 
    # 如有異常,退回到默認狀態。

在這個例子中,作者想要傳達一些有關異常情況的重要信息。但這條註釋沒能解釋清楚我們將退回到什麼樣的默認狀態。如果一條註釋要求我們轉到另一個模塊來找出默認值,那麼它就沒有發揮應有的作用。

註釋掉代碼

在團隊準備好刪除代碼之前先將其註釋掉似乎是一個好主意,但是不要這樣做。註釋代碼是一種弊端,團隊中的其他成員不會刪除它,因爲他們會認爲它很重要。我們不是都在使用源碼控制嗎?所以我們不需要保留舊的代碼。我們可以跳到任何我們想要的版本。

噪音註釋

有些註釋毫無意義,純粹是噪音。時間久了,我們的大腦就會走馬觀花,我們也會開始跳過那些需要注意的重要註釋。考慮一下下面的例子,其中的註釋提供了很多價值嗎?

-----------------------------
# Exhibit A
# 默認構造函數
def get_todays_date():
  return date.today()
-----------------------------
# Exhibit B
# 返回月份的天
# @return: 月份的天
def get_day_of_month()
  return day_of_month

用編寫乾淨代碼的決心取代製造噪音的誘惑,你將成爲一個更好、更快樂的程序員。

強制性註釋

這肯定會引起爭議。如果規定每個函數都需要一個Java文檔或Python docstring,是不是有點傻?大多數時候,類或函數名已經告訴我們註釋所描述的內容,它們是多餘的。在這個例子中,註釋的數量比代碼的數量還多——這讓我很惱火。

class ComplexNumber: 
    """ 
    這是一個用於複數的數學運算類。
      
    屬性:
        real (int):複數的實部。
        imag (int):複數的虛部。
    """
  
    def __init__(self, real, imag): 
        """ 
        ComplexNumber類的構造函數。
  
        參數:
           real (int):複數的實部。
           imag (int):複數的虛部。  
        """
  
    def add(self, num): 
        """ 
        該函數用於複數求和。
  
        參數:
            num (ComplexNumber):要加的複數。
          
        返回值:
            ComplexNumber:包含和的複數。
        """
  
        re = self.real + num.real 
        im = self.imag + num.imag 
  
        return ComplexNumber(re, im) 
  
help(ComplexNumber)  #訪問類的docstring 
help(ComplexNumber.add)  # 訪問方法的docstring 

使用好的函數名或變量名

你可以使用更具表達性的函數和變量名替換註釋,從而使代碼更簡潔。考慮下面的例子,第一個例子中的註釋就變得沒有必要了,因爲有一個更好的函數名可以準確地告訴讀者這個函數做了什麼。

# 檢查日期是否是過去的日期
def check_date(date):
   if date < today:
     return true 
   return false 

def is_past_date(date):
   if date < today:
     return true 
   return false

註釋不能彌補代碼的糟糕

編寫註釋有一個比較常見的原因是糟糕的代碼。我們以前都見過這種情況,在某種程度上,我們自己也犯過這樣的錯誤。我們寫一個模塊或類,我們心裏知道它混亂而無序。我們知道它一團糟。所以我們對自己說,“哦,我最好加下注釋!”不!你最好把代碼梳理清楚!

/* 
這段代碼糟透了。我知道,你知道,每個人都知道。
我們假裝什麼都沒發生,然後繼續前進。以後你叫我白癡好了。
*/

小結

我並不是提倡不寫代碼註釋,只是建議不要過於依賴註釋,這樣可以使代碼更乾淨、更有表現力,這也能提高開發人員的水平。我自己也在尋求編寫更簡潔的代碼,我盡力不編寫糟糕的註釋,並在可能時重構代碼——將我的代碼從宜家的一幅畫變成梵高的作品。

所以讓我們約法三章,不要寫這麼多註釋。

查看英文原文:Every time you comment code — you’ve already failed.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章