王垠:程序設(shè)計(jì)里的“小聰明”
很早就想寫這樣一篇博文了,可是一直沒來得及動(dòng)筆。在學(xué)校的時(shí)候,時(shí)間似乎總是不夠用,因?yàn)橐坏┯悬c(diǎn)時(shí)間,你就想是不是該用來多看點(diǎn)論文。所以我很高興,工作的生活給了我真正自由的時(shí)間,讓我可以多分享一些自己的經(jīng)驗(yàn)。
我今天想開始寫這系列文章的原因是,很多程序員的頭腦中都有一些通過“非理性”方式得到的錯(cuò)誤觀點(diǎn)。這些觀點(diǎn)如此之深,以至于你沒法跟他們講清楚。即使講清楚了,一般來說也很難改變他們的習(xí)慣。
程序員的世界,是一個(gè)“以傲服人”的世界,而不是一個(gè)理性的,“以德服人”的世界。很多人喜歡在程序里耍一些“小聰明”,以顯示自己的與眾不同。由于這些人的名氣和威望,人們對(duì)這些小聰明往往不加思索的吸收,以至于不知不覺學(xué)會(huì)了很多表面上聰明,其實(shí)導(dǎo)致不必要麻煩的思想,根深蒂固,難以去除。接著,他們又通過自己的“傲氣”,把這些錯(cuò)誤的思想傳播給下一代的程序員,從而導(dǎo)致惡性循環(huán)。人們總是說“聰明反被聰明誤”,程序員的世界里,為這樣的“小聰明”所栽的根頭,可真是數(shù)不勝數(shù)。以至于直到今天,我們?nèi)匀辉谄S趶浹a(bǔ)前人所犯下的錯(cuò)誤。
所以從今天開始,我打算陸續(xù)把自己對(duì)這些“小聰明”的看法記錄在這里,希望看了的人能夠發(fā)現(xiàn)自己頭腦里潛移默化的錯(cuò)誤,真正提高代碼的“境界”。可能一下子難以記錄所有這類誤區(qū),不過就這樣先開個(gè)頭吧。
小聰明1:片面追求“短小”
我經(jīng)常以自己寫“非常短小”的代碼為豪。有一些人聽了之后很贊賞,然后說他也很喜歡寫短小的代碼,接著就開始說 C 語言其實(shí)有很多巧妙的設(shè)計(jì),可以讓代碼變得非常短小。然后我才發(fā)現(xiàn),這些人所謂的“短小”跟我所說的“短小”,完全不是一回事。
我的程序的“短小”,是建立在語義明確,概念清晰的基礎(chǔ)上的。在此基礎(chǔ)上,我力求去掉冗余的,繞彎子的,混淆的代碼,讓程序更加直接,更加高效的表達(dá)我心中設(shè)想的“模型”。這是一種在概念級(jí)別的優(yōu)化,它其實(shí)只是間接的導(dǎo)致了程序的短小精悍。這種短小,往往是在“語義” (semantics) 層面的,而不只是在“語法”層面死摳幾行代碼。我絕不會(huì)為了程序“顯得短小”而讓它變得難以理解,或者容易出錯(cuò)。
相反,很多其它人所追求的“短小”,卻是盲目的,沒有原則的小伎倆。在很多時(shí)候,這些小伎倆都只是在“語法” (syntax) 層面,比如,想辦法把兩行代碼寫成一行??梢哉f,這種“片面追求短小”的錯(cuò)誤傾向,造就了一批語言設(shè)計(jì)上的錯(cuò)誤,以及一批“擅長于”使用這些錯(cuò)誤的程序員。
舉一個(gè)簡單的例子,就是很多語言里都有的 i++ 和 ++i 這兩個(gè)“自增”操作。很多人喜歡在代碼里使用這兩個(gè)東西,是因?yàn)檫@樣可以“節(jié)省一行代碼”。殊不知,節(jié)省掉的那區(qū)區(qū)幾行代碼,比起由于使用自增操作帶來的混淆和錯(cuò)誤,其實(shí)是九牛之一毛。
從理論上講,i++ 和 ++i 本身就是錯(cuò)誤的設(shè)計(jì)。因?yàn)樗鼈儼褜?duì)變量的“讀”和“寫”兩種根本不同的操作,毫無原則的合并在一起。這種對(duì)讀寫操作的混淆不清,帶來了非常難以發(fā)現(xiàn)的錯(cuò)誤,甚至在某些時(shí)候帶來效率的低下。
相反,等價(jià)的一種“笨”一點(diǎn)的寫法,i = i + 1,不但更易理解,而且更符合程序內(nèi)在的一種精妙的“哲學(xué)”原理。這個(gè)原理,其實(shí)來自一句古老的諺語:你不能踏進(jìn)同一條河流兩次。也就是說,當(dāng)你第二次踏進(jìn)“這條河”的時(shí)候,它已經(jīng)不再是之前的那條河!這聽起來有點(diǎn)玄,但是我希望能夠用一段話解釋清楚它跟 i = i + 1 的關(guān)系:
現(xiàn)在來想象一下,你就是超人卡卡西,你擁有明察秋毫的“寫輪眼”,你能看到處理器的每一步微小的操作,每一個(gè)電子的流動(dòng)?,F(xiàn)在對(duì)你來說,i = i + 1 的含義是,讓 i 和 1 進(jìn)入“加法器”。i 和 1 所含有的信息,以 bit 為大小,被加法器的線路分解,組合。經(jīng)過這樣一番復(fù)雜的轉(zhuǎn)換之后,在加法器的“輸出端”,出現(xiàn)了一個(gè)“新”的整數(shù),它的值比 i 要大 1。接著,這個(gè)新的整數(shù)通過電子線路,被放進(jìn)“另一個(gè)”變量,這個(gè)變量的名字,“碰巧”也叫做 i。特別注意我加了引號(hào)的詞,你是否能用頭腦想象出電子線路里面信息的流動(dòng)?
我是在告訴你,i = i + 1 里面的第一個(gè) i 跟第二個(gè) i,其實(shí)是兩個(gè)完全不同的變量——它們只不過名字相同而已!如果你把它們換個(gè)名字,就可以寫成 i2 = i1 + 1。當(dāng)然,你需要把這條語句之后的所有的 i 全都換成 i2(直到 i 再次被“賦值”)。這樣變換之后,程序的語義不會(huì)發(fā)生改變。
我是在說廢話嗎?這樣把名字換來換去有什么意義呢?如果你了解編譯器的設(shè)計(jì),就會(huì)發(fā)現(xiàn),其實(shí)我剛剛告訴你的哲學(xué)思想,足以讓你“重新發(fā)明”出一種先進(jìn)的編譯器技術(shù),叫做 SSA(single static assignment)。我只是通過這個(gè)簡單的例子讓你意識(shí)到,i++ 和 ++i 不但帶來了程序的混淆,而且延緩甚至阻礙了人們發(fā)明像 SSA 這樣的技術(shù)。如果人們?cè)缫稽c(diǎn)從本質(zhì)上意識(shí)到 i = i + 1 的含義(其實(shí)里面的兩個(gè) i 是完全不同的變量),那么 SSA 很可能會(huì)提前很多年被發(fā)明出來。
(好了,到這里我承認(rèn),想用一段話講清楚這個(gè)問題的企圖,失敗了。)
所以,有些人很在乎 i++ 與 ++i 的區(qū)別,去追究 (i++) + (++i) 這類表達(dá)式的含義,其實(shí)是徒勞的。“精通”這些細(xì)微的問題,并不能讓你成為一個(gè)好的程序員。真正正確的做法其實(shí)是:完全不使用 i++ 或者 ++i。當(dāng)然由于人們約定俗成的習(xí)慣,在某種非常固定,非常簡單的,眾人皆知“模式”下,你還是可以使用 i++ 和 ++i。比如: for (int i=0; i < n; i++)。但是除此之外,最好不要在任何其它地方使用。
如果你把它們放在表達(dá)式中間,或者函數(shù)的參數(shù)位置,比如 a[i++], f (++i) 等等,那么程序就會(huì)變得難以理解。而如果你把兩個(gè)以上的 i++ 放在同一個(gè)表達(dá)式里,就會(huì)造成“非確定性”的錯(cuò)誤。這種錯(cuò)誤會(huì)造成程序在不同的編譯器下出現(xiàn)不同的結(jié)果。
雖然我對(duì)這些都了解的非常清楚,但我不想繼續(xù)探討這些問題。因?yàn)榕c其記住這些,不如完全忘記 i++ 和 ++i 的存在。
好了,一個(gè)小小的例子,也許已經(jīng)讓你意識(shí)到了片面追求短小程序所帶來的巨大代價(jià)。很可惜的是,程序語言的設(shè)計(jì)者們?nèi)匀辉诶^續(xù)為此犯下類似的錯(cuò)誤。一些“新”的語言,設(shè)計(jì)了很多類似的,旨在“縮短代碼”,“減少打字量”的雕蟲小技。也許有一天你會(huì)發(fā)現(xiàn),這些雕蟲小技所帶來的,在短暫的興奮之后,其實(shí)是無窮無盡的煩惱。
思考題:
1. Google 公司的“代碼規(guī)范”里面規(guī)定,在任何情況下 for 語句和 if 語句之后必須寫花括號(hào),即使 C 和 Java 允許你在其只包含一行代碼的時(shí)候省略它們。比如,你不能這樣寫
- for (int i=0; i < n; i++)
- some_function (i);
而必須寫成
- for (int i=0; i < n; i++) {
- some_function (i);
- }
請(qǐng)分析:這樣的代碼規(guī)范,是好還是不好?請(qǐng)說明理由。
2. 當(dāng)我第二次到 Google 實(shí)習(xí)的時(shí)候,發(fā)現(xiàn)我的一年前的代碼很多被調(diào)整了結(jié)構(gòu)。幾乎所有如下結(jié)構(gòu)的代碼:
- if (x > 0) {
- return 0;
- } else {
- return 1;
- }
都被人改成了:
- if (x > 0) {
- return 0;
- }
- return 1;
請(qǐng)問這里省略了一個(gè)“else”和兩個(gè)花括號(hào),會(huì)帶來什么好處或者壞處?
3. 根據(jù)本文對(duì)于 ++ 操作的看法,再參考傳統(tǒng)的圖靈機(jī)的設(shè)計(jì),你是否發(fā)現(xiàn)圖靈機(jī)的設(shè)計(jì)存在類似的問題?你如何改造圖靈機(jī),使得它不再存在這種問題?
4. 參考這個(gè)《Go 語言入門指南》,看看你是否能從中發(fā)現(xiàn)由于“片面追求短小”而產(chǎn)生的,別的語言里都沒有的設(shè)計(jì)錯(cuò)誤?