程式設計師存在的目的之一,不就是為了創造臭蟲(Bug)嗎?只是這創造的臭蟲,經常地連創造者自己都找不到,許多開發者在學習的程式過程中,也鮮少教留意過除錯的訓練,反正程式寫久了,自然就知道怎麼除錯,是這樣嗎?
print大法好!
對程式設計來說,除錯有許多意義,多數開發者學習程式設計的第一關卡,就是跟編譯器或者直譯器搏鬥,根據編譯或直譯不通過的訊息找出問題,直到寫出型態正確且符合語法的程式碼。當跨越了第一道關卡後,接下來就是撰寫出正確的程式邏輯,只不過,程式是照你寫的跑,不是照你想的跑,當程式碼存檔、編譯、運行後,結果不如預期的那一瞬間,開發者會怎麼做?
也許是程式設計的第一個實例,總是從螢幕上顯示「Hello, World」的關係,在除錯時使用print之類的函式,將變數值等相關資訊顯示在螢幕,是許多開發者的第一個直覺反應,不可否認地,在手邊沒有個稱手的IDE之時,而程式本身不複雜、規模不大,或者問題範圍小時,print大法確實簡單而實用。
不過或許也因此,許多開發者不管三七二十一,無論哪種除錯場合都使用print了,在十幾年來業界教育訓練經驗中,很少看到能主動使用Debugger除錯的人,比較好的情況會看到使用logging來取代print,然後,某些程式碼流程中,幾乎塞滿為了能在除錯檢視相關資訊時logging,不管是否適合。
某些時候使用print確實方便,而正如先前〈從日誌API認識日誌需求〉,某些時候也確實可以用logging來檢視除錯訊息,然而,當應用程式本身有一定的複雜度,或者程式語言或技術本身掌握上有一定的難度時,單純使用print或logging,就顯得沒有效率,非但難以定位臭蟲,甚至只是縮小問題範圍,都很難達到了,JavaScript或許是最鮮明的例子,在撰寫瀏覽器相關應用時,沒有Debugger輔助下,除錯真的是寸步難行而毫無效率。
從Debugger看除錯
許多時候,除錯的難度並不在於臭蟲本身多困難,而在於難以找到臭蟲的所在地,這是除錯與測試的不同點,測試可以驗證某個函式呼叫結果有錯誤,然而,這錯誤是在函式本身的哪個地方產生呢?或者是函式又呼叫了某個子函式而產生?
這些問題就是除錯時必須釐清的,而Debugger的作用,就是有系統、有效率地逐步縮小範圍,而不是大海撈針式地猜測臭蟲在哪邊。
Debugger的功能之一,就是設置中斷點(break point),除錯時程式運行至中斷點就會停下,因此中斷點的設置技巧之一是,在臭蟲可能出現的步驟之前,不過若無法確定臭蟲活動範圍,這會有點困難;第二個技巧是,在預期會執行到的地方設置中斷點,如果程式沒有如預期般停下,那麼必然是先前流程有了問題,否則就繼續往後設置中斷點,如此一步步「縮小除錯範圍」。
中斷點的設置,就像是在打椿時拉出封鎖線,只是,一直用打椿的方式來接近臭蟲也會缺乏效率,在打椿的範圍內逐步進逼臭蟲這方面,Debugger都有步進操作,最基本方式就是Step Over、Step Into、Step Out,可用來分別「控制進逼臭蟲的速度」。
Step Over就是執行程式碼的下一步。如果下一步是個函式,會執行完該函式至返回,若只是想看看函式的執行結果或傳回值是否正確,就會使用Step Over,這就像察看街道,但先不進入建築物。如果發覺函式執行結果並不正確,可以使用Step Into,顧名思義,若下一步是個函式呼叫,就會進入函式逐步執行,以便查看函式中的演算與每一步執行結果,這就像是懷疑建築物中有臭蟲而進入察看。
如果目前正在某個函式之中,接下來,不想逐步檢視函式中剩餘之程式碼,可以執行Step Out,這會完成目前函式未執行完的部份,並返回上一層呼叫函式的位置,這就像是不用再清查建築物其他部份了,直接回到街頭上繼續找蟲。
無論是停在中斷點,或者是停在Step Over、Step In、Step Out的下一步,最重要的是「察看(Watch)目前周遭環境的相關資訊」,在圖形介面下,有的Debugger工具可以直接將滑鼠放在變數名稱上直接察看,有的可以直接將變數新增在Watch窗格,以便隨時掌握目前狀態。
而這就是使用Debugger,而不是print的理由之一。若察看的值是個物件,而且關聯了其他物件,有的Debugger還能夠逐層解析物件,換作是使用print的話,也許得停下程式來加上幾句print,然後重新運行一次程式,重點是不能停下來察看資訊——開發者還得從螢幕上的訊息分辨出,哪個訊息是在哪裡出現,不想觀看除錯資訊的話,還得除去那些print語句,而效率差異之大,就在於此。
光譜的另一端
聽起來Debugger似乎是個好物,開發者應該多多使用?對於從沒聽過或使用過Debugger,除錯時唯一考量print之類開發者而言,是應該考慮使用Debugger。不過,光譜的另一端,也有著不傾向使用Debugger的聲音出現。在《The Practice of Programming》中第5章〈Debugging〉一開始,就談到:「就個人來說,除了要取得stack trace或幾個變數值之外,我們不傾向使用Debugger」。
身為大師的Brian W. Kernighan與Rob Pike,都不鼓勵使用Debugger了,那麼,還在這邊鼓吹Debugger做什麼呢?實際上,確實有些環境沒有Debugger,不同的環境中Debugger的操作方式也會有所差異,在這類情況下,就必須依賴其他除錯工具、對程式碼的理解、除錯的邏輯與經驗等。
〈Debugging〉該章中,也談到:「盲目地使用Debugger查找是不可能有效率的」,不過也談到:「然而,Debugger是個無價之寶,在你的除錯工具箱中,應該要具備這麼一個工具」。大師們顯然一定知道Debugger,不過,這只會是他們除錯時的工具選擇之一,而不會是唯一,〈Debugging〉該章涉及了在沒有Debugger的情況下,開發者還能有的手段與工具來除錯,這些都是值得瞭解的課題。
建立起除錯邏輯
如果開發者並不是與大師位於光譜的同一側,那麼試著開始使用、熟悉Debugger,也是必要的課題之一。因為,這可以讓開發者專心且迅速地在除錯邏輯的建立與驗證,而不是安插瑣瑣碎碎的print語句;而所謂除錯邏輯的建立與驗證,也並不是學會怎麼設定中斷點、進行步進或察看變數值,而是怎麼縮小除錯範圍、控制進逼臭蟲的速度、察看周遭環境的相關資訊等。
無論是採取什麼手段或工具,實際上除錯時最重要的,都是從中做逆向推理,想辦法縮小或定位問題。除了使用Debugger之外,撰寫測試程式,將程式碼分而治之,將問題局部化,或者在這個過程中順道重構,將程式碼解釋給別人聽(或者是小熊、鴨子等任何開發者想要的對象),其實都是縮小或定位問題的方式之一,有時開發者甚至可以將電腦關掉,離開座位一下,思考一下:是否一開始的演算模型就出了問題,而不是程式碼本身出了問題。如果在思考推理的過程中,已經有具體的線索,或者範圍已經夠小夠明確,使用print作簡單的確認也就並不為過;相對地,若是無法建立除錯邏輯來界定問題範圍,只是盲目地設定中斷點,隨意地步進操作、察看資訊,看似很威地在使用Debugger,其實也只是在浪費時間罷了。