作者: 周思博 (Joel Spolsky)
譯: Paul May 梅普華
2005.5.11
?時(shí)間回到1983年九月, 我第一個(gè)真正的工作是在以色列的Oranim. 這家大型麵包工廠每晚都用六個(gè)貨機(jī)般大的巨型爐子烤出為數(shù)十萬(wàn)的麵包.
我第一次走進(jìn)那家麵包廠時(shí)覺(jué)得裡頭實(shí)在髒得離譜. 爐壁發(fā)黃機(jī)器生鏽而且到處都是油.
"這裡一直都這麼髒嗎?"我問(wèn)道.
"什麼? 你講這什麼話?"經(jīng)理回答說(shuō)."我們才剛打掃過(guò). 這已經(jīng)是幾週以來(lái)最乾淨(jìng)的時(shí)候了."
說(shuō)得真好!
我花了好幾個(gè)月每天早上打掃才真正瞭解他們的意思. 對(duì)麵包工廠來(lái)說(shuō), 乾淨(jìng)是指機(jī)器裡沒(méi)有生麵糰在烤, 垃圾堆裡沒(méi)有發(fā)酵的麵糰, 而且地板上也沒(méi)有堆生麵糰.
乾淨(jìng)並不是指爐子漆得雪白亮麗. 爐子大概十年才會(huì)漆一次, 並不會(huì)每天都來(lái)一回. 乾淨(jìng)也不是說(shuō)把油擦得乾乾淨(jìng)淨(jìng). 事實(shí)上很多機(jī)器都得定期上油, 一層薄淨(jìng)的油通常暗示機(jī)器剛做過(guò)清潔保養(yǎng).
?麵包工廠裡這整套乾淨(jìng)的概念都得經(jīng)由學(xué)習(xí)而來(lái). 圈外人不可能走進(jìn)去就能說(shuō)出哪裡乾淨(jìng)哪裡髒. 圈外人絕不會(huì)想到要看麵糰滾圓機(jī)(把方麵糰滾成球形的機(jī)器, 見(jiàn)右邊附圖)內(nèi)壁有沒(méi)有刮乾淨(jìng). 圈外人會(huì)覺(jué)舊爐子外壁鑲板掉色是有問(wèn)題的,因?yàn)殍偘搴?em>大很顯眼. 不過(guò)麵包師傅根本不在意爐子的塗漆開(kāi)始發(fā)黃. 因?yàn)辄I包的味道還是一樣棒.
在麵包工廠待兩個(gè)月, 你學(xué)會(huì)如何"看出"乾淨(jìng).
程式碼也是一樣的.
當(dāng)你剛開(kāi)始寫(xiě)程式或嘗試讀用新語(yǔ)言寫(xiě)的程式時(shí), 所有程式碼看起來(lái)都一樣神秘不可解. 而在瞭解該種程式語(yǔ)言前, 你連明顯的語(yǔ)法錯(cuò)誤都看不出來(lái).
在學(xué)習(xí)的第一階段, 你會(huì)開(kāi)始發(fā)現(xiàn)一種我們通常稱為"編程風(fēng)格"的東西. 於是你開(kāi)始注意那些不遵循縮排標(biāo)準(zhǔn)的程式碼和有用多個(gè)大寫(xiě)字母的變數(shù).
也就是這個(gè)階段你會(huì)說(shuō):"該死的混蛋, 我們這裡一定要定出一些一致的編程風(fēng)格!" 然後第二天寫(xiě)出一份你們團(tuán)隊(duì)用的編程風(fēng)格, 接下來(lái)用六天來(lái)討論One True Brace Style(譯著:就是K&R style), 然後再花三星期把舊程式碼改寫(xiě)成符合One True Brace Style, 一直做到經(jīng)理發(fā)現(xiàn)並責(zé)怪你把時(shí)間浪費(fèi)在不能賺錢的事為止. 你想想其實(shí)不需要一次全部改好, 看到哪裡改到哪裡也沒(méi)什麼關(guān)係. 於是有一半的程式碼已經(jīng)改成True Brace Style, 而沒(méi)多久你就忘記這件事了. 接下來(lái)你就開(kāi)始滿腦子想著其他與賺錢無(wú)關(guān)的事, 比如把某個(gè)字串類別換成另一個(gè)字串類別等等.
當(dāng)你對(duì)某特定環(huán)境下的程式愈來(lái)愈精通時(shí), 就會(huì)開(kāi)始學(xué)著看到其他東西. 那些東西可能完全合法並符合編程風(fēng)格, 卻又會(huì)讓你擔(dān)心不已.
舉例來(lái)說(shuō)在C語(yǔ)言裡:
char* dest, src;
這是合語(yǔ)法的程式碼; 這可能符合你的編程規(guī)範(fàn), 甚至可能是故意這樣寫(xiě)的, 不過(guò)如果你寫(xiě)C的經(jīng)驗(yàn)夠, 就會(huì)注意這種寫(xiě)法把dest宣告成字元指標(biāo)卻把src宣告成字元而已, 這可能是你的意思, 不過(guò)也可能不是. 反正這段程式看起來(lái)有點(diǎn)不對(duì)勁.
來(lái)看更細(xì)微的例子:
if (i != 0)
????foo(i);
這段程式是百分之百正確的;它符合大多數(shù)的編程規(guī)範(fàn)也完全沒(méi)有錯(cuò)誤, 不過(guò)你可能會(huì)質(zhì)疑if敘述所接的單敘述主體並未用大括號(hào)包起來(lái), 因?yàn)槟隳X子裡想到有人可能會(huì)插入另一行程式碼
if (i != 0)
????bar(i);
????foo(i);
...又忘記加上大括號(hào), 結(jié)果讓foo(i)變成永遠(yuǎn)會(huì)執(zhí)行! 所以當(dāng)你看到?jīng)]有用大括弧包起來(lái)的程式碼區(qū)段時(shí), 可能就會(huì)感覺(jué)到一絲絲讓你不舒服的氣味.
好啦, 到目前為止我已經(jīng)提到三種程式師的成就層級(jí):
1. 你不知道乾淨(jìng)和髒有什麼分別.
2. 你對(duì)乾淨(jìng)有粗淺的認(rèn)知, 主要以是否符合編程規(guī)範(fàn)為準(zhǔn).
3. 你開(kāi)始能嗅出藏在表面下不對(duì)勁的蛛絲馬跡. 你會(huì)察覺(jué)這是問(wèn)題並且找出來(lái)修正.
不過(guò)其實(shí)還有更高的層次, 而這也就是我真正要說(shuō)的:
4. 你有計(jì)劃地架構(gòu)程式碼, 藉助能察覺(jué)問(wèn)題的靈眼讓程式碼更正確.
這是真正的藝術(shù): 仔細(xì)地設(shè)計(jì)讓錯(cuò)誤顯而易見(jiàn)的編程規(guī)範(fàn), 藉此製作出穩(wěn)固的程式.
所以現(xiàn)在我要帶你看一個(gè)小例子然後再展示一個(gè)通用的規(guī)則. 你可以利用這個(gè)通則設(shè)計(jì)出創(chuàng)造增加程式穩(wěn)固的編程規(guī)範(fàn). 最後我會(huì)把主題導(dǎo)引到為某種匈牙利命名法(可能不是讓人們暈到的那種)進(jìn)行辯護(hù), 並且批判某些環(huán)境(也可能不是你最常用的那種環(huán)境)下的例外處理.
不過(guò)如果你深信匈牙利命名法不是好東西, 認(rèn)為例外處理是從自巧克力奶昔以來(lái)最棒的發(fā)明, 而且完全不想聽(tīng)聽(tīng)其他意見(jiàn), 沒(méi)問(wèn)題, 你可以改去羅力那裡看看好看的漫畫(huà); 反正你在這裡也沒(méi)什麼好看的; 事實(shí)上在一分鐘內(nèi)我就會(huì)拿出實(shí)際的程式碼範(fàn)例, 這些範(fàn)例很可能會(huì)讓你在不爽前就暈睡過(guò)去了. 沒(méi)錯(cuò). 我想我的計(jì)畫(huà)是把你哄到沈沈入睡, 趁你睡著無(wú)法抵抗時(shí)把"匈牙利命名法=好, 例外處理=壞"的想法偷偷塞進(jìn)你腦子裡面.
一個(gè)例子

