淺談變數型別之外的變數命名

doodlewind發表於2018-12-17

在程式設計實踐中定義變數時,我們所能控制的無非兩點:變數型別與變數名。某種程度上,這兩者分別考驗的其實是開發者的數學水平與語文水平。在今天,即便已經有了非常高大上的型別系統,「名不副實」的變數名仍然經常能對開發者造成困擾。那麼,我們有什麼理論能用來指導變數命名呢?

在電腦科學的萌芽時代,變數名和變數型別之間並沒有明確的界限。比如,著名的「匈牙利命名法」提倡的就是把屬性 + 型別 + 描述一股腦地寫進變數名:例如 const int max 就對應 c_i_max 的名字。程式語言發展到今天,這種實踐看起來自然就很彆扭了。對於現在的主流程式語言,我們大可以認為,它們中的變數名就應該是純粹的「對變數的描述」。

那麼,怎麼樣描述好一個變數呢?這時,我們的關注點其實已經從自然科學中的型別理論轉移到人文科學中的文學創作了 :) 在給變數取名時,我們實際上要做的是言簡意賅地描述清楚一個抽象的東西,這其實並不是件容易的事情——「名字」可是萬惡的自然語言的範疇裡的東西,試問列位能在白板上手寫 bug-free 紅黑樹的大神們,可曾用母語寫出過滿分的高中作文?

所以,自然語言在程式設計中其實發揮著非常重要的作用——畢竟程式碼是寫給人看的。筆者相信,每個正常的閱讀者,都會在潛意識裡用從小耳濡目染的自然語言來理解程式碼邏輯,然後才是套用(也許是培訓班昨天才教的)程式語言語法規則來考量細節。這裡,某些標榜著簡潔但充斥著大量符號的程式語言就可以作為反例:用它們寫出的程式碼也許對於熱愛推導公式的 Geek 來說非常友好,但普通人第一眼看上去就是天書。當然,矯枉過正的例子也不是沒有:對於一些鼓勵超長變數名的語言,維護它們的程式碼……多少會有些捏著鼻子的感覺。

到此,我們已經總結出了變數命名的若干微妙之處:

  • 變數命名與型別系統是分離的。
  • 變數命名依據其實是自然語言。
  • 命名過於簡潔時,可讀性不好。
  • 命名過於冗長時,可讀性也差。

聽起來是不是很主觀,很難量化呢?這就引出了我們的主題:雖然命名難以量化,但你可以依據自然語言的語法,來整理出一些通用的規則呀 :)

語法是筆者高中時代比較反感的東西——你不學它,光憑「語感」也能中個八九不離十;你學了它,反而可能稀裡糊塗套錯場景……不過,語法其實很像自然語言的型別系統,它與程式語言中相應的概念有著很微妙的聯絡。比如,JavaScript 中的關鍵字,就可以根據語法中的詞性來這樣粗略地分類:

名詞
function var class

動詞
import export extends return break continue
delete switch new try catch throw yield

介詞
for in else

連詞
if while

代詞
this
複製程式碼

是否能夠注意到動詞明顯地比名詞更多?而且,程式語言中還有不少介詞 / 連詞這類的虛詞來表達控制流。相比之下,HTML 則幾乎就是名詞的天下:吃我 head body form button 啦!HTML 無非是一堆名詞的堆疊巢狀,相比存在著複雜邏輯的程式語言來說,較少遇到與命名相關的維護性問題。那麼,在維護程式語言的程式碼時,有哪些自然語言的規則能夠有助於提高可讀性和可維護性呢?這裡筆者總結了這麼幾點:

  • 注意命名與型別在語義上的匹配
  • 保持概念的一致性
  • 避免製造出反語言直覺的結構
  • 慎用寬泛的抽象概念

讓我們逐條看一下吧 :-D

注意命名與型別在語義上的匹配

前面我們已經知道,詞彙的詞性和變數的型別有著一些微妙的關係。在命名時,保持這種關係的匹配有利於增強可讀性。注意,這並不是像匈牙利命名法那樣把型別寫入變數名,而是根據型別來選擇更契合的變數名:

  • 對於布林型別的變數,其命名常用形如 isSomething / hasSomething 的形式。這其實非常接近自然語言裡的陳述句——陳述句有著肯定和否定的形式,這就暗合了布林值的 truefalse
  • 對於陣列型別的變數,其命名常用複數形式。例如 apples 在自然語言裡就暗示著有不止一個 apple,這和陣列的思維模型是非常契合的。
  • 對於物件型別的變數,其命名常用名詞性短語。例如 UserModel 這樣的 class。別忘了,名詞和動詞都屬於實詞,不過二者一個更加「物件導向」,一個更加「函式式」罷了。
  • 對於函式型別的變數,其命名常用動詞,或者說更接近祈使句式。一個函式就應該明確地去「做某件事」,這時候「說什麼就做什麼」顯然更加簡單真誠。比如,你可以嘗試給你的類方法前面都加上個 Please,這時候 getUserData() 就能無縫地變成通順的 Please get user data 的祈使句了。當然,在函數語言程式設計中,面向函式耍出的各種花樣,還可以用各種虛詞來粉飾。譬如像 withState 這樣的名字,其中就暗含著對引數的「修飾」能力。再有各類回撥的場景,也常見 on / before / after 這樣的介詞來表明相應動作的觸發時機。

