至今, 你大概已经到处碰过各式各样的错误. 一般上, 你可能看到的错误有叁种: 编译时段错误 (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) { 2 int x;看你想做的是什麽, 此处实际上的错误在第叁行 (表示你遗漏了一个 {) 或第五行 (表示你多加一个 }) . 但是, driver 会回报它在第六行找到一个错误. 实际的 driver 讯息每种 driver 可能都不一样, 但是不管是哪一种 driver, 你会看到第六行产生一个错误. 因为第五行的 } 会解释为 test() 函式结束. 在第六行, driver 看见你有一个 write() 出现在函式定义之外, 所以回报为错误. 一般来说, driver 也会继续回报它在第七行找到一个多加 } 的错误.
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