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字符串的錯誤引起的。