好了. 提到這個(gè)例子. 讓我們假裝你正在寫(xiě)某種web應(yīng)用程式, 因?yàn)檫@陣子小朋友似乎都流行寫(xiě)這玩意.
現(xiàn)在有一種叫跨站腳本漏洞(Cross Site Scripting Vulnearability)的安全漏洞, 縮寫(xiě)為XSS. 我在這裡不談細(xì)節(jié): 你只需要知道在寫(xiě)web應(yīng)用程式時(shí), 一定要小心絕不能把使用者填入表單的任何字串直接傳回來(lái).
舉例來(lái)說(shuō), 如果你有一個(gè)網(wǎng)頁(yè)會(huì)讓使用者在編輯框輸入姓名, 傳送後就會(huì)跳到另一個(gè)寫(xiě)著"你好啊, 張三!"(假設(shè)使用者的名字是張三)的網(wǎng)頁(yè). 很好, 這就是個(gè)安全漏洞, 因?yàn)槭褂谜呖赡懿惠斎?張三"而輸入某種奇怪的HTML及JavaScript, 這些奇怪的JavaScript就可能會(huì)做些低級(jí)事情, 比如讀出你寫(xiě)的cookie內(nèi)容轉(zhuǎn)送到壞人的壞網(wǎng)站去. 而這些低級(jí)事現(xiàn)在看起來(lái)就是你搞的鬼.
讓我們把程式用虛擬碼的方法寫(xiě)出來(lái). 想像以下的程式
s = Request("name")
會(huì)由HTML表格讀取使用者輸入(一個(gè)POST的參數(shù)). 如果你曾經(jīng)寫(xiě)出下面的程式碼:
Write "你好, " & Request("name")
那你的網(wǎng)站已經(jīng)有讓XSS攻擊的漏洞了. 光這樣就夠了.
你必須在複製回HTML之前先編碼才能避免這個(gè)漏洞. 所謂編碼就是把"換成", 把>換成>, 如此類推. 所以
Write "你好, " & Encode(Request("name"))
是絕對(duì)安全的.
所有來(lái)自使用者的字串都是不安全的. 任何不安全的字串都得先編碼後才能輸出.
讓我們嘗試設(shè)計(jì)一組編程規(guī)範(fàn), 確保當(dāng)你犯這種錯(cuò)時(shí)程式碼看起來(lái)就是錯(cuò)的. 如果程式碼有錯(cuò)(至少看起來(lái)錯(cuò)), 就很有機(jī)會(huì)被修改或?qū)徱曔@段程式的人抓到.
可能方案一
方案一是將所有字串立即編碼, 由使用者取得後馬上就進(jìn)行:
s = Encode(Request("name"))
所以我們的規(guī)範(fàn)會(huì)寫(xiě)著: 如果你看到?jīng)]有被Encode包住的Request, 程式一定是錯(cuò)的.
你開(kāi)始訓(xùn)練自己的眼睛找尋落單的Request, 因?yàn)樗鼈冞`反規(guī)範(fàn).
這是有用的, 因?yàn)橹灰阕裱?guī)範(fàn)就不會(huì)有XSS問(wèn)題. 不過(guò)這並不是最好的架構(gòu). 比方說(shuō)你可能想要把這些使用者字串存到資料庫(kù)裡, 這時(shí)候儲(chǔ)存以HTML編碼過(guò)的字串並不合理, 因?yàn)樽执锌赡軙?huì)用在HTML網(wǎng)頁(yè)以外的場(chǎng)合. 假如是信用卡處理程式要用時(shí)編碼過(guò)的資料就會(huì)產(chǎn)生問(wèn)題. 大部份web應(yīng)用程式開(kāi)發(fā)都會(huì)依循一個(gè)原則: 所有字串在內(nèi)部都是未編碼的, 要等到送至HTML網(wǎng)頁(yè)的前一瞬間才會(huì)處理, 因此這可能並不是正確的架構(gòu).
我們真的要能讓字串維持在不安全格式一段時(shí)間.
好吧. 我再試看看.
可能方案二
如果建立一種編程規(guī)範(fàn), 要求在寫(xiě)出任何字串時(shí)必須加以編碼, 是否可以滿足要求嗎?
s = Request("name")
// 很後面:
Write Encode(s)
現(xiàn)在當(dāng)你看到一個(gè)落單沒(méi)有Encode跟著的Write時(shí)就知道有有問(wèn)題了.
唉, 這也不太好...有時(shí)候你的程式裡會(huì)有一小段的HTML碼, 這種情況下是不能夠編碼的:
If mode = "linebreak" Then prefix = "<br>"
// 很後面:
Write prefix
這照我們的規(guī)範(fàn)來(lái)看是錯(cuò)的, 我們必須要在輸出時(shí)加以編碼:
Write Encode(prefix)
不過(guò)現(xiàn)在應(yīng)該要新增一行的"<br>"卻被編碼成<br>, 結(jié)果變成使用者可以看到的字元< b r >. 這樣的解法也不對(duì).
所以說(shuō)有時(shí)候你不能在讀入字串時(shí)編碼, 有時(shí)候你也不能在輸出時(shí)編碼, 這兩種提案都不能用. 可是沒(méi)有適當(dāng)?shù)木幋a規(guī)範(fàn), 我們還是有出下列問(wèn)題的風(fēng)險(xiǎn):
s = Request("name")
...好幾頁(yè)之後...
name = s
...好幾頁(yè)之後...
recordset("name") = name // 把名字存在資料庫(kù)中的姓名欄
...好幾天後...
theName = recordset("name")
...好幾頁(yè)甚至好幾個(gè)月之後...
Write theName
我們還會(huì)記得要對(duì)字串編碼嗎? 你在任何單一的地方都看不到問(wèn)題. 連可以嗅的地方都沒(méi)有. 如果這種程式有一大缸子, 要一大票偵探才能追蹤出所有字串的來(lái)源並確認(rèn)是否已編碼..
正解
所以讓我提議一種能用的編程規(guī)範(fàn). 我們只有一個(gè)規(guī)則:
所有來(lái)自使用者的字串都必須存在以"us"(表示Unsafe String,不安全字串)為字首的變數(shù)(或資料庫(kù)欄位)中. 所有經(jīng)HTML編碼或來(lái)自確認(rèn)安全來(lái)源的字串都必須存在以"s"(表示Safe String,安全字串)為字首的變數(shù)中.
讓我們重寫(xiě)程式, 只是依規(guī)範(fàn)重新命名變數(shù), 其他完全不動(dòng).
us = Request("name")
...好幾頁(yè)之後...
usName = us
...好幾頁(yè)之後...
recordset("usName") = usName
...好幾天後...
sName = Encode(recordset("usName"))
...好幾頁(yè)甚至好幾個(gè)月之後...
Write sName
新規(guī)範(fàn)中值得注意的是, 只要遵循編碼規(guī)範(fàn), 不安全字串相關(guān)的錯(cuò)誤一定可以由單一行的程式碼看出來(lái):
s = Request("name")
是之前的錯(cuò)誤, 因?yàn)槟憧梢钥吹?tt>Request的結(jié)果被指派給以s開(kāi)頭的變數(shù), 這違反了規(guī)則. Request的結(jié)果一定是不安全的, 所以必須指派給以"us"開(kāi)頭的變數(shù).
us = Request("name")
一定沒(méi)問(wèn)題.
usName = us
一定沒(méi)問(wèn)題.
sName = us
一定是錯(cuò)的.
sName = Encode(us)
一定是對(duì)的.
Write usName
一定是錯(cuò)的.
Write sName
沒(méi)問(wèn)題, 下面也一樣沒(méi)問(wèn)題
Write Encode(usName)
每一行程式光是看程式碼本身就足以檢查, 而且如果每一行程式都對(duì), 組合起來(lái)整個(gè)程式也是對(duì)的.
終於好了, 利用這套編碼規(guī)範(fàn), 你的眼睛學(xué)著看到Write usXXX就知道是錯(cuò)的, 而且你也立即知道要如何修正. 我知道一開(kāi)始要看到錯(cuò)誤的程式是有一點(diǎn)難, 不過(guò)進(jìn)行三個(gè)星期後你的眼睛就會(huì)習(xí)慣, 就像麵包廠的工人看到大麵包工廠就會(huì)馬上說(shuō):"搞什麼鬼, 這裡都沒(méi)人在掃哦! 這算啥麵包廠."
事實(shí)上我們可以再把規(guī)則延伸一點(diǎn), 把Request和Encode函數(shù)改名(或封裝)成UsRequest和SEncode...換句話說(shuō), 傳回不安全字串以及安全字串的函數(shù)要和變數(shù)一樣, 分別要用Us及S作為字首. 現(xiàn)在看看程式碼:
us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SEncode(recordset("usName"))
Write sName
看到我們的成果沒(méi)? 現(xiàn)在你可以看看等號(hào)兩邊的字首是否相同就能找到錯(cuò)誤.
us = UsRequest("name") // 沒(méi)問(wèn)題, 兩邊都以US開(kāi)頭
s = UsRequest("name") // 錯(cuò)
usName = us// 對(duì)
sName = us// 一定錯(cuò).
sName = SEncode(us) // 一定對(duì).
我還能再進(jìn)一步把Write改名成WriteS並把SEncode改名成SFromUs:
us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SFromUs(recordset("usName"))
WriteSsName
這使得錯(cuò)誤更加顯而易見(jiàn). 你的眼睛會(huì)學(xué)習(xí)"看出"可疑的程式碼, 另外這也能協(xié)助你經(jīng)由一般撰寫(xiě)或閱讀程式碼的動(dòng)作找到隱藏的安全漏洞.
讓錯(cuò)的程式看得出錯(cuò)是很棒沒(méi)錯(cuò), 不過(guò)卻不是所有安全問(wèn)題的最佳解答. 它無(wú)法找到所有可能的問(wèn)題或錯(cuò)誤, 因?yàn)槟憧赡軟](méi)法子看過(guò)每一行程式碼. 不過(guò)絕對(duì)比什麼都不做要好, 而我很希望有套編碼規(guī)範(fàn)能讓錯(cuò)誤的程式碼至少看起來(lái)是錯(cuò)的. 你馬上就能獲得好處, 每當(dāng)程式師的眼睛掃過(guò)一行程式, 就能檢查並防止某些特定的錯(cuò)誤.
一個(gè)通則
這種讓錯(cuò)誤程式看起來(lái)錯(cuò)的作法有個(gè)前提, 就是要讓對(duì)的東西在螢?zāi)簧暇o靠在一起. 當(dāng)我看到某個(gè)字串時(shí)並要決定 程式碼正確與否, 我必須知道字串出現(xiàn)的所有位置以及字串是安全的還是不安全的. 我不希望這些資料出現(xiàn)在另一個(gè)檔案或是要捲動(dòng)畫(huà)面才能看到的另一頁(yè). 我必須能當(dāng)場(chǎng)看到, 而這說(shuō)的就是一套變數(shù)命名規(guī)範(fàn).
有很多其他的例子可以說(shuō)明, 只要把某些東西搬在一起就可以改善程式碼. 大多數(shù)的編程規(guī)範(fàn)都有如下的規(guī)則:
- 保持函數(shù)名稱簡(jiǎn)短.
- 變數(shù)宣告的地方離使用的位置愈近愈好.
- 不要用巨集建立你個(gè)人專屬的程式語(yǔ)言.
- 不要使用goto.
- 不要讓右括弧離左括弧超過(guò)一個(gè)畫(huà)面.
這些規(guī)則有一個(gè)共同點(diǎn), 就是儘量讓一行程式碼實(shí)際作用的相關(guān)資訊在畫(huà)面上愈近愈好. 這樣能提高眼球找出程式實(shí)質(zhì)運(yùn)作內(nèi)容的機(jī)會(huì).
大體上我得承認(rèn)我有點(diǎn)害怕會(huì)藏東西的程式語(yǔ)言功能. 當(dāng)你看到程式碼
i = j * 5;
... 就C來(lái)說(shuō)你至少會(huì)知道j會(huì)乘以5而結(jié)果會(huì)存到i.
不過(guò)如果你在C++裡看到相同的片段, 你什麼都不知道. 在C++中唯一能知道真正發(fā)生什麼事的方法就是找出i和j所屬的型別, 而這個(gè)型別可能會(huì)在完全不一樣的地方宣告. 因?yàn)?tt>j的運(yùn)算子*可能有過(guò)荷, 在你要做乘法時(shí)會(huì)做些很機(jī)靈的事. 而i的運(yùn)算子=可能也是過(guò)荷的, 而兩者型別可能是不相容的, 於是又呼叫到某個(gè)自動(dòng)型別強(qiáng)制轉(zhuǎn)換的函數(shù). 光是檢查變數(shù)的型別還不足以確認(rèn), 還得檢查實(shí)作該型別的程式碼才行, 萬(wàn)一實(shí)作時(shí)又有繼承其他型別就更麻煩了, 因?yàn)槟愕没厮蓊悇e繼承的祖宗八代才能找到真正的程式碼, 不巧又有用到別處的多型就真的有大麻煩了, 因?yàn)楣馐侵?tt>i和j宣告的型別並不夠, 還得知道它們此刻的型別, 這不知道要看多少的程式碼, 而且依照計(jì)算理論的停機(jī)問(wèn)題, 你永遠(yuǎn)都不能真的百分之百確定自己已經(jīng)看完所有地方了(啊啊啊啊啊!!!).
當(dāng)你看到C++的i=j*5時(shí)你只能自求多福了, 兄弟. 這對(duì)我來(lái)說(shuō)就降低了光看程式碼找出在問(wèn)題的能力.
當(dāng)然囉, 理論上這應(yīng)該沒(méi)什麼關(guān)係. 當(dāng)你做些過(guò)荷運(yùn)算子*之類聰明事時(shí), 只要為了要提供一個(gè)優(yōu)美而安全的抽象罷了. 天啊, 其實(shí)j是個(gè)萬(wàn)國(guó)碼字串型別, 一個(gè)萬(wàn)國(guó)碼字串乘以一個(gè)整數(shù)顯然是把正體中文轉(zhuǎn)成簡(jiǎn)體中文的良好抽象作法, 對(duì)嗎?
問(wèn)題當(dāng)然出在沒(méi)有絕對(duì)安全的抽象方法. 我已經(jīng)在抽象出錯(cuò)定律裡討論很多了, 所以不會(huì)在這裡重複.
Scott Meyers示範(fàn)了各種抽象出錯(cuò)(至少是C++)的型式以及所造成的傷害, 他靠這個(gè)主題就創(chuàng)出一番事業(yè)了. (順便一提, Scott的書(shū)Effective C++第三版剛剛上市; 整本書(shū)都重寫(xiě)過(guò);?今天就去買一本吧!)
好吧.
有點(diǎn)失焦了. 我最好回顧一下到目前為止的內(nèi)容:
找出能讓錯(cuò)誤程式看起來(lái)錯(cuò)的編程規(guī)範(fàn). 讓正確的資訊集中在程式碼中相同的地方, 方便你看出某些問(wèn)題並立即修正.
我是匈牙利
?我們現(xiàn)在回到惡名昭彰的匈牙利命名法.
匈牙利命名法是微軟程式設(shè)計(jì)師Charles Simonyi發(fā)明的. Simonyi在微軟做的主要計(jì)劃是Word; 事實(shí)上他還主持了世界上第一個(gè)所見(jiàn)即所得的文書(shū)處理器(在Xerox Parc名為Bravo計(jì)劃).
在所見(jiàn)即所得的文書(shū)處理中會(huì)用到可捲動(dòng)的視窗, 所以座標(biāo)值有兩種意義:相對(duì)於視窗或相對(duì)於處理頁(yè). 兩種座標(biāo)的差異很大, 所以好好安排是非常重要的.
我猜這正是Simonyi開(kāi)始採(cǎi)用某些之後被稱作匈牙利命名法的原因之一. 它看起來(lái)像匈牙利文, 而Simonyi是從匈牙利來(lái), 所以以匈牙利為名. 在Simonyi版本的匈牙利命名法中, 每個(gè)變數(shù)都會(huì)加一個(gè)小寫(xiě)的字首, 表示變數(shù)內(nèi)容的種類.

我是故意用種類(kind)這個(gè)詞, 因?yàn)镾imonyi在他的文章中誤用了型別(type), 結(jié)果好幾世代的程式師都誤解了他的意思.
如果你仔細(xì)讀Simonyi的文章, 就會(huì)發(fā)現(xiàn)他所講的和我之前範(fàn)例所用的命名規(guī)範(fàn)是一樣的, 在我的範(fàn)例中把us和s分別定義為不安全字串和安全字串. 這兩者的型別都是字串. 如果你把某種字串指派另一種, 編譯器並不會(huì)給任何警告, Intellisense也不會(huì)說(shuō)些什麼. 可是他們的語(yǔ)意是不同的; 他們解讀和處理的方式都不同, 要把兩種字串互相指派時(shí)還要某些轉(zhuǎn)換函數(shù)做轉(zhuǎn)換, 否則就會(huì)有執(zhí)行時(shí)期的問(wèn)題. 祝你好運(yùn).
微軟內(nèi)部稱Simonyi對(duì)匈牙利命名法的原始概念為應(yīng)用匈牙利命名法, 因?yàn)樗渺稇?yīng)用程式部門, 也就是Word及Excel. 在Excel的原始程式碼裡有大量的rw和col, 你看到這些字首就知道它們指的是行(row)和列(column). 沒(méi)錯(cuò), 它們都是整數(shù), 可是兩者間的轉(zhuǎn)換完全沒(méi)有意義. 有人告訴我說(shuō)Word的程式碼裡有大量的xl和xw, xl代表相對(duì)於排版頁(yè)面的水平座標(biāo), 而 xw則代表相對(duì)視窗的水平座標(biāo). 兩者都是整數(shù)但卻是不能互轉(zhuǎn)的. 兩個(gè)程式裡都有很多cb, 意思是位元組的個(gè)數(shù). 沒(méi)錯(cuò), 這也是整數(shù)型別, 不過(guò)光看變數(shù)名就可以得到更多資訊: 這是位元組的個(gè)數(shù), 也就是緩衝區(qū)的大小. 另外如果你看到xl = cb就可以拉警報(bào)了. 這顯然是錯(cuò)的程式, 雖然xl和cb都是整數(shù), 可是把以像素為單位的水平位移設(shè)成位元組個(gè)數(shù)絕對(duì)是瘋了.
在應(yīng)用匈牙利命名法中字首可以用於函數(shù)和變數(shù). 因此雖然我真的沒(méi)看過(guò)Word的原始碼, 我還是敢打賭Word裡一定有個(gè)叫YlFromYw的函數(shù), 可以把垂直方向的視窗座標(biāo)轉(zhuǎn)成垂直方向的排版頁(yè)座標(biāo). 應(yīng)用匈牙利命名法用TypeFromType取代傳統(tǒng)的?TypeToType, 這樣每個(gè)函數(shù)名就會(huì)以傳回的型別開(kāi)頭, 這正與我稍早在範(fàn)例中把Encode改名為SFromUs的作法相同. 事實(shí)上在正規(guī)的應(yīng)用匈牙利命名法中Encode函數(shù)一定要改名為SFromUs. 應(yīng)用匈牙利命名法在該函數(shù)命名上並沒(méi)有提供其他選擇. 這其實(shí)是件好事, 因?yàn)槟闵僖患乱? 另外也不必?fù)?dān)心Encode究竟是用什麼型別. 程式也變得精確多了.
應(yīng)用匈牙利命名法非常有用, 特別是當(dāng)初C語(yǔ)言盛行, 而編譯器尚未提供很有用的型別系統(tǒng)時(shí).
不過(guò)接下來(lái)卻出了一些問(wèn)題.
黑暗世界占用了匈牙利命名法.
似乎沒(méi)有人知道為什麼或是如何發(fā)生的, 不過(guò)似乎是視窗團(tuán)隊(duì)中寫(xiě)文件的人不小心創(chuàng)造出後來(lái)名為系統(tǒng)匈牙利命名法的東西.
某處有人讀了Simonyi的文章看到裡面用了"型別"這個(gè)字眼, 因此認(rèn)為作者指的就是型別, 意思就像是類別或是型別系統(tǒng)中, 或是編譯器所做的型別檢查. 其實(shí)不然. 作者很小心並精確的解釋他用"型別"這個(gè)字的意義, 不過(guò)沒(méi)有用. 傷害已經(jīng)造成了.
應(yīng)用匈牙利命名法的字首很有用而且有意義, "ix"表示陣列索引, "c"表示個(gè)數(shù), "d"表示兩個(gè)數(shù)字間的差(比如"dx"表示"寬度"), 如此類推.
系統(tǒng)匈牙利命名法的字首作用就差多了, "l"表示長(zhǎng)整數(shù), "ul"表示正長(zhǎng)整數(shù)而"dw"代表雙字組(呃, 事實(shí)上就是正長(zhǎng)整數(shù)). 在系統(tǒng)匈牙利命名法中, 字首只能告訴你變數(shù)真正的資料型別.
這誤解了Simonyi的意圖和實(shí)作, 差異雖細(xì)微實(shí)質(zhì)上卻是完全不同. 這件事唯一的教訓(xùn)是讓你知道, 如果你寫(xiě)出些沒(méi)人能懂的艱深難解學(xué)術(shù)文章, 你的想法可能會(huì)一再被誤解, 結(jié)果變得非常荒謬, 完全違背你的原意. 所以在系統(tǒng)匈牙利命名法中會(huì)出現(xiàn)大量的dwFoo表示"雙字組的某某", 可惡的是某個(gè)變數(shù)是雙字組這件事對(duì)你幾乎是完全沒(méi)用的. 難怪大家都很討厭系統(tǒng)匈牙利命名法.
系統(tǒng)匈牙利命名法的流傳既深又廣; 它是整個(gè)視窗程式設(shè)計(jì)文件的標(biāo)準(zhǔn); Charles Petzold的視窗程式設(shè)計(jì)(學(xué)習(xí)視窗程式設(shè)計(jì)的聖經(jīng))等書(shū)籍更為它廣為宣揚(yáng), 很快的它也成為匈牙利命名法的主要?jiǎng)萘? 即使在微軟內(nèi)部也一樣. 在微軟內(nèi)也只有少數(shù)不在Word和Excel團(tuán)隊(duì)的程式師瞭解他們搞出什麼樣的錯(cuò).
接下來(lái)就是大反抗了. 有群程式師們從一開(kāi)始就沒(méi)搞懂過(guò)匈牙利命名法, 他們發(fā)現(xiàn)自己用的竟是煩人又幾近無(wú)用的分支, 於是就起來(lái)反抗. 不過(guò)系統(tǒng)匈牙利命名法裡還是有些好東西可以幫你看出問(wèn)題. 如果用系統(tǒng)匈牙利命名法, 至少會(huì)在使用時(shí)知道變數(shù)型別. 不過(guò)沒(méi)應(yīng)用匈牙利命名法那麼有價(jià)值就是了.
大反抗在.NET.第一版發(fā)行時(shí)到達(dá)巔峰, 那時(shí)微軟終於告訴大家"不建議使用匈牙利命名法". 這還真是歡聲雷動(dòng)啊. 我根本不認(rèn)為微軟會(huì)花心思解釋原因. 他們只是掃瞄文件中命名指引的章節(jié)然後加上"不要使用匈牙利命名法"的字句. 當(dāng)時(shí)匈牙利命名法非常不受歡迎所以沒(méi)有人會(huì)真的抱怨, 而除Excel及Word以外的人都因?yàn)椴槐卦儆眠@麼麻煩的命名規(guī)範(fàn)而鬆了一口氣, 他們認(rèn)為在有強(qiáng)型別檢查及Intellisense的時(shí)代也不需要這種規(guī)範(fàn).
不過(guò)應(yīng)用匈牙利命名法還是很有價(jià)值的, 它加強(qiáng)了程式碼的連結(jié)讓程式碼更易閱讀, 撰寫(xiě), 除錯(cuò)及維護(hù), 最重要的是它讓錯(cuò)誤的程式看得出錯(cuò).
在繼續(xù)之前還有一件事我說(shuō)過(guò)要做, 就是再罵一次例外處理. 我上次這樣做惹來(lái)很多麻煩. 我在周思博趣談軟體首頁(yè)上一篇即興的評(píng)論中寫(xiě)說(shuō)我不喜歡例外處理, 因?yàn)樗鼘?shí)際上就是隱藏的goto, 我認(rèn)為這比看得到的goto更糟糕. 當(dāng)然就有幾百萬(wàn)人跑出來(lái)痛罵我. 全世界唯一跳出來(lái)替我辯護(hù)的當(dāng)然也就是Raymond Chen. 順帶一提, 他既然是世界上最好的程式師, 當(dāng)然得出來(lái)講講話, 對(duì)嗎?
這篇文章講到例外處理的重點(diǎn)了. 你的眼睛學(xué)著看到錯(cuò)誤的程式碼, 這樣就能防止問(wèn)題發(fā)生. 為了讓程式能變得真正穩(wěn)固, 進(jìn)行程式碼檢視時(shí)得有一套能集中資訊的命名規(guī)範(fàn). 換而言之, 你眼前有關(guān)程式運(yùn)作的資訊愈多, 尋找錯(cuò)誤的結(jié)果愈好. 當(dāng)你看到以下的程式碼時(shí)
dosomething();
cleanup();
...你的眼睛會(huì)說(shuō)沒(méi)什麼問(wèn)題啊. 我們總是要做清除的動(dòng)作! 不過(guò)dosomething有可能會(huì)引發(fā)一個(gè)例外, 所以有可能不會(huì)呼叫cleanup. 用finally等很簡(jiǎn)單就能修正這個(gè)問(wèn)題, 不過(guò)這並不是我的重點(diǎn): 問(wèn)題在於要知道cleanup一定會(huì)被呼叫到的唯一方法, 就是調(diào)查整個(gè)dosomething呼叫樹(shù), 看看是否有任何場(chǎng)合會(huì)產(chǎn)生例外. 這也還好, 可控制式例外處理(checked exception)可以讓你不用那麼辛苦, 不過(guò)重點(diǎn)是例外處理把資訊分散開(kāi)來(lái)了. 你得去看其他地方才能知道程式能正確執(zhí)行, 所以無(wú)法運(yùn)用你眼睛天賦的功能去學(xué)習(xí)看出錯(cuò)的程式碼, 因?yàn)楦緵](méi)東西可看.
如果我寫(xiě)個(gè)小腳本程式, 只是每天一次到處收集資料然後印出來(lái), 這時(shí)候例外處理好用得不得了. 我只想忽略所有可能出錯(cuò)的地方, 直接把整個(gè)程式用一個(gè)大try/catch包起來(lái), 如果有出什麼問(wèn)題就用catch把錯(cuò)誤電郵給自己. 例外處理對(duì)簡(jiǎn)單隨便寫(xiě)的程式很有用, 對(duì)腳本程式或是不是非常重要或無(wú)關(guān)生死的程式也不錯(cuò). 不過(guò)如果你在寫(xiě)一套作業(yè)系統(tǒng)或核電廠程式, 或是用於開(kāi)心手術(shù)的高速電鋸, 例外處理可是危險(xiǎn)的很.
我知道大家會(huì)認(rèn)為我是個(gè)無(wú)法正確理解例外處理的笨程式師, 完全不知道只有當(dāng)我衷心接納例外處理後它才能改善我的生活. 這種想法真是太糟糕了. 想要寫(xiě)出真正可信賴的程式碼, 應(yīng)該要嘗試用考慮到人有弱點(diǎn)的簡(jiǎn)單工具, 而不是靠那些提供有問(wèn)題的抽象並把副作用隱藏起來(lái), 還認(rèn)為程式師絕不出錯(cuò)的複雜工具.
補(bǔ)充讀物
如果你還是衷心於例外處理, 讀讀Raymond Chen的文章更乾淨(jìng)更優(yōu)雅, 不過(guò)更難讀. "例外處理用得正確與否, 很難由程式碼看得出來(lái)...?例外處理太難了, 我實(shí)在不夠聰明無(wú)法掌握."
Raymond對(duì)致命巨集的文章A rant against flow control macros討論了另一個(gè)讓資訊分散導(dǎo)致程式無(wú)法維護(hù)的例子. "當(dāng)看到使用[巨集]的程式碼時(shí), 你必須看遍各個(gè)標(biāo)頭檔才能瞭解它們的作用."
想要瞭解匈牙利命名法的歷史背景, 可以由Simonyi的原文匈牙利命名法開(kāi)始. Doug Klunder在另一篇比較清楚的文章中把它引進(jìn)Excel團(tuán)體?. 想知道更多匈牙利命名法的故事以及如何被文件撰寫(xiě)人破壞的始末, 可以去看Larry Osterman站上的貼文, 特別是Scott Ludwig的評(píng)論, 或是Rick Schaut貼的文章.
文件來(lái)源: Making Wrong Code Look Wrong (英文)?