9.6 异常的高级功能
除非你对这门语言已经有了很好的了解,否则第一次阅读这部分内容时最好跳过。在此之前,你可以先阅读下一章,将来再回到这一部分。
在本章的最后一部分,我将介绍一些与异常处理相关的高级主题。我将介绍嵌套异常(RaiseOuterException)和拦截类的异常(RaisingException)。
这些功能在早期版本的 Object Pascal 语言中并不存在,但它们为系统增添了强大的功能。
9.6.1 嵌套异常和 InnerException 机制
如果在异常处理器中引发异常,会发生什么情况?传统的答案是,新的异常将取代现有的异常,这就是为什么通常的做法是至少将错误信息合并在一起,写出这样的代码(缺少任何实际操作,只显示与异常相关的语句):
procedure TFormExceptions.ClassicReraise;
begin
try
// Do something...
raise Exception.Create('Hello');
except
on E: Exception do
// Try some fix...
raise Exception.Create('Another: ' + E.Message);
end;
end;
这段代码是 AdvancedExcept 示例的一部分。在调用该方法并处理异常时,你会看到一个包含两个异常信息的单一异常:
procedure TFormExceptions.BtnTraditionalClick(Sender: TObject);
begin
try
ClassicReraise;
except
on E: Exception do
Show('Message: ' + E.Message);
end;
end;
输出结果(非常明显)是:
Message: Another: Hello
现在在Object Pascal中,有了对嵌套异常的全系统支持。 在异常处理程序中,您可以创建并引发一个新的异常,同时仍然保持当前异常对象的活动状态,并将其连接到新的异常。 要做到这一点,Exception类有一个InnerException属性(引用前面的异常)和一个BaseException属性(允许访问系列中的第一个异常),因为异常嵌套可以递归。 这些是Exception类中与嵌套异常管理相关的要素:
type
Exception = class(TObject)
private
FInnerException: Exception;
FAcquireInnerException: Boolean;
protected
procedure SetInnerException;
public
function GetBaseException: Exception; virtual;
property BaseException: Exception read GetBaseException;
property InnerException: Exception read FInnerException;
class procedure RaiseOuterException(E: Exception); static;
class procedure ThrowOuterException(E: Exception); static;
end;
静态类方法是类方法的一种特殊形式。我们将在第 12 章中解释这一语言特性。
从用户的角度来看,要在保留现有异常的同时引发异常,应调用 RaiseOuterException 类方法(或相同的 ThrowOuterException 方法,后者使用面向 C++ 的命名)。处理类似异常时,可以使用新属性访问更多信息。请注意,您只能在异常处理器中调用 RaiseOuterException,这一点基于源代码文档:
Use this function to raise an exception instance from within an exception handler and you want to “acquire” the active exception and chain it to the new exception and preserve the context. This will cause the FInnerException field to get set with the exception currently in play.
使用此函数从异常处理程序中引发异常实例,并且您希望“获取”活动异常并将其链接到新异常并保留上下文。这将导致 FInnerException 字段设置为当前正在播放的异常。
You should only call this procedure from within an except block where this new exception is expected to be handled elsewhere.
您只应从 except 块中调用此过程,在该块中,此新异常应在其他地方处理。
有关实际示例,请参阅 AdvancedExcept 示例。在这个示例中,我添加了一个以新方式引发嵌套异常的方法(与前面列出的 ClassicReraise 方法相比):
procedure TFormExceptions.MethodWithNestedException;
begin
try
raise Exception.Create('Hello');
except
Exception.RaiseOuterException(Exception.Create('Another'));
end;
end;
现在,在这个外部异常的处理器中,我们可以访问两个异常对象(也可以看到调用新的 ToString 方法的效果):
try
MethodWithNestedException;
except
on E: Exception do
begin
Show('Message: ' + E.Message);
Show('ToString: ' + E.ToString);
if Assigned(E.BaseException) then
Show('BaseException Message: ' + E.BaseException.Message);
if Assigned(E.InnerException) then
Show('InnerException Message: ' + E.InnerException.Message);
end;
end;
该调用的输出结果如下:
Message: Another
ToString: Another
Hello
BaseException Message: Hello
InnerException Message: Hello
有两个相关要素需要注意。首先,在单个嵌套异常的情况下,BaseException 属性和 InnerException 属性都指向同一个异常对象,即原始异常对象。其次,虽然新异常的消息只包含实际消息,但通过调用 ToString,可以访问所有嵌套异常的合并消息,并用 sLineBreak 分隔(如 Exception.ToString 方法的代码所示)。
在这种情况下选择使用换行符会产生奇怪的输出结果,但一旦你知道了它,你就可以按照自己喜欢的方式来格式化它,用你选择的符号替换换行符,或者将它们赋值给字符串列表的 Text 属性。
再举一个例子,让我向你展示引发两个嵌套异常时会发生什么。这个就是新方法:
procedure TFormExceptions.MethodWithTwoNestedExceptions;
begin
try
raise Exception.Create('Hello');
except
try
Exception.RaiseOuterException(
Exception.Create('Another'));
except
Exception.RaiseOuterException(
Exception.Create('A third'));
end;
end;
end;
这调用的方法与我们之前看到的方法完全相同,输出结果如下:
Message: A third
ToString: A third
Another
Hello
BaseException Message: Hello
InnerException Message: Another
这一次,BaseException 属性和 InnerException 属性指向了不同的对象,ToString 的输出跨越了三行。