加菲猫注:写程序有明暗两条线,一条是明线即正常流程,一条是暗线异常流程,其中错误处理就是暗线处理中最重要的一部分。
本文虽然是举例VFP8,VFP9一样是可用的。
VFP 8 有了结构化错误处理,它的特征是新的 TRY...CATCH...FINALLY...ENDTRY 结构。这个强大的新功能向我们提供了错误处理的第三个层次,并让你可以省下大量的传递和处理错误信息的代码。这个月,Doug Hennig 讨论了结构化错误处理,并演示了怎样将它集成到一个完整的错误处理策略中去。
正文:
VFP 3 通过给对象增加了一个 Error 方法极大的增强了错误处理能力。它让对象可以封装它们自己的错误处理,并且不需要依赖于全局的错误处理器。不过,把代码放在对象的 Error 方法中的一个缺点是:它排斥了 ON Error 命令的使用。这么做是有道理的,因为不然的话就会破坏错误处理的封装。但是,这就导致了一个问题,如果一个对象的 Error 方法代码中调用了过程代码(例如一个 PRG 文件)或者另一个 Error 方法中没有代码的对象的某个方法,当被调用代码中出现一个错误时,触发的是调用对象的代码..即使被调用代码中自己有一个本地的 ON Error 错误处理器也是这样。这种机制存在着两个问题:
l 许多错误可以预先就被估计到,例如试图打开一个表,而这个表已经被别人独占使用了。然而,由于被调用程序的 ON Error 错误处理器不会被触发,那么这个程序就没有机会去处理它自己的错误。
l 由于调用对象不可能预知到被调用对象会发生什么错误,除了那些一般性的处理方法(记录下错误信息,向用户显示一个错误信息对话框、然后退出等等)以外,调用对象对被调用对象中发生的处理简直可以说是束手无策。很显然,我们需要一个更好的机制。幸运的是,VFP 8 给了我们一个更好的工具:结构化错误处理。
结构化错误处理
C++ 已经有结构化错误处理很长时间了。.NET 把结构化错误处理提供给了那些以前缺少它的语言,例如 VB.NET。那么到底什么是结构化错误处理呢?结构化错误处理意味着被执行的代码是在一个特殊的块、或者结构中的,当这段代码中发生任何错误的时候,这个结构中的另一块代码会处理它。
VFP 8 通过下面的途径实现了结构化错误处理:
. TRY...ENDTRY 结构让你可以运行可能会发生错误的代码, 并在这个结构中处理错误。这个结构会屏蔽所有其它的错误处理器。
. 一个新的THROW 命令让你可以将错误传递给一个更高层次的错误处理器。
. 一个新的Exception 基础类提供了一种面对对象的传递错误信息的途径。
TRY, TRY again
结构化错误处理的关键, 是新的TRY...ENDTRY 结构。这里是它的语法:
try
[ TryCommands ]
[ Catch [ to VarName ] [when lExpression ]
[ CatchCommands ] ]
[ exit ]
[ throw [uExpression ] ]
[ Catch [ to VarName ] [when lExpression ]
[ CatchCommands ] ]
[ exit ]
[ throw [uExpression ] ]
[ ...(其它的catch 代码块)]
[ finally
[ FinallyCommands ] ]
endtry
TryCommands 表示VFP 将试着去运行的命令。如果没有错误发生, 接下来在可选的FINALLY 代码块中代码将会被执行( 如果有FINALLY 代码块的话), 最后执行在Endtry 之后的代码。如果在Try 块中发生了任何错误,VFP 会立即退出这块代码并开始执行那些Catch 语句。
如果一个Catch 语句中有VarName,VFP 会建立一个Exception 对象,用错误的信息填充这个对象的属性,然后把对这个对象的一个引用放到VarName 变量中去。VarName 只可以是一个正常的变量, 不能为一个对象的属性。如果你前面声明过这个变量, 那么这个变量的有效范围就是你声明的那种( 比如LOCAL);如果你没有声明, 那么这个变量的有效范围就是PRIVATE 。稍候我们将探讨一下Exception 基础类。
如果用上了可选的WHEN 子句, 那么CATCH 语句就象是CASE 语句一样。在WHEN 子句中的表达式返回的必须是一个逻辑值, 以便VFP 能决定该怎么办。如果这个表达式的结果是.T., 那么该CATCH 语句块就会被执行。如果为.T., 则VFP 会转到下一个CATCH 语句。这样一直继续下去直到碰到某个WHEN 表达式返回.T. 的CATCH 语句、一个没有WHEN 子句的CATCH 语句、或者没有别的CATCH 语句了( 稍候我会谈谈最后这种情况)。通常情况下,WHEN 表达式会查看一下Exception 对象的一些属性( 例如ErrorNo, 它表示错误号)。
一旦VFP 找到一个可用的CATCH 语句, 在这一块中的代码就会被运行, 这块代码运行完毕之后, 接着被运行的, 是在可选的FINALLY 块( 如果有这个块的话) 中的代码。
最后则继续运行在ENDTRY 之后的代码。
下面的示例来自附带的SimpleTry.prg 文件, 它演示了CATCH 语句是怎么被运算的、
以及TRY 结构是怎么覆盖ON ERROR 设置的:
on error llError = .T.
llError = .F.
try
wait window xxx
catch to loException when loException.ErrorNo = 1
wait window 'Error #1'
catch to loException when loException.ErrorNo = 2
wait window 'Error #2'
catch to loException
lnError = loException.ErrorNo
messagebox('Error #' + transform(lnError) + chr(13) + ;
'Message: ' + loException.Message)
finally
messagebox('Finally')
endtry
on error
messagebox('ON ERROR ' + iif(llError, 'caught ', ;
'did not catch ') + 'the error')
如果VFP 没有找到一个可用的CATCH 语句, 就会弹出一个“ 未处理的例外错误”( 错误号2059)。有许多原因你会不想让这种情况发生, 其中最大的问题是:这样一来, 原来的错误就不可能解决了, 因为我们手头就只有那个“ 未处理的例外错误” 的错误信息了。
下面的例子演示了当有一个未处理的异常时将会发生的事情( 取自UnhandledException.prg)。当你运行这段代码的时候,你将会看到弹出一个错误信息告诉你,这是一个未处理的异常, 而不是引起这个错误的原因:变量XXX 不存在。
on error do ErrHandler with error(), program(), lineno()
try
wait window xxx
catch to loException when loException.ErrorNo = 1
wait window 'Error #1'
catch to loException when loException.ErrorNo = 2
wait window 'Error #2'
finally
messagebox('Finally')
endtry
on error
procedure ErrHandler(tnError, tcMethod, tnLine)
local laError[1]
aerror(laError)
messagebox('Error #' + transform(tnError) + ;
' occurred in line ' + transform(tnLine) + ' of ' + ;
tcMethod + chr(13) + 'Message: ' + message() + ;
chr(13) + 'Code: ' + message(1))
后退无门
结构化错误处理和其它VFP 错误处理方式的一个重要的区别是:你不能返回到代码出错的地方。在Error 方法或者ON ERROR 例程中有几种可以继续的办法:
. Return( 或者一个没有Return 语句的隐式Return) 返回到出错代码的下一行。但是, 由于导致出错的那一行代码没有完成自己的工作( 例如初始化一个变量或者打开一个表等等), 那么这么做通常会导致发生其它的错误。
. Retry 返回到出错的那一行代码。除非错误奇迹般的自己消失了,否则肯定会再发生同样的错误。
. QUIT 直接退出应用程序。
. Return To 返回到调用堆栈上的一个例程, 例如一个包含着Read Events 语句的程序。如果你不想退出应用程序, 而又不愿意返回到出错的那个例程的时候, 这个命令是非常有用的。当然,这并不意味着一切OK 了, 但当发生的并非是什么灾难性的错误的时候( 例如当进入表单的时候发生了一个简单的资源占用问题), 它通常让用户可以做些什么。
在一个TRY 结构的情况下, 一旦发生了一个错误, 那么TRY 块里运行的代码就立即被结束, 并且你再也不能返回到那里面去。如果你在CATCH 块中( 事实上, 在这种结构的任何地方都不能用) 使用了Return 或者Retry, 就会发生2060 号错误。当然, 你还是可以用QUIT 来退出应用程序的。
用 FINALLY 来擦屁股
搞清楚 FINALLY 子句为什么会重要着实花了我一点时间。毕竟,不论是否发生了错误,在 ENDTRY 语句后面的代码都会被运行。不过我后来发现事情并非如此..一会儿我讨论 THROW 命令的时候你就会知道了,错误可以被“冒泡”给下一个更高级别的错误处理器,而且很有可能就不再从这个高级别的错误处理器中返回了。这样的话,我们就不能保证在 ENDTRY 后面的代码一定会被运行。我们能保证的是:在 FINALLY 块中的代码肯定会被运行(不过事情总是没有绝对的..如果一个COM 对象的错误处理器调用了COMReturnError()函数,就会立即返回到COM 客户端)。
这里是一个演示这种情况的例子(UsingFinally.prg)。对 ProcessData 函数的调用被封装在一个 TRY 结构中。ProcessData 自己也有一个TRY 结构,不过它只处理不能独占的打开表错误。所以,WAIT WINDOW XXX 错误就不会被捕捉到。结果,错误将会被冒泡给在主程序中的外层的 TRY 结构。这样的话,跟在 ProcessData 的 EndTry 语句后面的代码就不会被运行。如果把 ProcessData 中 FINALLY 块里的代码给注释掉,当程序结束时你会看到 Customers 表还打开在那里。不注释掉这段代码再运行的话..你将看到这一次Customers 表被关闭了,这样代码就把自己的屁股擦干净了。
try
do ProcessData
catch to loException
lnError = loException.ErrorNo
messagebox('Error #' + transform(lnError) + ' occurred.')
endtry
if used('customer')
messagebox('Customer table is still open')
else
messagebox('Customer table was closed')
endif used('customer')
close databases all
function ProcessData
try
use (_samples + 'data\customer') exclusive
* 做一些处理
wait window xxx
* 处理器不能独占的打开表
catch to loException when loException.ErrorNo = 1705
* 不管用什么清理代码
* 把这部分先注释掉、再取消注释,运行下看看有什么区别
finally
if used('customer')
messagebox('Closing customer table in FINALLY...')
use
endif used('customer')
endtry
* 现在再清理是没用的,代码不会被运行,因为错误已经被冒泡出去了。
if used('customer')
messagebox('Closing customer table after ENDTRY...')
use
endif used('customer')
Exception 对象
Visual FoxPro 8.0 包含有一个新的 Exception 基础类,以为传递错误信息提供一个面对对象的解决方案。如你前面所见,Exception 对象是当你在 CATCH 命令中使用了 To Varname 的时候被建立的。还有一种情况就是当你使用 Throw 命令的时候也会建立 Exception 对象,这个稍后再说。
除了常见的那些属性、事件、方法(Init、Destroy、BaseClass、AddProperty 等等)以外,Exception 还有一套关于一个错误的信息的属性..如表1 所示。所有这些属性都是运行时可读写的。
属性 | 类型 | 相似函数 | 功能 |
Details | 字符型 | SYS(2018) | 关于错误的其它信息(例如不存在的变量或者文件的名称等等),若没有则为Null |
ErrorNo | 数值型 | Error() | 错误号 |
LineContents | 字符型 | MESSAGE(1) | 导致错误发生的那行代码 |
LineNo | 数值型 | LINENO() | 行号 |
Message | 字符型 | MESSAGE(1) | 错误消息 |
Procedure | 字符型 | PROGRAM() | 发生错误的过程或者方法 |
StackLevel | 数值型 | ASTACKINFO() | 该过程的调用堆栈层数 |
UserValue | 变体型 | 没有相应的函数 | 在一个 Throw 语句中指定的表达式 |
加菲猫注: UserValue多层抛出会包含上一层的Exception 对象
把它扔给我
结构化错误处理的最后一部分是新的 Throw 命令。Throw 就象是原有的 Error 命令.
它把错误的情况传递给一个错误处理器..不过,它的工作方式是相当不同的。这里是这个命令的语法:throw [uExpression]
uExpression.可以是你希望的任何东西, 例如一个message 、一个数字、或者一个Exception 对象。
如果指定了uExpression,THROW 会建立一个Exception 对象,并把它的ErrorNo 属性设置为2071 、把Message 属性设置为“User Thrown Error”、以及把UserValue 属性设置为uExpression 。如果没有指定uExpression, 那么若当前已有一个Exception 对象( 就是当错误发生的时候被Catch to 命令建立的那个),则该Exception 对象被使用;若当前没有,则会新建一个Exception 对象。不管是哪一种情况, 接下来它会把Exception 对象传递给更高一层错误处理器( 通常指的是包含着“ 调用Throw 语句的当前TRY 结构” 的一个TRY 结构)。这里是TestThrow.prg 中的一个例子:
TRY
TRY
WAIT WINDOW xxx
CATCH TO loException WHEN loException.ErrorNo = 1
WAIT WINDOW "Error #1."
CATCH TO loException WHEN loException.ErrorNo = 2
WAIT WINDOW "Error #2."
CATCH TO loException
Throw loException
ENDTRY
CATCH TO loException
lnError = loException.ErrorNo
lcMessage = loException.Message
MESSAGEBOX(‘Error #’ + transform(lnError) + chr(13) + ;
‘Message: ’ + lcMessage, 0, ‘Thrown Exception’)
lnError = loException.UserValueErrorNo
lcMessage = loException.UserValueMessage
MESSAGEBOX(‘Error #’ + transform(lnError) + chr(13) + ;
‘Message: ’ + lcMessage, 0, ‘Original Exception’)
ENDTRY
** 现在再来一遍, 不过Throw 语句不带任何表达式
TRY
TRY
WAIT WINDOW xxx
CATCH TO loException WHEN loException.ErrorNo = 1
WAIT WINDOW "Error #1"
CATCH TO loException WHEN loException.ErrorNo = 2
WAIT WINDOW "Error #2"
CATCH TO loException
Throw
ENDTRY
CATCH TO loException
lnError = loException.ErrorNo
lcMessage = loException.Message
MESSAGEBOX(‘Error #’ + transform(lnError) + chr(13) + ;
‘Message: ’ + lcMessage, 0, ‘Thrown Exception’)
ENDTRY
也许你会认为上面代码中的THROW loException 命令是把loException 掷出给上一层的错误处理器,其实并非如此。THROW 掷出的是一个新建的Exception 对象,loException 则被放在这个新Exception 对象的UserValue 属性里面。因此,在前一个外部TRY 结构中的代码表明该TRY 结构接收到的是一个用户掷出错误,要获得真正的错误信息,你必须从对象引用UserValue 来取得内部Exception 对象的属性。
上面代码中的第二个例子表明,如果有Exception 对象,那么THROW 自己会去重新掷出这个Exception。在这种情况下,外部TRY 结构获得的与内部TRY 结构建立的是同一个对象,所以,外部错误处理器不需要去引用这个对象的UserValue 属性。
你可以在一个TRY 结构的外面使用Throw 语句,但这样做不会带来什么比Error 命令更多的好处,因为不管何种情况下,Throw 语句掷出的Exception 对象都必须要有一个能捕捉到它的TRY 结构,若没有的话,则VFP 的错误处理器会被调用。事实上,如果真的有什么不属于TRY 结构的东西捕捉到了这个Exception,那才是个令人困惑的问题,因为只有TRY 结构才能捕捉到被掷出的Exception 对象。在Error 方法或者ON ERROR 例程的情况下,被接收的参数和AERROR()的结果所关联的,将是Throw 语句和未处理的exception 错误,而不是为什么使用了Throw 语句的原因。Exception 对象的某些属性将被放在由AERROR() 函数填充好了的数组的第2 或第3 列中,以便错误处理器能够分析这些列。无论如何,这都不能算是一种处理错误的正确方式。所以,请保证Throw 语句被使用的时候,有一个TRY 结果能够捕捉到它。
天堂里的烦恼
在VFP 的错误处理机制中最大的问题是:怎么在错误状况下,阻止错误的发生。我说“错误状况”的意思指的是错误当前已经发生了,并且,已经被某个对象的Error 方法或者全局ON Error 错误处理器设下的陷阱所捕捉到..但是,又还没碰到一个Return 或者Retry 语句。在这个状况中,如果出现了什么新的错误,那么就没有什么安全的办法可以捕捉到这个错误..通常情况下,用户会得到一个VFP 错误对话框,带着一个VFP 错误消息以及Cancel 和Ignore 按钮。所以说,你的整个错误处理机制必须是你的应用程序中Bug 最少的部分,并且你还必须对那些并非Bug 而是由环境引起的问题进行测试。
例如,假定你的错误处理器会把错误情况记录到一个叫做ERRORLOG.DBF 的表中。那么,要是这个表不存在的话该怎么办?
你必须用FILE()去检查这个表是否存在,如果没有的话就要建立这个表。如果这个表已经被别的什么地方的代码独占的打开了呢?你可以最大限度的减少独占打开该表的机会,以减少错误发生的机会,但是,要做到绝对的安全的话,你应该先用一下FOPEN()试试能否打开它,因为FOPEN()如果不能打开的话会返回一个错误代码而不是导致发生一个错误。
如果Errorlog.dbf 文件存在,并且也可以用FOPEN()来打开它..但是文件已经被损坏了怎么办呢?糟糕的是没有测试这个问题的好办法。
看到问题所在了吧?为了要对付可能出错的每件事情,你的错误处理器的代码会变得相当的复杂..糟糕的是,这么复杂的代码肯定会出现不少新的Bug。
在VFP 以前的版本中没有解决这个问题的办法。你只能写下合理的代码、尽可能多的测试它,然后就只能求老天保佑了。幸运的是,在VFP8 中终于有了解决的办法:把你的错误处理器封装在一个TRY 结构中。由于任何发生在TRY 块中的错误都会被CATCH 捕捉到,
所以现在我们的错误处理代码终于有了一个安全网了。
这里是一个简单的例子(下载文件中的WrappedErrorHandler.prg):
On Error do ErrorHandler with error(), program(), lineno()
Use MyBadTableName && 这个表并不存在
On Error
Procedure ErrorHandler(tnError, tcMethod, tnLine)
Try
Use ErrorLog
Insert into ErrorLog Values (tnError, tcMethod, tnLine)
Use
** 在这个错误处理器中会忽略任何问题
CATCH
ENDTRY
Return
EndProc
如果ErrorLog 表由于文件不存在、表无法打开、文件内容损坏或者任何其他原因导致不能使用,CATCH 块都将会被运行。在这个例子里,由于CATCH 块中没有打开,所以在错误中的所有错误都被忽略了。
错误处理策略
现在我们来总结一下,并探讨一个整体的错误处理策略。这里是我目前使用的办法:
l 使用三个错误处理层:TRY 结构对付本地错误;Error 方法用作可以被封装的对象层次的错误处理器;而ON Error 命令则负责全局的错误处理。在前面的两层中处理那些预料到的错误,而把未预料到的错误传递给下一层。
2 把错误处理器封装在一个TRY 结构中,以防止当错误处理代码本身出现错误时会弹出的VFP 错误处理对话框的出现。
3 为你的错误处理机制使用责任链设计模型。这个问题请参见Foxtalk 1998 年第1 期上我的专栏文章《再论错误处理》( Error Handling Resitied )、或者在www.stonefield.com 上关于错误处理的白皮书。
4 不要使用一个TRY 结构来封装你的整个应用程序。如果这样做的话,一旦任何错误发生,就没办法留在程序的内部观察错误的信息了。
5 除非有一个TRY 结构,否则不要使用Throw 语句。
总结
在VFP8 中新增的结构化错误处理功能为我们提供了三个错误处理器的层次:本地的、对象的、还有全局的。结构化错误处理让你可以减少传递错误信息的代码数量,使你的应用程序变得简单、可读性更好、并且更容易维护。此外,某些棘手的错误..例如一个对象漫不经心的去捕捉发生在另一个对象中的错误这种情况..现在可以更明快的被处理了。我建议你花些时间好好的玩转它,并考虑将它引入为你的整个错误处理策略的一个关键部分。