越是自認為有經驗的開發者,越是會對他人犯下的某些Bug指指點點。「goto fail?蘋果怎麼會犯這種低級的Bug?」
越是經驗老到的開發者,越會為此感到懊悔。「因為一個低級的Bug,害我浪費了好幾個小時(甚至一整天)的時間。」
感覺滿矛盾的,經驗多寡跟Bug低不低級無關嗎?
語言的gotcha
先別管什麼是低級的Bug了,想想看,你在學習新語言時,就算是最簡單的"Hello, world",是不是也出過錯呢?
我不是在說第一門程式語言,就算開發者已經學過好幾門程式語言,也可能在最初級的小範例中犯下錯誤,也許是大小寫有誤,也許是括號不對,為什麼會犯這類低級的錯誤?因為你對語言不夠瞭解,看不出這類寫法其實是個錯誤!
隨著編譯器或直譯器指出的錯誤越多,開發者修正的東西越多,對語言越來越瞭解,慢慢地,就會將一些錯誤歸類為低級的Bug,或者是新手才會犯的錯誤。
然而,無論一個的語言入門是困難或容易,都會有其怪異之處,瞭解這些特性,避免誤用而產生低級的Bug,往往也是語言學習的一部份,有幾個英文名詞用來形容這類特性,像是quirk、pitfall、trap等,對我來說,最傳神的名詞則gotcha,就像在你浪費了部份生命,終於找出Bug所在,會脫口而出的話——「got you(抓到你了)」。
往往地,入門越是簡單、越廣為使用的語言,越會有這類gotcha,在Java正吃香且被視為簡單的(相對於C/C++)那個年代,就曾出了本《Java PuzzlersTraps, Pitfalls, and Corner Cases》,細說Java語言中這類看似低級的Bug,雖說是低級,裏頭許多的案例,還真有不少就算是Java老手,也很難一眼就察覺出來的錯誤!現實中確實遇過類似的案例。
不管使用哪種語言,對語言中的gotcha認識越多,才越能避免低級Bug出現的機會,在網路上,搜尋語言名稱加上quirk、pitfall、trap、gotcha或commonmistake等字眼,通常就能找到不少文件可以閱讀。
另一個方向的搜尋,是尋找語言的good parts,並在程式撰寫時多利用語言中這類優良部份,若想系統性地吸收這類知識,也可以找尋《EffectiveJav a》、《Ef f e c t i ve P y thon》、《Ef f e c t iveJavaScript》等,這類書籍有著指標性的意義,代表著一門語言已經被廣為使用,並且有著相當的實踐經驗累積下來,包括那些難解的gotcha。
讓工具幫上一點忙
關於低級Bug,蘋果在2014年二月的SSL/TLSBug,大概是近期內最著名的一個案例:
if ((err = SSLHashSHA1.update(&hashCtx,&signedParams)) != 0) goto fail; goto fail;
撇除goto的爭議不說,這樣的錯誤,基本上是因為不良的寫作慣例,才會發生,因為goto fail的縮排在同一層,Code Review也不容易發現。就現在程式設計的實踐來說,就算只有一行陳述句,也別省略花括號,讓程式碼明確地位於同一區塊之中,不過如果看一下當中其他程式碼,會發現有些地方程式碼排版格式滿不一致的,縮排有四個空白,也有八個空白,有些甚至於沒有縮排,例如:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx))!= 0) goto fail;
上面這程式,其實出現了六次,這就讓我聯想到近來研究Go語言,它有個gofmt工具程式,強制使用固定的格式,確實有其道理。在閱讀、撰寫或修改程式碼之前或之後,使用一些編輯器或其他工具提供的格式化功能,重新編排一下程式碼,基本上,不是壞事,就像我在〈跳入程式屎坑〉中說的:「機器一成不變按照規則來編排,會比肉眼編排更為可信」。
另一方面,如果能事先檢查出goto fail之後無法執行的程式碼,也能避免這類錯誤,在〈由蘋果的低級Bug想到的〉(htt p://goo.gl/mneMkO)中,提到一些可能的做法,像是藉助靜態分析工具來檢查這類問題。
暫時做點別的事
低級的Bug之所以低級,往往是因為你實際上看了不下n遍,然而不管怎麼看,每個程式區塊或設定等看似都是對的,然而整個流程執行下來,結果卻是錯的,在幾個小時甚至一整天之後,才赫然發現,Bug明明就在那邊,可是就像幽靈一樣,之前卻怎樣也看不到它,浪費時間的同時,只能嘲笑自己在搞笑了。
問題也許就在於你重複看了n遍,然而,每一次都只是慣性地循著同樣的思路或經驗,試圖找出錯誤,這就像是拿著選臺器不斷地轉換電視頻道,冀望下個頻道,就能找到想看的節目一樣。你以為自己在思考Bug的原因,然而,只是不斷地重複無意義的動作。
這時候,暫時做些別的動作,會有助於脫離慣性的思考模式,也許先別急著找Bug了,若想做些別的,但仍是貼近程式設計行為的動作,大概是整理程式碼,除了做些排版之外,也許對程式進行一下重構,讓程式碼變得更可讀一些,往往地,在重構的過程中,Bug就突然現身了。
或者是因為程式碼重構過了,下次整個重看程式時,能因為觀看程式碼的流程有了些變化,就突然想到是哪出了問題。
暫且對程式碼寫些測試,往往也能奏效,人腦作為編譯器或執行環境的問題就是,你以為這塊程式會是A的結果,然而,實際上編譯或執行後的結果卻是B;有時也不見得是程式流程出了問題,而是一開始程式所接收的輸入就不是正確的,然而你卻一直覺得是正確的輸入,這有點像是思考安全的問題,開發者總是容易慣性地認為,使用者一定會提供正確的輸入,然而,思考安全問題或除錯時,往往是要從另一個方向來設想。
有時不見得要做與程式設計相關的行為,起身動一下、倒倒水、上個廁所之類的,就突然想到哪裡有Bug了。
要從改變慣性的角度來說的話,大概是暫時改變了腦袋中血液流動的方向,而觸發了腦袋中另一區塊的運作吧!也許更好的方式,是找個剛好也起身的同事,或者對桌子旁的小黃鴨聊聊天,說說剛才遇到的問題,或許解決方式就自動出現了,這可不是開玩笑的,聽過橡皮鴨除錯法(Rubber DuckDeBugging)嗎?
開發者都會犯低級錯誤
基本上,只要是開發者,無論程度,無論是在哪間公司或大廠商,都有可能寫出低級的Bug,沒有人會想要製造錯誤,然而錯誤還是發生了,事後直接把錯誤攤在你的眼前,當然錯誤顯而易見,然而,就算當時換作是你,可不見得就能察覺出錯誤,畢竟你不知道,就當時的時程、人力、資源等各方面限制下,自己是否仍能有顆清楚的腦袋,做出正確的判斷。
當然,我們還是得從錯誤中學習,無論是對語言技術有更深的認識,或者從工具、慣例、重構、可讀性、測試等各方面下手,減少低級錯誤發生的機會,如果在這樣的一個改進過程下,仍然寫出了一個低級Bug,也是一種從錯誤中學習的機會。
當然,在沒有任何學習下就蠻幹程式,或者發生Bug只是單純修正,而毫無檢討改進,致使程式中一而再、再而三地出現相同或類似錯誤,此時,就根本沒資格稱這些錯誤為低級的Bug,而是完完全全的愚蠢了!