这些try/catch块是否等价?

17

场景

我有一个执行数据库操作的方法(假设如此)。如果在该操作期间引发任何异常,我只想将该异常抛给调用方。我不想在 catch 块中执行任何特定任务,假设调用方将对该异常执行任何想要执行的任务。在这种情况下,哪种异常处理技术是适合的?

try
{
    // Some work that may generate exception
}
catch(Exception)
{
    throw;
}
finally
{
    // Some final work
}

以上代码与以下try/catch/finally代码等效吗?

try
{
    // Some work that may generate exception
}
catch
{
    throw;
}
finally
{
    // Some final work
}

以上代码等同于以下的try/finally语句吗?

try
{
    // Some work that may generate exception
}
finally
{
    // Some final work
}

哪个比另一个好?应该使用哪一个?


10
你问题帖子底部的问题有些难以回答,因为在这三个选项里,我只能推荐最下面的一个。一般情况下不建议捕获所有异常然后简单地重新抛出它们。我假设你打算在catch块内处理它们,但在这种情况下,取决于你打算做什么。如果你打算记录异常并简单地重新抛出,那么是的,捕获任何类型的异常可能会起作用,但在几乎所有其他情况下,你应该只捕获你知道如何处理的异常。 - Lasse V. Karlsen
此外,前两个与最后一个有所不同,因为您没有一个 catch 块。但是,如果该catch块只会重新抛出异常(如您的示例所示),并且没有额外/其他处理,则根本没有必要使用该catch块。 - Lasse V. Karlsen
基本上,推荐做什么取决于你在问题中没有提供的上下文。 - Lasse V. Karlsen
@HimBromBeere 如果你问“哪一个比其他的更好”,如果有一种客观的方式来衡量每个选择的“好处”,那么它不必是基于意见的。我的观点是,除了提出更具体的场景之外,这个问题没有足够的上下文来推荐任何东西,而在这些场景中可能存在着良好和可靠的建议。 - Lasse V. Karlsen
1
有了新的场景,如果您不打算对任何异常做任何事情,那么您什么都不需要做。异常将在没有您帮助的情况下向上传播到调用堆栈(并且在某些情况下跨越线程和任务边界,具体取决于所涉及的框架)。只有当您打算处理异常时,才需要使用try/catch。如果您想确保一些清理代码始终运行,则可能需要使用try/finally。换句话说,对于您的特定场景,如果需要,可以使用try/finally替代方案,或者根本不使用try/catch/finally。 - Lasse V. Karlsen
显示剩余2条评论
3个回答

31

不,它们不等同。在某些情况下它们可能等价,但一般的答案是否定的。

catch块的不同种类

指定异常类型的catch

以下代码只会捕获从System.Exception继承的托管异常,然后执行finally块,无论是否抛出异常。

try
{
   // Some work that may generate exception
}
catch (Exception)
{
   throw;
}
finally
{
   // Some final work
}

catch 块没有指定异常类型

以下不带类型说明符的 catch 块还将捕获非托管异常,这些异常不一定由托管的 System.Exception 对象表示,然后执行 finally 块,无论是否抛出异常都会执行该块。

try
{
   // Some work that may generate exception
}
catch
{
   throw;
}
finally
{
   // Some final work
}

finally块没有catch

如果您没有catch块,无论是否发生异常,finally仍将被执行。

try
{
   // Some work that may generate exception
}
finally
{
  // Some final work
}

它们何时是等效的?

如果你的catch块没有指定异常并且只包含throw;语句,则后两者确实是等效的。如果你不关心非托管异常并且你的catch块只包含throw;语句,则三者都可以被视为等效。

注释

throw的一个注意点

以下两段代码之间有微妙的差异。后者将重新抛出异常,这意味着它将重写异常的堆栈跟踪,因此它们绝对不是等效的:

catch (Exception e)
{
    throw;
}

然而

catch (Exception e)
{
    throw e;
}

