7.1.2 语义或“运行期”错误
语法错误的发现和处理是令人烦恼的,但在编程中会遇到一些真正“令人兴奋”的另一类型的错误——语义错误(semantic error)或称“运行期”错误(runtime error)。这类错误仅当运行一个脚本代码或其他程序时才会发现。换句话说完整有效的代码已经通过解释器或编译器的解释或编译,在执行时产生了错误。术语“运行期错误”通过是指语义错误的结果,也就是说这类错误存在于代码的语义中,当代码运行时它们才变成可见的。
这种区别来自于这种事实:程序编译器或解释器在处理程序代码之前必须建立一种内部代码的描述,涉及多种结构开头和结尾的匹配,以便标明每种结构包含什么内容,然后分析每个句子,以便知道如何执行这个句子。例如,如果在程序代码中有一个if then … else … end if 结构,解释器或编译器做的第一步工作就是分析哪些语句在“then”的部分,哪些在“else”部分。这一步的目的是,在对结构中的if条件进行测试之后,可以决定该到哪个分支去执行。
编译器(诸如在编程语言像visual basic和c++中见到的那种)和解释器(诸如用于像vbscript和jscript那样的脚本语言的解释器)之间真正区别在于:编译器不试图运行程序代码,而是在对源程序进行两次预处理后,形成二进制指令或符号代码,并形成一个.exe文件或.dll文件。解释器不含有代码的文件,而是在运行时逐步执行。
1. 使运行停止的错误
如果程序中含有一个语义错误,通常在运行时可得到提示。如果幸运的话,当错误发生时,程序会停止,这样可以容易地找出错误所在。例如,下面这段程序定义了一个有六个元素的数组。
<%
dim arrvalues(5) to hold six elements, indexed from 0 to 5
arrvalues(6) = "whoops, got an error"
%>
如果试图读或设置下标为6的元素值,可以得到一个运行期错误,如图7-7所示:
图7-7 程序执行结果6
注意这里的错误类型是“runtime”(相当于语义)错误,而不是语法错误。错误信息显示了错误所在行数和错误的描述,有助于我们比较容易地找到相应的错误。但这是一个简单的例子,在更复杂的程序代码中,这种错误可能出现在一些遍历一些值并把它们加到一个数组中程序中。如下所示:
<%
dim arrvalues(5) to hold six elements
for intloop = 0 to intlistcount the number of items in some list
arrvalues(intloop) = request.form("selecteditems")(intlistcount)
next
%>
这种情况下,很可能是得到了过多的列表条目,或者是数组的索引不够,根据代码的要求,可以判断是那种错误,并且能够通过增加数组大小来解决这个错误。
<%
dim arrvalues(10) to hold eleven elements
for intloop = 0 to intlistcount the number of items int some list
arrvalues(intloop) = request.form("selecteditems")(intlistcount)
next
%>
或者相应地设置循环的参数来解决处理这个错误。
<%
dim arrvalues(5) to hold six elements
intarraymax = intlistcount
if intarraymax > 5 then intarraymax = 5
for intloop = 0 to intarraymax only add the first six items
arrvalues(intloop) = request.form("selecteditems")(intlistcount)
next
%>
许多其他运行期错误能够使网页运行停止,诸如一些组件或对象的实例化失败,原因是有progid错误,或者是因为组件没有正确安装。在这些情况下,结果总是给出“activex cannot create object”错误提示信息,后面跟着调用server.createobject方法的行号。
2. 产生错误结果的错误
上面提到,如果遇到一个使程序代码停止的运行期错误,我们可能是幸运的。但是另一种情况是程序能很好地执行,好像什么也没有发生,最后产生一个错误的结果。这是最难发现和解决的错误,因为意识不到哪里出错了。例如,假设有一个网页,这个网页把用户的生日作为日期型的值,并且单独显示日期元素(可以把它们作为三个条目加到一个数据库中)。
<%
get the value from the request and display it
datbirthdate = request.form("birthdate")
response.write "the value you entered is: " & datbirthdate & "<p>"
get the individual date elements
intday = day(datbirthdate)
intmonth = month(datbirthdate)
intyear = year(datbirthdate)
and display them
response.write "day: " & cstr(intday) & "<br>"
response.write "month: " & cstr(intmonth) & "<br>"
response.write "year: " & cstr(intyear) & "<br>"
%>
图7-8是结果,是用美国日期风格月/日/年显示的,好像一切都没有问题。
图7-8 显示生日的屏幕
然而如果输入一个非法日期,或者让输入文本框空着,便得到一个运行期错误,如图7-9所示:
图7-9 错误提示屏幕
(1) 如果不是一位jscript专家
在寻找错误时,这不是一个大问题,因为我们能够迅速发现为什么会出现错误。事实上网页停止运行有助于我们跟踪错误。然而意外的错误可能会发生。例如,用jscript重写程序代码,由于不是一位jscript专家,里面出现一些细小错误。
<%
// get the value from the request and display it
var datbirthdate = new date(request.form("birthdate"));
response.write("the value you entered is: " + datbirthdate + "<p>");
// get the individual date elements
intday = datbirthdate.getday();
intmonth = datbirthdate.getmonth();
intyear = datbirthdate.getyear();
// and display them
response.write("day: " + intday.tostring() + "<br>");
response.write("month: " + intmonth.tostring() + "<br>");
response.write("year: " + intyear.tostring() + "<br>");
%>
图7-10即是运行结果,尽管程序没有停止运行并给出运行期错误,还是马上看出其中有些问题,月份不可能是0。
图7-10 显示生日的屏幕
问题出现的原因在于jscript的getmonth函数返回的结果为0~11范围内的数,因此需要再加1,才能得到正确的结果。
intmonth = datbirthdate.getmonth() + 1;
(2) 衍生错误
即使不把初始值赋给网页去和结果比较,上面这种错误也可能是相当明显的。然而,如果面对的是一个数据库系统,并且没有看到显示出不正确的结果,可能不知道为什么程序不能正确地更新数据库。更糟糕的是,如果简单地把数值做为整型数据存入数据库,可能直到有人试图对这个数据查询时才能发现这个错误。
现在,发现大约有十二分之一的成员出生在0月份可能会使人吃惊,并会引起一些问题。记住,不仅仅是那些1月份出生的人员存在数据库中的信息不正确,而且每个成员都是这样。如果有许多应用程序都能增加和修改这个数据库中的记录,跟踪这个错误可能是艰苦的工作,特别是,不能去查找错误出现在哪个程序行,而是首先要找出错误出现在哪个应用程序中。
(3) 掌握日期的用法
在上面的程序中出现的日期型数据的错误不是非常明显,不论使用都输入什么样的日期,程序代码只能给出0~6之中的值,原因在于编码中的设定,特别是从vbscript转换到jscript时。在jscript中,getday函数返回的周中的某一天,而不是月中的某一天,这等价于vbscript中的weekday函数,getday函数的返回值是0(代表星期日)到6(代表星期六)。
注意vbscript的weekday函数返回1(代表星期日)到7(代表星期六)。
因此,在jscript中由getdate函数获得某月的日期的正确代码是:
…
// get the individual date elements
intday = datbirthdate.getdate();
intmonth = datbirthdate.getmonth() + 1;
intyear = datbirthdate.getyear();
…
运行这段程序便可得到想要的结果,如图7-11所示:
图7-11 显示正确生日的屏幕
7.2 各种运行期错误
本章前面部分展示了一些问题,包括错误如何出现、如何寻找错误和如何处理错误等等。现在更重要的是要掌握能够发生不同种类的错误,并且如何区分这些错误。需要记住的是,如果知道了到哪里去找和寻找什么,调试则是比较容易的。在本章最后,将介绍错误确实出现时如何捕获错误,并且要尽可能早地阻止错误的发生。
在学习这些内容之前,首先要深入了解一下在某阶段肯定会遇到的不同类型的运行期和语义错误,主要讨论以下内容:
· 逻辑错误。
· 脚本运行期错误。
· asp和ssi运行期错误。
· 客户端脚本错误。
7.2.1 逻辑错误
逻辑错误在脚本中通常难于跟踪,因为这些错误常常是产生错误的结果而不中止网页运行。通常只有一些值出现超出边界的情况,如在前面数组实例中看到的那样,错误才显现出来。
然而,在错误和调试环境中,一种算法并不像数学课上所学的那样复杂。从计算的角度看,算法只是指一段能完成某个任务(通常返回某个结果)的程序。
1. 数值超界(数据溢出)
典型的逻辑错误一般涉及到数值,或者是涉及数据溢出等。例如,如果有名为image1.gif、image2.gif等一系列图像,编写以下一段程序随机挑选一幅图像用以显示:
<%
create a random number between 1 and 5
intrandom = cint(rnd() * 5) +1
%>
<img src="<% = "image” & cstr(intrandom) & ".gif" %>">
在网页中创建<img>元素用以指定随机选中的图像,例如:
<img src="image3.gif">
然而,如果碰巧这段程序产生的结果是image6.gif文件。在这种情况下,如果本来仅希望得到在1~5中的一个结果,网页会是一个破碎的图像符号。原因是vbscript中的cint函数将值取整到最近的整数值。为了舍去小数部分,需要使用int或者fix函数代替cint。
2. 运算符号的优先级
其他类型的逻辑错误有按指令计算而出现的错误,例如想用除法时采用了乘法会产生错误的结果。而由于程序中数学运算符号的运行顺序或优先级,会引起一些更难发现的错误,例如,下面这段程序可能会产生不正确的结果。
intresult = intvalue1 * intvalue2 + intvalue3
因为乘法比加法有较高的运算优先级,所以先进行计算。但是如果想把第一个数和后两个数的和相乘,必须用括号来改变这种缺省的运算优先权。
intresult = intvalue1 * (intvalue2 + intvalue3)
在vbscript 5.0文档中的vbscript basics| vbscript operators中,给出了所有脚本运行符号的优先级表。对于jscript,在jscript tutorial|jscript basic|jscript operators下也可找到相应的优先级表。然而需要记住的最基本原则是:乘、除法优先于加、减法。
3. 管理和格式化字符串数据
从计算意义上考虑,具有计算功能的任何结构或函数都可看作一种算法。例如,可以从数据库中取值构成一个字符串,代表顾客的名字。这里不涉及如何从数据库中提取数据(本书的后面部分进行讨论)。下面程序的功能是字符串连接。
strtitle = {get from database}
strfirstname = {get from database}
strmiddleinitial = {get from database}
strlastname = {get from database}
strother = {get from database}
strprint = strtitle & ". " & strfristname & " " & strmiddleinitial _
& ". " & strstrlastname & " " & strother
运行这段程序可以得到如下结果:
ms. janet c. clarke mbna.bsc.mecheng.
但不是每个人都和“janet”一样,有一个中间名字。并且许多人可能没有头衔,所以可能仅仅得到:
. alex . homer
这当然不是一个能引起脚本不能运行或者产生运行期错误的致命错误。然而,对于用户来说,提供这样的脚本是不可接受的。最好程序能在输出字符串之前检查名字的每一部分。
…
strprint = ""
if len(strtitle) then strprint = strprint & strtitle & ". "
if len(strfirstname) then strprint = strprint & strfirstname & " "
if len(strmiddleinitial) then strprint = strprint & strmiddleinitial & ". "
if len(strlastname) then strprint = strprint & strlastname
if len(strother) then strprint = strprint & " " & strother
上面这段程序保证了空格和小数点仅加在名字中有值的地方。如果仅给strother字符串赋值,而对其他都不赋值的话,将在开始处得到一个空格。然而出现这种情况的可能性非常小。如果有姓的话,通过仅添加“other”部分可以防止这种错误的发生。
…
strprint = ""
if len(strtitle) then strprint = strprint & strtitle & ". "
if len(strfirstname) then strprint = strprint & strfirstname & " "
if len(strmiddleinitial) then strprint = strprint & strmiddleinitial & ". "
if len(strlastname) then
strprint = strprint & strlastname
if len(strother) then strprint = strprint & " " & strother
end if
最坏的情况是结果为一个空字符串,可以检查这种可能性并中止打印。
…
if len(strprint) = 0 then
response.clear
response.end
end if
7.2.2 脚本运行期错误
使用一个不存在的函数,或者破坏了脚本语言使用的规则,会出现脚本运行期错误。许多错误是语法错误(本章前面讨论过的),但是许多错误是由于所赋的值和函数参数的要求不一致引起的。例如,用一个窗体收集来自用户的日期,并存入数据库中,或者用其他方式进行处理。为了确定日期是有效的,在把数据插入数据库之前使用cdate函数:
<%
strdate = request.form("thedate")
datdate = cdate(strdate)
…
如果用户在填表时出现了差错,程序便会产生一个脚本错误,如图7-12所示:
图7-12 出错信息的屏幕
查看错误信息,可以发现错误是由执行程序代码的脚本引擎产生的。错误号用十六进制显示出来,它是由vbscript错误号和十六进制数0x800a0000相加得到的(见第4章),上例中vbscript错误号是十六进制0xd,或者十进制数的13。
大多数微软技术(包括asp)返回的错误号是由8位十六进制数组成的。第一位字符总是8,表明这个状态信息是服务器错误信息。后面跟着2位0,然后是服务代码。对vbscript和jscript错误,服务代码总是“a”,最后4位字符是用十六进制数表示的错误号。
如果查看一下vbscript文档,你会发现13号错误是“type mismatch”错误。当然,我们从asp错误页中显示的错误描述中已经知道了这一点。然而,在本章后面我们将要看到,在错误处理技术中,得到错误号是非常有用的。
注意,在错误信息显示窗口中,显示的是服务器对错误的反馈信息。http状态代码为500.100,属于“internal server error”。在第4章,讨论asp定制错误网页的工作方式时,我们发现这种错误常常因为载入了错误网页。本章后面,将会看到在网页中如何处理这些错误。
7.2.3 asp和ssi的运行期错误
脚本错误是由正在使用的脚本引擎发现的,然而asp dll和ssi dll也能发现脚本错误,尽管它们与使用的脚本引擎无关。典型的ssi例子是在#include指令中给文件一个错误的名字或路径。错误是由ssi dll或asp发现的,而不是由脚本引擎发现。可看到此时错误类型是“active server pages”,asp内部错误代码是“asp 0126”,如图7-13所示,然而在这种情况下,错误号是4005,指出了这是一种ssi dll(ssinc.dll)定义的特殊错误。
图7-13 出错信息的屏幕
asp错误代码总览
对于在asp dll中造成失败的错误,表7-1是返回的错误代码。当这类错误发生时,你可以在asperror对象的aspcode属性中找到这些错误代码。
表7-1 asp错误代码
错误代码
错误消息和扩展信息
asp0100
out of memory(内存溢出)
asp0101
unexpected error(函数返回exception_name)
asp0102
expecting string input(期待字符串输入)
asp0103
expecting numeric input(期待数字输入)
asp0104
operating not allowed(操作不允许)
asp0105
index out of range(数组下标溢出)
asp0106
type mismatch(数据类型不匹配)
asp0107
stack overflow(处理的数据量超过了允许的范围)
asp0115
unexpected error(出现在外部对象中的可捕获的错误exception_name,脚本不能继续运行)
asp0177
server.createobject falied(无效的progid)
asp0190
unexpected error(当释放外部对象时,出现的可捕获的错误)
asp0191
unexpected error(当外部对象的onstartpage方法中出现的可捕获的错误)
asp0192
unexpected error(在外部对象的onendpage方法中出现的可捕获的错误)
asp0193
onstartpage failed(在外部对象onstartpage方法中出现错误)
asp0194
onendpage failed(在外部对象的onendpage方法中出现错误)
asp0240
script engine exception(脚本引擎从object_name抛出异常exception_name)
asp0241
createobject exception(object_name的createobject方法所导致的异常exception_name)
asp0242
query onstartpage interface exception(查询对象object_name的onstartpage或onendpage方法所导致的异常exception_name)
asp错误通常仅当组件有问题或服务器本身有问题时才出现。最常见是使用server.createobject时的asp 0177错误和严重的asp 0115错误。asp 0115错误通常表示组件程序代码中发生的错误,而asp 0177错误通常是由不能正确安装组件引起的或者由我们指定的progid字符串的错误引起的。
