至今, 你大概已經到處碰過各式各樣的錯誤. 一般上, 你可能看到的錯誤有三種: 編譯時段錯誤 (compile time error) 、執行時段錯誤 (run time error) 、故障的程式碼 (malfunctioning code). 在大多數的 mud 中, 你會找到一個私人的檔案, 裡頭記錄著你的編譯時段錯誤. 對大多數人來說, 你可以在你的家 (home) 目錄找到名叫 "log" 或 ".log" 的檔案, 或在 "/log" 目錄找到以你的名字命名的檔案. 另外, mud 執行時, 會維持一份執行時段錯誤的紀錄. 而此檔案也在 "/log" 目錄中. 對 MudOS mud 來說, 它叫做 "debug.log". 其他的 mud 中, 稱為不同的名字, 像是 "lpmud.log". 如果你還不知道編譯時段和執行時段錯誤紀錄在哪裡, 請詢問你的系統管理者.編譯時段錯誤是 driver 試著載入一個物件到記憶體的時候發生的錯誤. 如果此時它看不懂你寫的東西, 它會無法把物件載入記憶體, 並在你私人的錯誤紀錄檔中記錄為什麼它無法載入該物件. 最普遍的編譯時段錯誤是打字錯誤、遺漏或多加 () , {}, [], ""、沒有正確宣告物件所使用的函式和變數.
執行時段錯誤是一個在記憶體中的物件, 當它執行某段敘述時所發生的錯誤. 舉例來說, driver 不可能知道任何情況下, "x/y" 是否有效. 實際上, 它是一個有效的 LPC 運算式. 但是, 如果 y 的值為 0, 則會發生執行時段錯誤, 因為你不能除以 0. 當 driver 執行一個函式時碰上錯誤, 它放棄執行函式並紀錄在遊戲執行時段錯誤紀錄檔中. 如果有定義、如果玩家是創作者, driver 也會對 this_player() 顯示錯誤訊息, 不然就只對玩家顯示 "什麼 ?". 大多數導致執行時段錯誤的原因, 是不正確的值和試著執行沒有定義運算資料型態的運算式.
不過, 最狡猾的錯誤種類, 就是故障的程式碼. 這些錯誤不會紀錄下來, 因為 driver 永遠不可能知道有地方出錯. 簡單地說, 這種錯誤就是你認為程式碼做的是一件事, 但是實際上它做的是另一件事. 常遇到這種錯誤的人, 一定會認定是 mudlib 或 driver 的錯誤. 每個人都製造過各式各樣的錯誤, 而更常見的不是程式碼不按照它該運作的的方式工作, 而是你錯讀它.
編譯時段錯誤是最常見以及最容易修正的錯誤. 新手程式撰寫人常常因為一些怪異的錯誤訊息, 而感到挫折. 雖然如此, 只要一個人變得習慣於他們 driver 產生的錯誤訊息, 修正編譯時段錯誤就成了例行公事.在你的錯誤紀錄中, driver 會告訴你錯誤的種類, 還有它最後在第幾行注意到該錯誤. 注意, 這不表示此行一定是錯誤實際發生的地方. 除了打字錯誤, 最常見的編譯時段錯誤是遺漏或多加各式括號和引號: (), [], {}, "". 這種是最常困擾新手程式撰寫人的錯誤, 因為 driver 不會注意到遺漏或多加的部分, 直到稍後出問題為止. 以下是範例:
1 int test(string str) {看你想做的是什麼, 此處實際上的錯誤在第三行 (表示你遺漏了一個 {) 或第五行 (表示你多加一個 }) . 但是, driver 會回報它在第六行找到一個錯誤. 實際的 driver 訊息每種 driver 可能都不一樣, 但是不管是哪一種 driver, 你會看到第六行產生一個錯誤. 因為第五行的 } 會解釋為 test() 函式結束. 在第六行, driver 看見你有一個 write() 出現在函式定義之外, 所以回報為錯誤. 一般來說, driver 也會繼續回報它在第七行找到一個多加 } 的錯誤.
2 int x;
3 for(x =0; x<10; x++)
4 write(x+"\n");
5 }
6 write("Done.\n");
7 }修正這種錯誤的秘訣在於程式撰寫風格. 將結束的 } 與該子句開頭的 { 垂直對齊, 在你除錯時, 會讓你看到你哪裡遺漏它們. 同樣, 當你使用多組括號時, 像這樣用空白將各組分開:
if( (x=sizeof(who=users()) > ( (y+z)/(a-b) + (-(random(7))) ) )你可以看到, for() 敘述的括號與其餘的敘述以空白隔開. 另外, 個別的子群也用空白隔開, 讓它們在產生錯誤時易於找出.一旦你擁有幫助你找出錯誤的程式撰寫風格, 你就會學到哪一種錯誤訊息傾向於指出哪一種錯誤. 修正此種錯誤時, 你會檢查出問題的那一行之前與之後的程式碼. 大多數的情況下, 你會直接找到錯誤.
另一種普遍的編譯時段錯誤是 driver 回報一個不明的 identifier. 一般來說, 打字錯誤和錯誤宣告變數導致此種錯誤. 幸運的是, 錯誤紀錄檔中幾乎都能告訴你錯誤所發生的實際位置. 所以修正此種錯誤時, 進入編輯程式並找到出問題的該行. 如果該問題出在變數上而不是打字錯誤, 請確定你正確地宣告該變數. 另一方面, 如果是打字錯誤, 就改正它 !
但是, 小心一件事, 這種錯誤有時候會與遺漏括號的錯誤結合在一起. 在這種情形下, 不明 identifier 的問題常常是誤報. driver 誤讀 {} 或其他東西, 而導致變數宣告混淆. 因此在煩惱此種錯誤困擾之前, 請確定已修正所有其他的編譯時段錯誤.
與前述的錯誤同一級的是普通的語法錯誤. 當 driver 無法了解你寫的東西時, 它就產生此種錯誤. 這又常是打字錯誤引起的, 卻也是因為不了解某些特徵正確的語法所致, 像是把 for() 敘述寫成這樣:
for(x=0, x<10, x++)如果你像這樣的錯誤, 卻不是語法錯誤, 試著重新檢查錯誤發生的敘述中, 語法是否正確.
執行時段錯誤比起編譯時段錯誤要複雜得多. 幸運的是, 這些錯誤都有紀錄, 但是許多創作人並不了解, 或是他們不知道紀錄在哪裡. 執行時段錯誤的紀錄一般也紀錄得比編譯時段錯誤詳細, 也就是你可以從它開始到它出錯之處, 追蹤執行程序的過程. 所以你可以利用紀錄檔, 使用前編譯器敘述 (precompiler statement) 設置除錯陷阱 (debugging trap). 但是, 執行時段錯誤常肇因於複雜的程式撰寫技巧, 而初學者並不使用這些技巧. 這表示你一般會碰上比簡單的編譯時段錯誤還要複雜的錯誤.執行時段錯誤幾乎都是肇因於使用錯誤的 LPC 資料型態. 最常見的是, 試著用 NULL 值的物件變數做外界呼叫, 索引指向 NULL 值的映射、陣列、字串變數, 或函式傳入錯誤的參數. 我們看一個 Nightmare 真實的執行時段錯誤:
Bad argument 1 to explode()除了最後一個以外, 所有的錯誤, 都對一個函式傳入一個錯誤的參數. 第一個錯誤, 是對 explode() 傳入錯誤的第一個參數. explode() 外部函式要一個字串當作第一個參數. 修正這類型的錯誤時, 我們會到 /bin/system/_grep.c 的第 32 行檢查第一個傳入參數到底其資料型態為何. 在此情況下, 傳入的值應是字串.
程式: bin/system/_grep.c, 物件: bin/system/_grep 第 32 行
' cmd_hook' in ' std/living.c' (' std/user#4002') 第 83 行
' cmd_grep' in ' bin/system/_grep.c' (' bin/system/_grep') 第 32 行Bad argument 2 to message()
程式: adm/obj/simul_efun.c, 物件: adm/obj/simul_efun 第 34 行
' cmd_hook' in ' std/living.c' (' std/user#4957') 第 83 行
' cmd_look' in ' bin/mortal/_look.c' (' bin/mortal/_look') 第 23 行
' examine_object' in ' bin/mortal/_look.c' (' bin/mortal/_look') 第 78 行
' write' in 'adm/obj/simul_efun.c' (' adm/obj/simul_efun') 第 34 行Bad argument 1 to call_other()
程式: bin/system/_clone.c, 物件: bin/system/_clone 第 25 行
' cmd_hook' in ' std/living.c' (' std/user#3734') 第 83 行
' cmd_clone' in ' bin/system/_clone.c' (' bin/system/_clone') 第 25 行Illegal index
程式: std/monster.c, 物件: wizards/zaknaifen/spy#7205 第 76 行
' heart_beat' in ' std/monster.c' ('wizards/zaknaifen/spy#7205') 第 76 行如果因為某些原因, 我實際上傳入其他的東西, 我在此只要確定傳入字串就能修正錯誤. 但是在此情況要複雜得多. 我需要追蹤傳入 explode() 的變數值為何, 我才能知道傳入 explode() 外部函式的值到底是什麼.
出問題的那行是:
borg[files[i]] = regexp(explode(read_file(files[i]), "\n"), exp);files 是一個字串陣列, i 是整數, borg 是映射. 所以很明顯, 我們需要找出 read_file(file[i]) 的值到底是什麼. 好, read_file() 這個外部函式傳回一個字串, 除非該檔案根本不存在, 或是該物件沒有權限讀取該檔案, 或是該檔案是個空的檔案, 這些情形都會導致此函式傳回 NULL. 很明顯, 我們的問題是這些情形的其中一種. 要找出是哪一種, 我們要看 file[i].檢查程式碼, 這個檔案陣列透過 get_dir() 外部函式取得它的值. 如果該物件有權限讀取此目錄, get_dir() 就傳回目錄中所有的檔案. 所以問題不在於權限不足或檔案不存在. 導致這個錯誤的檔案一定是空的. 而且事實上, 這就是導致錯誤的原因. 要修正此錯誤, 我們要透過 filter_array() 外部函式傳入檔案, 確定只有檔案大小大於 0 的檔案可以讀入陣列.
修正執行時段錯誤的關鍵在於, 了解有問題的所有變數值在產生錯誤之時, 它們確實的值為何. 你閱讀你的執行時段錯誤紀錄時, 小心地經由錯誤發生的檔案分辨物件. 舉個例子, 上面的索引錯誤是物件 /wizard/zaknaifen/spy 產生, 但是錯誤發生在執行它所繼承的 /std/monster.c 函式.
你所遇到最陰險的問題, 就是你程式碼的行為不是預期中的行為. 物件順利載入, 沒有產生任何執行時段錯誤, 但是事情就是不對勁. 既然 driver 不可能認出這種錯誤, 就沒有任何紀錄. 所以你需要一行接一行瀏覽整個程式碼, 並搞清楚到底發生了什麼事.第一步: 找出你已知能順利執行的最後一行程式碼.常常, 這些問題出現於你使用 if() 敘述沒有料到所有的可能情形. 舉個例:
第二步: 找出你已知開始出錯的第一行程式碼.
第三步: 從已知順利執行的地方到第一個出錯的地方, 檢查程式碼的流程.int cmd(string tmp) {在此段程式碼中, 我們發現它編譯和執行起來沒有問題. 問題是它執行起來完全沒作用. 我們確定 cmd() 函式已經執行, 所以我們可以從此著手. 我們也知道實際上 cmd() 傳回 1, 因為我們輸入此命令時, 沒看到 "什麼 ?" . 馬上, 我們可以看到因為某些原因, tmp 變數有字串或整數以外的值. 就此得到的答案是, 我們輸入的命令沒有參數, 所以 tmp 是 NULL , 並讓所有的測試條件失敗.
if(stringp(tmp)) return do_a()
else if(intp(tmp)) return do_b()
return 1;
}上面的例子相當簡單, 幾近於愚蠢. 但是, 它讓你知道在修正故障的程式碼時, 如何檢查程式碼的流程. 其他的工具能協助你除錯. 最重要的工具就是使用前編譯器來除錯. 以前面的例子來說, 我們有一個子句檢查傳入 cmd() 的整數. 我們輸入 "cmd 10" 時, 我們希望執行 do_b() . 進入迴圈之前, 我們需要看 tmp 的值為何:
#define DEBUG在我們輸入命令之後, 立刻就知道 tmp 的值是 "10" . 回頭看程式碼, 我們會怪自己愚蠢, 忘了我們把 tmp 當整數使用之前, 必須要用 sscanf() 把命令參數轉換成整數.int cmd(string tmp) {
#ifdef DEBUG
write(tmp);
#endif
if(stringp(tmp)) return do_a();
else if(intp(tmp)) return do_b();
else return 1;
}
修正任何 LPC 問題的關鍵是, 永遠要知道你程式碼中任何一步的變數值為何. LPC 的執行降到變數值改變這種最簡單的層級上, 所以程式碼載入記憶體時, 不正確的值導致錯誤發生. 如果你遇到函式有不正確的參數時, 常常是你對一個函式傳入 NULL 值的參數. 這情形常發生於物件, 因為大家常常會做以下的事:1) 使用設定在一個物件中的值, 而該物件已經被摧毀.另外, 大家會常常遇上不合法的索引 (illegal indexing) 或索引指向不合法的型態 (indexing on illegal types). 最常見的是因為有問題的映射或陣列沒有初始化, 所以無法索引之. 關鍵在於了解出問題的地方, 其陣列和映射的完整值. 另外, 注意索引編號是否比陣列的長度還大.
2) 使用 this_player() 的傳回值, 而根本沒有 this_player().
3) 使用 this_object() 的傳回值, 而 this_object() 剛好已被摧毀.最後, 使用前編譯器暫時扔出或扔進顯示變數值的程式碼. 前編譯器讓你很容易地刪掉除錯程式碼. 你只需要在除錯完畢之後, 刪除 DEBUG 定義.
Copyright (c) George Reese 1993