不同的程式語言和正規化,在詞彙的選用上會有非常鮮明的區別。比如 Java 就是個名詞王國,而函式一等公民的語言裡,動詞的地位就高得多。這裡的風格建議是在什麼地方就按什麼地方的規矩來說話,這樣到哪都吃得開 XD

保持概念的一致性

在有了 Lint 等格式化工具之後,專案裡的換行縮排一般都能得到統一,這確實能消除程式碼格式排版上的潦草感覺。但是,許多專案裡仍然很常見這樣的地方:

  • 三個類似的操作分別封裝成 loadUser / fetchUser / getUser 三個不同的函式,它們好像都是一回事,可是你敢隨便換著用嗎?
  • 上面一個 elements.forEach(element),下面就是 elements.forEach(elem)。一個 index 也可以有 i / idx / inx / ind 四種不同的縮寫 :)
  • 現有模組中私有變數按照形如 $xxx 的格式宣告,新程式碼上來就是個 __xxx

其實這些問題單獨拿出來,都很難稱之為嚴重,相應的解決方案應該也都是老生常談的了。但如果這樣的不一致性遍佈專案的程式碼庫,那麼再工整的空格縮排,也掩蓋不了程式碼的雜亂感。這裡的一個非技術建議是建立 Code Review 機制:這些問題很難單純通過 Lint 工具來約束,提出 Review 意見與修改也並不難,關鍵在於流程:

  • 新人很難一上來就瞭解各種隱式的約定,許多老成員輕車熟路的實踐,對於新人來說反而是個不熟悉的暗坑。
  • 一旦程式碼併入主幹,再去修改它的成本就會顯著地提高——老程式碼跑得好好的,重構壞了誰負責?

當然了,Code Review 還有許多其他的功效,這裡就不再展開啦。

避免製造出反語言直覺的結構

我們已經提到過,直覺在編碼中發揮著潛移默化的作用。那麼,什麼叫做「反直覺」呢?譬如這麼些小地方:

  • 雙重否定表示肯定,那麼我們就應該放心地用 2N 重的否定來表示肯定嗎?機器當然能正確地理解 if (!dataNotLoaded !== false) 的精妙邏輯,不過這時候恐怕很容易把自己和別人繞進去,尤其是在與或條件多起來的時候 :-p
  • 把資料「封裝一層」是很常見的手法,不過這時候如果出現了 data.data,該怎麼確定其它程式碼中用到的 data 變數指的是誰呢?
  • 深度巢狀和花式跳轉等形式的控制流也是反直覺的,但是這已經超過變數命名的範疇了 XD

慎用寬泛的抽象概念

得益於自然語言中豐富的抽象概念,總有一些變數名是非常「方便偷懶」的。比如,如果你實在想不通一個裝資料的變數該怎麼命名,那就叫它 data 啊!如果一個基類不知道叫什麼好,那就叫它 Base 唄!

其實,如果是因為「沒有想清楚該起什麼名字」而使用了這樣寬泛的概念,實際上就會在暗中欠下「沒有清晰正確的設計」的技術債。比如上面的行為,在擴充套件或重構時很可能遇到這樣的問題:

  • 到處都是 data,以至於不僅沒法簡單地查詢與替換來重構,依賴 IDE 來改個變數名都要提心吊膽,不確定影響範圍。
  • 各種形如 core / base / common 的模組,難以界定邊界在哪:core 到底管些什麼事,繼承它到底會帶來哪些副作用,而我的新函式要不要放在裡面呢?

對於裝資料的變數來說,data / item 這樣的名字寫了和沒寫差不多——當然了,對於可以多處複用的工具函式,這樣的命名完全沒有問題。相對地,對於函式來說,各種具體的動作就比較容易描述清楚,不過你如果非要把所有的回撥名一律寫成 callback,那也不是不可以……

其實上面的這些問題,很多隻要在提交前 double check 一遍流程,就能夠在自省中發現。這也是一個非常重要的技能:找出自己哪裡做得還不夠好,本身就是一種進步了。

總結

對各種概念的抽象過程常常是程式設計中最有樂趣的地方之一,而好的變數命名無疑有助於將思維過程更清晰地表達出來。在本文提出的觀點裡,變數的命名之難,與型別的強弱和型別系統的動態靜態並沒有直接的關係:數學大師的語文未必就足夠好,反之亦然。我們也基於一些自然語言裡非常簡單的規則來提出了一些對編碼實踐的建議。不過,如果下次遇到 diss 你變數命名的 review 建議的時候,本文也可以為你提供一個有力的反擊論據:

我們連程式語言的型別系統都很難完全搞明白,更何況自然語言的呢?

相關文章