變數名很有用
軟體是寫給人來理解的;因此要合理地選擇變數名。別人需要梳理你的程式碼,並且去理解程式碼的意圖,才能夠擴充套件或者修復。既浪費了空間又不直接明瞭的變數名很多。即使用心良苦,很多工程師最後選的變數名最多也只是徒有其表。這篇文章目的就是幫助工程師如何選取好的變數名。我們側重於程式碼評審,因為在程式碼評審中最容易暴露出糟糕變數名的問題。當然,使用好的變數名還有其他很多原因(比如提高程式碼可維護性)。
為什麼要命名變數
給變數一個有意義的命名主要原因就是能夠讓人理解這些變數。如果僅僅只是寫給機器的話,那麼那些自動生成並且沒有任何意義的變數也無妨,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int f1(int a1, Collection<Integer> a2) { int a5 = 0; for (int a3 = 0; a3 < a2.size() && a3 < a1; a3++) { int a6 = a2.get(a3); if (a6 >= 0) { System.out.println(a6 + " "); } else { a5++; } } System.out.println("n"); return a5; } |
任何一位工程師都看得出上面的程式碼很難理解,因為它違背了兩條準則:1)禁止簡寫;2)賦予有意義的變數名。不過話說回來,這些準則也不一定總是有用的。縮寫也不一定就是不好,之後我們會討論到。“有意義”這個概念有些模糊,而且取決於不同人的解釋。有些工程師認為冗長的就是有意義的(例如MultiDictionaryLanguageProcessorOutput)。有些人則發現理解所謂”真正的“有意義是很困難的,所以在做努力之前就已經放棄了。因此,在儘量滿足上述兩條準則下,寫出的程式碼可能如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int processElements(int numResults, Collection<Integer> collection) { int result = 0; for (int count = 0; count < collection.size() && count < numResults; count++) { int num = collection.get(count); if (num >= 0) { System.out.println(num + " "); } else { result++; } } System.out.println("n"); return result; } |
比起第一個例子來,評審人員更容易理解上述程式碼。變數名很準確而且可讀性強。但實際上卻沒有任何的幫助並且浪費空間,因為:
processElements
幾乎所有的程式碼都是在“處理”事物(畢竟,程式碼的作用都是“processor”),所以process這個單詞其實就是七個沒意義的字母,僅僅只是表示“計算”而已。Elements這個詞也沒有好到哪裡去。很顯然這個函式是要在集合上進行操作。而且使用這個函式名也不能幫助讀者找出bug。
numResults
大多數程式碼都會產生“結果”(最終都會);所以就像process一樣,Results也是七個沒意義的字母。完整的變數名,numResults給人感覺像是要限制輸出的數量,但是又太含糊讓讀者很傷腦筋。
collection
浪費空間;很顯然這是個集合,因為之前的型別申明就是Collection<Integer>.
num
僅僅就表達這是int型別
result, count
這兩個就是編碼時的陳腔濫調了;就如numResults一樣,它們既浪費了空間也過於空泛,並沒有提供幫助來讓讀者理解這段程式碼。
然而,我們需要牢記變數名的真正用意:讀者需要理解程式碼,這就需要達到以下兩點:
- 程式設計師的意圖是什麼?
- 這段程式碼到底是在做什麼?
來看一個長變數名的例子是怎麼給讀者增加精神負擔的,下面是重新寫好的程式碼,這段程式碼很好地展示了什麼是讓讀者去推測變數名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
int doSomethingWithCollectionElements(int numberOfResults, Collection<Integer> integerCollection) { int resultToReturn = 0; for (int variableThatCountsUp = 0; variableThatCountsUp < integerCollection.size() && variableThatCountsUp < numberOfResults; variableThatCountsUp++) { int integerFromCollection = integerCollection.get(count); if (integerFromCollection >= 0) { System.out.println(integerFromCollection + " "); } else { resultToReturn++; } } System.out.println("n"); return resultToReturn; } |
這種改變讓程式碼看起來比自動生成的程式碼都糟糕,至少自動生成的程式碼更短。這段程式碼並沒有讓程式設計師的意圖更明顯,甚至需要讀者看更多的字元。要知道程式碼評審需要看很多程式碼,糟糕的命名使得一項艱鉅的任務更加艱鉅了。那如何才能減少程式碼評審負擔呢?
關於程式碼評審
程式碼評審的時候主要有兩種精神負擔:距離和樣板程式碼。從變數名角度來說,距離的意思是指評審人員需要額外看多少程式碼才能夠理解這個變數的作用。評審人員不會像編碼人員寫程式碼的時候那樣腦海裡有大概輪廓,他們只能快速地自己重建這個輪廓。而且評審人員需要很快完成;因為不值得在評審上花費和編碼同樣的時間。好的變數名能夠很好地解決距離這個問題,因為它們能夠提醒評審人員這些變數的目的是什麼。那樣的話評審人員也不需要花時間去回看之前的程式碼。
另一個負擔就是樣板程式碼。程式碼經常在做一些複雜的事情;它是其他人寫的;評審人員經常會根據自己的程式碼進行上下文切換;他們每天都要看大量程式碼並且很有可能評審了多年。介於這些,評審人員很難一直保持精神集中。因此,每一個沒用的字元都會消耗評審的效率。對於單獨一個小的案例,其實不清楚也不是什麼大問題。在有足夠的時間和精力(可能需要和編碼人員有後續交流)的情況下,評審人員完全可以搞清楚所有程式碼的作用。但是他們不能年復一年地重複這麼做。這相當於將評審人員千刀萬剮。
一個好的例子
所以,為了能夠讓程式碼評審人員理解意圖,編碼人員在儘可能少用字元情況下可以重寫成以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int printFirstNPositive(int n, Collection<Integer> c) { int skipped = 0; for (int i = 0; i < c.size() && i < n; i++) { int maybePositive = c.get(i); if (maybePositive >= 0) { System.out.println(maybePositive + " "); } else { skipped++; } } System.out.println("n"); return skipped; } |
我們一起來分析一下每一個變數看看為什麼能夠讓程式碼更容易理解:
printFirstNPositive
不像processElements,現在很清楚編碼人員寫這個函式的目的(並且提供了難得的機會發現bug)
n
有了清晰的函式名,對於n就沒必要用個複雜的名字了
c
集合並不值得花費太多精力,所以我們削減了9個字元來減少讀者瀏覽樣板字元時的疲勞;介於這個函式很短,而且也只有一個集合變數,所以很容易就記住了c是一個整型的集合
skipped
不像results,現在自己就說明了(不需要註釋)返回值是什麼。介於這是個很短的函式,並且對skipped宣告為一個int型別也很容易看到,如果用numSkipped就會浪費了3個字元。
i
在遍歷一個迴圈的時候,使用i變數是個約定俗成的習慣,每個人都能夠理解。姑且不說count這個變數名沒一點用,i變數還節省了4個字元。
maybePositive
num僅僅只說明int做的事,然而maybePositive就很難被誤解並且可以幫助定位出bug。
現在也更容易發現這段程式碼裡面其實有兩個bugs。在最初的版本中,如果編碼人員只是想列印出正整數的話是很難發現。現在讀者們可以注意到一個bug就是0並不是正數(所以n應該大於0,而不是大於等於)。(這裡應該也需要單元測試)。此外,因為第一個引數現在是maxToPrint(相反的,maxToConsider),很顯然如果集合裡面有非正整數的話,函式不會列印出足夠的元素。如何正確重寫這個函式將留個讀者作為練習。
命名的原則
- 作為程式設計師,我們職責是和其他的人類讀者交流,而不是和機器交流
- 不要讓我去猜。變數名應該直接表達編碼人員的意圖,所以讀者是不應該去猜測的。
- 程式碼評審非常重要,但是也會有精神疲勞。儘可能減少樣板程式碼,因為它會分散評審人員的集中力。
- 相比較註釋,我們更喜歡好的命名,但是並不是說可以完全取代註釋。
參考準則
為了滿足這些原則,寫程式碼的時候可以使用下面一些實用指南:
不要把資料型別放到變數名中
把變數的型別放到變數名中會加重讀者的精神疲勞(需要掃描更多的樣板程式碼)而且也不是一個好的替換。現在的編輯器比如Eclipse能夠很好地展示變數的型別,使得新增型別到命名中很累贅、這種做法也會招致一些錯誤,我就看過下面這種程式碼:
1 |
Set<Host> hostList = hostSvc.getHost |
最容易犯的錯誤就是在名字後面新增Str或者String,或者加入集合的型別。這裡有一些建議:
Bad Name(s) | Good Name(s) |
hostList, hostSet | hosts, validHosts |
hostInputStream | rawHostData |
hostStr, hostString | hostText, hostJson, hostKey |
valueString | firstName, lowercasedSKU |
intPort | portNumber |
一般來說:
- 使用複數命名而不要包含集合型別
- 如果確實要在你的變數名中加入標量型別(int,String,Char),你應該做到:
- 更好地解釋了這是個什麼變數
- 解釋使用這個新變數有什麼變化
儘可能使用日耳曼語系名字
大多數命名都應該用日耳曼語系,它遵循了像挪威語那樣的優點,而不是晦澀含糊如英語一樣的羅曼語系。挪威語裡面有更多像tannlege(“牙醫”)和sykehus(“病房”)的單詞,很少如dentist和hospital這類單詞(這類單詞拆分之後就不是英語單詞了,除非你知道它們的意思,不然這就很難理解)。你應該儘可能使用日耳曼語系的優點來給你的變數命名:即使不認識的情況下也容易理解。
另一種方式使用日耳曼語系名字是在沒有錯誤情況下儘可能的具體。比如,如果一個函式僅僅用於檢測CPU的負荷,那就把這個函式命名為overloadedCPUFinder,而不是unhealthyHostFinder。雖然這個函式可能是被用於查詢不正常的主機,但是unhealthyHostFinder會使得聽起來比其本身更籠統。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// GOOD Set<Host> overloadedCPUs = hostStatsTable.search(overCPUUtilizationThreshold); // BAD Set<Host> matches = hostStatsTable.search(criteria); // GOOD List<String> lowercasedNames = people.apply(Transformers.toLowerCase()); // BAD List<String> newstrs = people.apply(Transformers.toLowerCase()); // GOOD Set<Host> findUnhealthyHosts(Set<Host> droplets) { } // BAD Set<Host> processHosts(Set<Host> hosts) { } |
日耳曼語系命名也有例外,在這部分的後面也會提到:習語和短變數名
值得一提的是這裡也不是說禁止使用籠統的命名。那些確實是在做一些一般性工作的程式碼就可以用個籠統的命名。例如,在下面這個例子中的transform是可以的,因為這是一個一般性字串操作庫裡面的一部分。
1 2 3 |
class StringTransformer { String transform(String input, TransformerChain additionalTransformers); } |
將簡單的註釋寫入到變數名中
像之前所說的,變數名是無法(也是不應該)代替註釋的。如果用變數名代替一條註釋,那這也是很可以的,因為:
- 能夠讓程式碼評審人員讀程式碼時減少視覺上的混亂(註釋也是一種精神疲勞,所以提供其真正的價值)
- 如果一個變數的使用離註釋較遠,那麼程式碼評審人就沒必要轉移他們的注意力而返回去檢視註釋來理解變數的用意。
例如,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// BAD String name; // First and last name // GOOD String fullName; // BAD int port; // TCP port number // GOOD int tcpPort; // BAD // This is derived from the JSON header String authCode; // GOOD String jsonHeaderAuthCode; |
避免過度使用陳詞濫調
除了不使用日耳曼語系命名外,以下的這些變數名被廣泛濫用了很多年,而實際上這些變數名從來不應該被使用:
- val, value
- result, res, retval
- tmp, temp
- count
- str
還有僅僅只是在變數名加上其型別名稱也是要被禁止的,比如像tempString或者intStr這類等等
在必要之處使用一些習慣用法
不像之前所說的陳詞濫調,有一些習慣的用法是被廣泛理解而且能夠被安全使用即使是字面上看含義有些模糊。這裡有一些事例(都是些Jave/C的例子,但是也適用於其他所有語言):
- 在迴圈語句中使用i,j,k作為迭代
- 當用意明顯的時候,使用n作為界限或者數量
- 在catch語句中,使用e作為一個異常
1 2 3 4 5 |
// OK for (int i = 0; i < hosts.size(); i++) { } // OK String repeat(String s, int n); |
警告:習語應該只有在用意明顯的時候被使用到
用意明顯的時候使用短命名
短的命名甚至是一個字母的變數名在某些場合中是更好的。當評審人員看到一個很長的名字,他們就會覺得需要去注意這些長的命名,如果最後發現這個命名完全沒用,那純屬於浪費時間。一個短的命名錶達了唯一需要了解這個變數的就是它的型別。所以在一下兩個成立的情況下,使用短名字(一個或者兩個字母)的完全合理的:
- 變數的宣告和使用不是很遠(比如五行以內,所以也就是說變數宣告是在讀者視覺範圍以內)
- 除了型別以外,找不到一個更好得變數名
- 讀者在這段程式碼裡面沒有其他的東西需要記住(研究表明人類能夠同時記住七件事)
這裡有個例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
void updateJVMs(HostService s, Filter obsoleteVersions) { // 'hs' is only used once, and is "obviously" a HostService List<Host> hs = s.fetchHostsWithPackage(obsoleteVersions); // 'hs' is used only within field of vision of reader Workflow w = startUpdateWorkflow(hs); try { w.wait(); } finally { w.close(); } } |
也可以寫成:
1 2 3 4 5 6 7 8 9 10 |
void updateJVMs(HostSevice hostService, Filter obsoleteVersions) { List<Host> hosts = hostService.fetchHostsWithPackage(obsoleteVersions); Workflow updateWorkflow = startUpdateWorkflow(hosts); try { updateWorkflow.wait(); } finally { updateWorkflow.close(); } } |
但是這種寫法需要佔據更多的空間卻沒有其他的收益;變數使用都很緊湊,讀者們也沒有問題去理解其用意。還有就是,長命名updateWorkflow表示著這個變數名有特別之處。評審人員要花費精力去看這個命名是不是樣板。在這裡可能不是個大問題,但是記住,程式碼評審會“死於”這種“千刀萬剮”。
刪除沒意義的一次性變數
一次性變數,也被成為垃圾變數,是指那些被迫用於函式傳遞間的中間結果。他們有時也是有用的(詳見下一準則),但是大多數時候都是無意義的,並且也會使得程式碼庫混亂。下面的程式碼段中,編碼人員就讓讀者讀起來更艱難:
1 2 |
List<Host> results = hostService.fetchMatchingHosts(hostFilter); return dropletFinder(results); |
相對於上面的程式碼,編碼人員應該將程式碼簡化成:
1 |
return dropletFinder(hostService.fetchMatchingHosts(hostFilter)); |
使用短的一次性變數來拆分較長的行
有時需要一個一次性變數來拆分長的程式碼行
1 2 |
List<Host> hs = hostService.fetchMatchingHosts(hostFilter); return DropletFinderFactoryFactory().getFactory().getFinder(region).dropletFinder(hs); |
這是可以的,由於兩行程式碼距離不遠,可以使用一次性變數和一個或者兩個字母的變數名(hs)來減少視覺混亂。
使用短的一次性變數來拆分複雜的表達
讀這段程式碼會很困難:
1 2 3 |
return hostUpdater.updateJVM(hostService.fetchMatchingHosts(hostFilter), JVMServiceFactory.factory(region).getApprovedVersions(), RegionFactory.factory(region).getZones()); |
寫成這樣就會更好:
1 2 3 4 |
List<Host> hs = hostService.fetchMatchingHosts(hostFilter); Set<JVMVersions> js = JVMServiceFactory.factory(region).getApprovedVersions(), List<AvailabilityZone> zs = RegionFactory(region).getZones(); return hostUpdater.updateJVM(hs, js, zs); |
重申一下,因為程式碼間距離很短而且意圖明顯,短的變數名完全可以使用。
使用長的一次性變數來解釋難懂的程式碼
有時你需要一次性程式碼簡單地解釋這段程式碼在做什麼。例如,有時候不得不使用別人寫的一些糟糕命名的程式碼,所以你不能修復這些命名。但是你能做的就是使用有意義的一次性變數來解釋在做些什麼,比如,
1 2 |
List<Column> pivotedColumns = olapCube.transformAlpha(); updateDisplay(pivotedColumns); |
記住,這種情況下你不能使用短的一次性變數:
1 2 |
List<Column> ps = olapCube.transformAlpha(); updateDisplay(ps); |
這僅僅只是新增了視覺混亂而並沒有幫助有效地解釋程式碼。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!