如果您在IDisposable中使用finally,下面两个代码几乎是等价的,但存在微妙的差异:

  • 当对象为空时,using语句将不会抛出NullReferenceException
  • 使用try-finally技术时,变量仍然在作用域内,尽管强烈不建议在对象释放后使用任何对象。但是您仍然可以将变量重新分配给其他内容。

    Something obj = null; try { obj = new Something() // 做一些事情 } finally { obj.Dispose(); }

以及

using (var obj = new Something())
{
    // Do something
}

2
@Bhuban 没有理由让你这样做。你要么想重新抛出异常,要么想抛出另一个异常(可能带有原始异常作为“InnerException”)。当然,所有需要完成的工作都会对性能产生影响 - 只有您可以判断性能影响在您的应用程序中是否可察觉,并且只能通过测量来进行判断。一般来说,异常是异常情况,因此在异常情况下成本较高的代码通常是可以接受的 - 如果您发现抛出异常使程序太慢,则应该修复潜在问题。 - Luaan
1
请注意,1和2在大多数情况下是等效的,因为运行时将未处理的异常包装到托管异常(例如SEHException)中。从技术上讲,catch { ... }编译成类似于catch (object) { ... }的东西 - CLI支持其他对象类型的传播,但CLS规定异常的基类必须是System.Exception - Lucas Trzesniewski
关于using的解释:一个区别是,在finally之后,obj仍然在作用域内,这可能是相关的。 - ths
1
关于您的using语句,编译器会在finally块中处理objnull的情况。 - Matthew
也许在“关于 throw 的说明”中添加一条建议使用 throw 而不是 throw e?重新抛出异常有合理的理由,但初学者应默认使用 throw - Brian
显示剩余9条评论

11

到目前为止,您已经得到了一些不错的回答,但是他们还没有提到一个有趣的区别。请考虑:

try { ImpersonateAdmin(); DoWork(); } 
finally { RevertImpersonation(); }

对手

try { ImpersonateAdmin(); DoWork(); }
catch { RevertImpersonation(); throw; }
finally { RevertImpersonation(); }

假设DoWork抛出异常。

现在首先需要回答的问题是“是否有一个catch块可以处理这个异常”,因为如果答案是“没有”,那么程序的行为就是实现定义的。运行时可能选择立即终止进程,它可能会在终止进程之前运行finally块,它可能会选择在未处理的异常点启动调试器,或者它可能会选择执行任何它喜欢的操作。允许有未处理的异常的程序可以执行任何操作。

因此,运行时开始寻找catch块。在这个try语句中没有找到,所以它会查找调用堆栈。假设它找到了一个带有异常过滤器的catch块。它需要知道过滤器是否允许处理异常,因此过滤器是在权限还原之前运行的。如果过滤器意外或故意执行某些只有管理员才能执行的操作,它将成功!这可能不是你想要的。

在第二个例子中,catch立即捕获异常,还原身份,然后重新抛出异常。现在,运行时开始寻找一个catch块来处理重新抛出的异常。如果有一个过滤器存在,则在还原身份之后运行。(当然finally还会再次还原;我假设在这里还原身份是幂等的。)

这是这两个代码片段之间的一个重要区别。如果绝对禁止任何代码看到由try引起的全局状态,并被finally清理掉的状态,那么你必须在finally之前捕获异常。"Finally"不意味着"立即",它意味着"最终".


4

try/catch语句中的两种写法都是等效的,因为它们都重新抛出了原始被捕获的异常。但是空的catch更加宽泛(正如Venemo已经指出的,可以捕获未受管控的异常)。如果您要捕获异常并将其保存在一个变量中,您可以将其用于日志记录,或者在传递原始异常作为参数的情况下throw一个新的异常,使其成为“内部异常”

finally无论如何都会执行相同的功能。

在不需要日志记录异常和明确假定调用方将处理引发的异常(例如写入文件流或发送电子邮件)的情况下,应该使用哪种方法?

如果调用方将处理异常,并且您不需要在此级别上记录异常的发生,则根本不应该进行捕获。如果调用方将处理抛出的异常,则没有必要捕获异常,只需重新抛出它。

捕获并重新抛出异常的有效原因:

  • throw new Exception("WTF Happened", ex); // 用作内部异常
  • 记录异常
  • 使用finally块来执行一些清理代码

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接