很早就想寫這樣一篇博文了,可是一直沒來得及動筆抄淑。在學(xué)校的時候屠凶,時間似乎總是不夠用,因為一旦有點時間肆资,你就想是不是該用來多看點論文矗愧。所以我很高興,工作的生活給了我真正自由的時間郑原,讓我可以多分享一些自己的經(jīng)驗唉韭。
我今天想開始寫這系列文章的原因是,很多程序員的頭腦中都有一些通過“非理性”方式得到的錯誤觀點犯犁。這些觀點如此之深属愤,以至于你沒法跟他們講清楚。即使講清楚了酸役,一般來說也很難改變他們的習(xí)慣住诸。
程序員的世界,是一個“以傲服人”的世界涣澡,而不是一個理性的贱呐,“以德服人”的世界。很多人喜歡在程序里耍一些“小聰明”入桂,以顯示自己的與眾不同奄薇。由于這些人的名氣和威望,人們對這些小聰明往往不加思索的吸收抗愁,以至于不知不覺學(xué)會了很多表面上聰明馁蒂,其實導(dǎo)致不必要麻煩的思想,根深蒂固蜘腌,難以去除沫屡。接著,他們又通過自己的“傲氣”逢捺,把這些錯誤的思想傳播給下一代的程序員谁鳍,從而導(dǎo)致惡性循環(huán)。人們總是說“聰明反被聰明誤”劫瞳,程序員的世界里倘潜,為這樣的“小聰明”所栽的根頭,可真是數(shù)不勝數(shù)志于。以至于直到今天涮因,我們?nèi)匀辉谄S趶浹a前人所犯下的錯誤。
所以從今天開始伺绽,我打算陸續(xù)把自己對這些“小聰明”的看法記錄在這里养泡,希望看了的人能夠發(fā)現(xiàn)自己頭腦里潛移默化的錯誤,真正提高代碼的“境界”奈应±窖冢可能一下子難以記錄所有這類誤區(qū),不過就這樣先開個頭吧杖挣。
小聰明1:片面追求“短小”
我經(jīng)常以自己寫“非常短小”的代碼為豪肩榕。有一些人聽了之后很贊賞,然后說他也很喜歡寫短小的代碼惩妇,接著就開始說 C 語言其實有很多巧妙的設(shè)計株汉,可以讓代碼變得非常短小。然后我才發(fā)現(xiàn)歌殃,這些人所謂的“短小”跟我所說的“短小”乔妈,完全不是一回事。
我的程序的“短小”氓皱,是建立在語義明確路召,概念清晰的基礎(chǔ)上的。在此基礎(chǔ)上波材,我力求去掉冗余的股淡,繞彎子的,混淆的代碼各聘,讓程序更加直接揣非,更加高效的表達我心中設(shè)想的“模型”。這是一種在概念級別的優(yōu)化躲因,而程序的短小精悍只是它的一種“表象”早敬。這種短小,往往是在“語義” (semantics) 層面的大脉,而不是在“語法”層面死摳幾行代碼搞监。我絕不會為了程序“顯得短小”而讓它變得難以理解,或者容易出錯镰矿。
相反琐驴,很多其它人所追求的“短小”,卻是盲目的,沒有原則的绝淡。在很多時候宙刘,這些小伎倆都只是在“語法” (syntax) 層面,比如牢酵,想辦法把兩行代碼寫成一行悬包。可以說馍乙,這種“片面追求短小”的錯誤傾向布近,造就了一批語言設(shè)計上的錯誤,以及一批“擅長于”使用這些錯誤的程序員丝格。
舉一個簡單的例子撑瞧,就是很多語言里都有的 i++ 和 ++i 這兩個“自增”操作(下文合稱“++操作”。很多人喜歡在代碼里使用這兩個東西显蝌,因為這樣可以“節(jié)省一行代碼”预伺。殊不知,節(jié)省掉的那區(qū)區(qū)幾行代碼琅束,比起由此帶來的混淆和錯誤扭屁,其實是九牛之一毛。
從理論上講涩禀,++操作本身就是錯誤的設(shè)計料滥。因為它們把對變量的“讀”和“寫”兩種根本不同的操作,毫無原則的合并在一起艾船。這種對讀寫操作的混淆不清葵腹,帶來了非常難以發(fā)現(xiàn)的錯誤,甚至在某些時候帶來效率的低下屿岂。
相反践宴,一種等價的,“笨”一點的寫法爷怀,i = i + 1阻肩,不但更易理解,而且更符合程序內(nèi)在的一種精妙的“哲學(xué)”原理运授。這個原理烤惊,其實來自一句古老的諺語:你不能踏進同一條河流兩次。也就是說吁朦,當(dāng)你第二次踏進“這條河”的時候柒室,它已經(jīng)不再是之前的那條河!這聽起來有點玄逗宜,但是我希望能夠用一段話解釋清楚它跟 i = i + 1 的關(guān)系:
現(xiàn)在來想象一下你擁有明察秋毫的“寫輪眼”雄右,能看到處理器的每一步微小的操作空骚,每一個電子的流動。現(xiàn)在對你來說擂仍,i = i + 1 的含義是囤屹,讓 i 和 1 進入“加法器”。i 和 1 所含有的信息防楷,以 bit 為大小牺丙,被加法器的線路分解则涯,組合复局。經(jīng)過一番復(fù)雜的轉(zhuǎn)換之后,在加法器的“輸出端”粟判,形成一個“新”的整數(shù)亿昏,它的值比 i 要大 1。接著档礁,這個新的整數(shù)通過電子線路角钩,被傳送到“另一個”變量,這個變量的名字呻澜,“碰巧”也叫做 i递礼。特別注意我加了引號的詞,你是否能用頭腦想象出線路里面信息的流動羹幸?
我是在告訴你脊髓,i = i + 1 里面的第一個 i 跟第二個 i,其實是兩個完全不同的變量——它們只不過名字相同而已栅受!如果你把它們換個名字将硝,就可以寫成 i2 = i1 + 1。當(dāng)然屏镊,你需要把這條語句之后的所有的 i 全都換成 i2(直到 i 再次被“賦值”)依疼。這樣變換之后,程序的語義不會發(fā)生改變而芥。
我是在說廢話嗎律罢?這樣把名字換來換去有什么意義呢?如果你了解編譯器的設(shè)計棍丐,就會發(fā)現(xiàn)误辑,其實我剛剛告訴你的哲學(xué)思想,足以讓你“重新發(fā)明”出一種先進的編譯器技術(shù)骄酗,叫做 SSA(static single assignment)稀余。我只是通過這個簡單的例子讓你意識到,++操作不但帶來了程序的混淆趋翻,而且延緩甚至阻礙了人們發(fā)明像 SSA 這樣的技術(shù)睛琳。如果人們早一點從本質(zhì)上意識到 i = i + 1 的含義(其實里面的兩個 i 是完全不同的變量)盒蟆,那么 SSA 可能會提前很多年被發(fā)明出來。
(好了师骗,到這里我承認(rèn)历等,想用一段話講清楚這個問題的企圖,失敗了辟癌。)
所以寒屯,有些人很在乎 i++ 與 ++i 的區(qū)別,去追究 (i++) + (++i) 這類表達式的含義黍少,追究 i++ 與 ++i 誰的效率更高寡夹,…… 其實都是徒勞的〕е茫“精通”這些細(xì)微的問題菩掏,并不能讓你成為一個好的程序員。真正正確的做法其實是:完全不使用 i++ 或者 ++i昵济。
當(dāng)然由于人們約定俗成的習(xí)慣智绸,在某種非常固定,非常簡單访忿,眾人皆知的“模式”下瞧栗,你還是可以使用 i++ 和 ++i。比如: for (int i=0; i < n; i++)海铆。但是除此之外迹恐,最好不要在任何其它地方使用。特別不要把 ++ 放在表達式中間游添,或者函數(shù)的參數(shù)位置系草,比如 a[i++], f(++i) 等等,因為那樣程序就會變得難以理解唆涝,容易出錯找都。如果你把兩個以上的 ++ 放在同一個表達式里,就會造成“非確定性”的錯誤廊酣。這種錯誤會造成程序可能在不同的編譯器下出現(xiàn)不同的結(jié)果能耻。
雖然我對這些都了解的非常清楚,但我不想繼續(xù)探討這些問題亡驰。因為與其記住這些晓猛,不如完全忘記 i++ 和 ++i 的存在。
好了凡辱,一個小小的例子戒职,也許已經(jīng)讓你意識到了片面追求短小程序所帶來的巨大代價。很可惜的是透乾,程序語言的設(shè)計者們?nèi)匀辉诶^續(xù)為此犯下類似的錯誤洪燥。一些“新”的語言磕秤,設(shè)計了很多類似的,旨在“縮短代碼”捧韵,“減少打字量”的雕蟲小技市咆。也許有一天你會發(fā)現(xiàn),這些雕蟲小技所帶來的再来,除了短暫的興奮蒙兰,剩下的就是無盡的煩惱。
思考題:
- Google 公司的代碼規(guī)范里面規(guī)定芒篷,在任何情況下 for 語句和 if 語句之后必須寫花括號搜变,即使 C 和 Java 允許你在其只包含一行代碼的時候省略它們。比如梭伐,你不能這樣寫
for (int i=0; i < n; i++)
some_function(i);
而必須寫成
for (int i=0; i < n; i++) {
some_function(i);
}
請分析:這樣多寫兩個花括號痹雅,是好還是不好?
- 當(dāng)我第二次到 Google 實習(xí)的時候糊识,發(fā)現(xiàn)我一年前給他們寫的代碼,很多被調(diào)整了結(jié)構(gòu)摔蓝。幾乎所有如下結(jié)構(gòu)的代碼:
if (condition) {
return x;
} else {
return y;
}
都被人改成了:
if (condition) {
return x;
}
return y;
請問這里省略了一個“else”和兩個花括號赂苗,會帶來什么好處或者壞處?
根據(jù)本文對于 ++ 操作的看法贮尉,再參考傳統(tǒng)的圖靈機的設(shè)計拌滋,你是否發(fā)現(xiàn)圖靈機的設(shè)計存在類似的問題?你如何改造圖靈機猜谚,使得它不再存在這種問題败砂?
參考這個《Go 語言入門指南》,看看你是否能從中發(fā)現(xiàn)由于“片面追求短小”而產(chǎn)生的魏铅,別的語言里都沒有的設(shè)計錯誤昌犹?