如何区分致命异常和非致命异常
本文关键字:异常 何区 | 更新日期: 2023-09-27 17:50:58
我正在用c#编写一个通用离散事件系统仿真库。我将在此基础上编写另一个库它将实现一种特定的离散事件模拟。以下是代码的完整版本:
static class Engine
{
[ThreadStatic] internal static uint time;
//...
public static void Run(OnException onException = OnException.PauseAndRethrow,
IList<Type> exceptionsToPass = null)
{
//...
while (!stop)
{
Event e = currentTimeEventQueue.PopEvent();
if (e == null) break;
try {
e.Activate();
}
catch (EngineException ex)
{
// An engine method that shouldn't have been called
// was called, and Activate didn't handle that
// handle this situation...
}
catch (/* fatal exception type*/ ex)
{
throw;
}
catch (Exception ex)
{
// code to decides whether to dismiss exception
// pause, stop, reset based on parameters to this method
}
}
}
}
问题是:我是否应该专门捕获已知不可恢复的异常类型(我不应该尝试以任何方式处理)。那些例外是什么(我可以想到OutOfMemoryException
和StackOverflowException
)。是否存在致命例外的列表?我记得有些是抓不到的。所以我对可以捕获的致命异常列表感兴趣。我只想把它们重新扔了,什么都不做。另一方面,我想处理任何其他类型的异常。或者我需要另一个角度。
编辑:好吧,我在写问题的时候犯了一个很大的疏忽。Activate()
是抽象的。我正在写一个通用的离散事件系统仿真库。引擎正在与完全未知的Event
子类一起工作。所以它调用了一个绝对未知的方法Activate()
,这可能会抛出任何类型的异常。我可以忽略这个问题,但是我想把过程的控制权交给调用者。从Run()
方法的参数中可以看到,如果调用Activate()
出现异常,调用者决定引擎将做什么(它可以指示引擎忽略并继续,或者暂停并重新抛出,或者……)。这就是为什么我试图将fatal与所有其他例外分开。如果调用者指示引擎忽略来自Activate()
的任何异常,那么捕获并忽略致命异常是不明智的。(有龙:))
我是否应该专门捕获已知不可恢复的异常类型
。你不应该。如果它们是不可恢复的,你不应该尝试从中恢复。
关于异常的规则是——捕获并处理你知道如何恢复的异常。让其他气泡出现——如果这意味着应用程序崩溃,这可能是最好的选择。
下面是代码气味,不应该被编码:
catch (/* fatal exception type*/ ex)
{
throw;
}
没有throwable是真的"uncatchable";任何可以在。net中抛出的东西都来源于Exception,因此可以被捕获。然而,很多东西不应该被捕获。因此你的问题;如何分辨呢?
我遵循的一般规则是"抓住你期望并知道如何处理的异常"。这需要您首先知道您的代码可能会抛出什么。MSDN文档通常很好地说明了各种框架方法在什么情况下会抛出什么;你自己的代码库可能没有那么好的文档,除非你正在开发一个供其他编码人员使用的中间库(或者你的管理/领导对适当的文档很挑剔)。
一旦你知道了代码可以抛出什么,确定什么,如果有的话,你应该抓住。异常捕获,又名"Pokemon handling"(必须抓住所有Pokemon)通常是一件坏事,因为有合理的理由让你的应用程序死亡并让用户重新启动它。例子包括stackoverflowexception, outofmemoryexception和各种win32exception,详细说明了一些内部失败,无法向程序提供所请求的资源。您通常无法以任何有意义的方式从中恢复。然而,大多数错误并没有那么严重。当找不到文件时,或者当网络连接被拒绝或意外关闭时,或者当您试图对类型执行某些操作而不检查空值时(当空值检查失败时会发生什么?)通常您无法继续,必须抛出自己的异常)。这些都是你应该预料到、捕捉到的东西,并且至少要以某种可理解的方式与最终用户沟通。在许多情况下,try/throw/catch对于预期的但不是日常的情况是一个有用的工具;如果你在等待结果时收到超时异常,而通常获得结果是没有问题的,那么甚至不要告诉用户有问题;再试一次。如果你可以(应该?)在计算无法求值时插入一些默认值(除以零,可能产生非真实结果的数学,但代码无法处理它们,等等),那么就这样做。
但是,有一种情况我可以想到,您必须捕获并重新抛出,这是在涉及数据库事务的情况下。如果您正在使用数据库事务执行大型"工作单元",并在此过程中处理多个持久化操作,那么如果出现任何错误,则应该回滚DB事务。在这种情况下,操作应该包含在try-catch(Exception)块中,该块捕获异常、回滚事务并重新抛出。任何可以正常处理的异常都应该使用嵌套的try-catch来处理,或者应该在操作失败之前检查条件。你的问题是一个合理的问题,但是唉,通常没有真正好的答案。c++中异常范型的一个主要缺点是,它在异常对象的类型中封装了太多的内容,不幸的是,其他语言和框架也从c++中借鉴了异常范型。在c++中,这是可以理解的,因为Bjarne Stroustrup希望避免将任何非基本类型"硬编码"到语言中;考虑到这个限制,c++的设计可能是最好的。然而,这样的设计有一些严重的限制。
最大的问题是考虑捕获和处理异常,然后吞下或重新抛出它的代码可能对许多事情感兴趣:
- 发生了什么阻止代码按照预期的方式运行
- 应该采取什么措施来应对这种情况
- 一旦采取了各种措施,情况是否应该被视为"解决"?
- 应该调整对系统状态的任何方面的假设,而不是方法失败所暗示的(例如,如果文件"foo/bar"无法读取,失败的性质可能会影响是否尝试读取"foo/boz"的决定)。
在Java和。net中,期望异常类型应该用于指示上面的第1条,并且没有标准的方法来回答第2条和第3条,,尽管它们在许多情况下比"发生了什么"重要得多。如果一个人尝试加载一个文档并且出现了一些问题,99%的情况下系统将处于与加载尝试之前相同的状态,所以异常,无论其类型如何,基本上都意味着"文件不可读并且无法加载"。此外,在剩下的1%的时间里,除了加载文件失败之外,还发生了一些"糟糕"的事情,异常类型完全有可能与99%的异常类型相同。
如果没有一些新的异常处理特性,最好的办法可能是,在可能的范围内,使用异常来指示状态损坏,使系统状态的损坏部分无效,以便将来对这些部分的所有操作都同样抛出异常。这样的行为将意味着代码无法处理的异常将最终导致系统崩溃(因为代码将继续尝试使用已经损坏的部分状态),同时允许代码从应该恢复的事情中恢复(例如,如果异常发生是因为正在构建的对象损坏,并且如果异常导致代码放弃正在构建的对象,那么这种放弃就足以"处理"异常。