传递异常的正确方式是什么?(C#)

25

我想知道在一个方法与另一个方法之间传递异常的正确方式是什么。

我正在开发一个分为展示层(web)、业务层和逻辑层的项目,当出现错误(例如 SqlExceptions)时,需要将异常从上层传递到下层以通知 web 层。

我见过三种基本方法:

try  
{  
    //error code
} 
catch (Exception ex)
{
    throw ex;
}

(简单地重新抛出异常)

try  
{  
    //error code
} 
catch (Exception ex)
{
    throw new MyCustomException();
}

抛出自定义异常,以避免传递到数据提供程序的依赖关系

然后简单地执行

//error code

(不做任何事情,让错误自行冒泡)

当然,在catch块中也会有一些日志记录。

我更喜欢第三种方法,而我的同事使用第一种方法,但我们两个都无法真正解释为什么。

每种方法使用的优缺点是什么?是否有更好的方法我不知道?是否有公认的最佳方式?


19
你的第一个示例是 C# 中的反模式,请使用 throw;,否则会将堆栈跟踪重置为当前抛出点。仅使用 throw; 将保留原始的堆栈跟踪信息。 - Nick Larsen
1
双重复制:https://dev59.com/M3VC5IYBdhLWcg3wz0l9 和 https://dev59.com/0nVD5IYBdhLWcg3wTZxm。 - Robert P
1
请注意,即使您使用 catch (Exception ex) { throw new MyCustomException(ex); } 更改异常类型,也可以保留堆栈跟踪,如果您在 MyCustomException 类中使用 Exception 类的提供的构造函数(所有 .NET 异常类型都是如此)。 - user253984
8个回答

24

如果你无法处理异常,那就让它向上层抛出,交由其他人来处理。

你也可以处理其中的一部分(比如记录日志),然后重新抛出异常。可以通过发送 throw; 来重新抛出异常,不需要明确指定异常名称。

try
{

}
catch (Exception e)
{
    throw;
}
处理异常的优点在于,您可以确保存在某种机制来通知您在不希望出现错误的地方发生了错误。但是,在某些情况下,比如第三方情况,您希望让用户处理该错误,此时应继续将其传递到更高层。

1
请记住,在上面的示例中,您将捕获像NullReferenceException这样的错误,因此,任何在throw点和try/catch之间的finally块都将被执行 - 即使明显检测到了致命错误。在程序处于这种未知状态下,那些finally块会做什么呢?谁知道呢? - Daniel Earwicker
2
@Earwicker,你的观点很有道理,但finally块无论如何都会运行。这些finally块已经必须编写为在面对错误时具有鲁棒性。我更关注的是安全性问题,而不是健壮性问题。健壮性已经不存在了;服务器崩溃了。让我们不要帮助那些导致服务器崩溃的人。 - Eric Lippert
@Eric - 就我个人而言,我不确定自己是否知道如何编写一个最终块,以便它能够针对任何错误进行强大的处理! :) 因此需要异常过滤和FailFast。但是我理解你所说的有时需要隐藏信息的必要性。您不希望在生产站点上向客户返回详细信息。但是您可能仍然希望在服务器上私下记录尽可能多的信息。 - Daniel Earwicker

10

我认为您应该从一个略有不同的问题开始

如果我的模块抛出异常,我应该如何期望其他组件与之交互?

如果使用者完全有能力处理由低层数据层抛出的异常,则只需什么都不做。上层可以处理异常,您只需要进行必要的最小限度维护状态,然后重新抛出异常。

如果消费者无法处理低级别的异常而需要更高级别的异常,请创建一个新的异常类供其处理。但请确保将原始异常作为内部异常传递。

throw new MyCustomException(msg, ex);

8
在C#中重新抛出异常的正确方法如下所示:
try
{
    ....
}
catch (Exception e)
{
    throw;
}

See this thread for specifics.


否则,你将会失去你的调用堆栈,这会让调试非常痛苦。 - Adam Neal

6
只在你期望且可以处理的异常周围使用try/catch块。如果你捕获了你无法处理的东西,那么它就失去了try/catch的目的,即处理预期的错误。
很少有人捕获大的异常是个好主意。第一次捕获OutOfMemoryException时,您真的能够优雅地处理它吗?大多数API都记录了每个方法可能引发的异常,并且应该仅处理这些异常,而且只有在您可以优雅地处理它的情况下才能处理。
如果您想在链中更高的位置处理错误,请让它自己泡沫而不是捕获并重新抛出它。唯一的例外是为了日志记录,但在每个步骤记录日志会做过多的工作。最好只记录公共方法允许泡沫的异常的位置,并让API的使用者决定如何处理它。

4
没有人指出你应该首先考虑的非常重要的事情:有哪些威胁
当后端层抛出异常时,发生了可怕且意外的事情。 这种意外可怕的情况可能是由于该层受到敌对用户的攻击而发生的。在这种情况下,您最不想做的事情是向攻击者提供详细的错误列表以及原因。当业务逻辑层出现问题时,正确的做法是仔细记录关于异常的所有信息,并将异常替换为通用的“很抱歉,出了点问题,管理员已被警告,请重试”页面。
其中一个要跟踪的事情是您拥有的有关用户及其在异常发生时所做的操作的所有信息。这样,如果您检测到同一用户似乎总是引起问题,您可以评估他们是否有可能正在寻找您的弱点,或者只是在使用未经充分测试的应用程序角落。
先正确设计安全性,然后再考虑诊断和调试。

3
我看到了各种不同的强烈观点。答案是我认为目前在C#中没有理想的方法。
曾经我认为(以Java的方式)异常是方法的二进制接口的一部分,就像返回类型和参数类型一样。但在C#中,它根本不是这样。这一点很明显,因为没有throws规范系统。
换句话说,如果你愿意,你可以采取这样的态度:只有你的异常类型应该从你的库方法中抛出,这样你的客户端就不会依赖于你的库的内部细节。但是很少有库这样做。
官方的C#团队建议是,如果你认为自己能够处理,就捕获每个可能被方法抛出的特定类型。不要捕获任何你真的无法处理的东西。这意味着在库边界处没有封装内部异常。
但反过来,这意味着你需要完美的文档说明给定方法可能会抛出什么异常。现代应用程序依赖于大量的第三方库,这些库正在快速发展。如果它们都试图捕获可能在未来版本的库组合中不正确的特定异常类型,而没有编译时检查,那将对静态类型系统产生嘲笑。因此,人们这样做:
try
{
}
catch (Exception x) 
{
    // log the message, the stack trace, whatever
}

问题在于它捕获了所有异常类型,包括那些根本表明严重问题的异常,比如空引用异常。这意味着程序处于未知状态。一旦检测到这种情况,它应该在对用户的持久数据造成损害之前关闭(开始损坏文件、数据库记录等)。
这里隐藏的问题是try/finally。它是一个非常好的语言特性 - 实际上是必不可少的 - 但是如果一个足够严重的异常正在向上传播,它真的应该导致finally块运行吗?在当前有错误发生时,你真的希望证据被销毁吗?如果程序处于未知状态,那么那些finally块可能会摧毁任何重要的东西。
所以,你真正需要的是(C# 6的更新!):
try
{
    // attempt some operation
}
catch (Exception x) when (x.IsTolerable())
{
    // log and skip this operation, keep running
}

在这个例子中,您需要将IsTolerable作为Exception的扩展方法编写,如果最内层的异常是NullReferenceExceptionIndexOutOfRangeExceptionInvalidCastException或任何其他您已经决定必须表示必须停止执行并需要调查的低级错误的异常类型,则返回false。这些是“不可容忍”的情况。
这可能被称为“乐观”的异常处理:假设所有异常都是可以容忍的,除了一组已知的黑名单类型。另一种方法(由C# 5及更早版本支持)是“悲观”的方法,其中只有已知的白名单异常被认为是可以容忍的,而其他任何异常都未处理。
多年前,悲观的方法是官方推荐的立场。但是现在CLR本身在Task.Run中捕获所有异常,因此它可以在线程之间移动错误。这会导致finally块执行。因此,默认情况下,CRL非常乐观。
您还可以注册AppDomain.UnhandledException事件,为支持目的保存尽可能多的信息(至少是堆栈跟踪),然后调用Environment.FailFast在任何finally块可以执行之前关闭进程(这可能会破坏需要调查错误的有价值信息,或者抛出隐藏原始异常的其他异常)。

1
@Luaan已更新答案! - Daniel Earwicker

2
我不确定是否存在公认的最佳实践,但在我看来。
try  // form 1: useful only for the logging, and only in debug builds.
{  
    //error code
} 
catch (Exception ex)
{
    throw;// ex;
}

除了日志记录方面,它没有实际意义,因此我只会在调试构建中执行此操作。捕获并重新抛出代价昂贵,因此您应该有一个支付这个代价的理由,而不仅仅是因为您喜欢查看代码。

try  // form 2: not useful at all
{  
    //error code
} 
catch (Exception ex)
{
    throw new MyCustomException();
}

这个句子完全没有意义。它正在忽略真正的异常并用一个包含有关实际问题信息较少的异常来替换它。如果我想增加有关发生了什么事情的异常信息,可能会这样做。
try  // form 3: about as useful as form 1
{  
    //error code
} 
catch (Exception ex)
{
    throw new MyCustomException(ex, MyContextInformation);
}

但我认为在几乎所有不需要处理异常的情况下,最好的方式是让更高级别的处理程序来处理它。

// form 4: the best form unless you need to log the exceptions.
// error code. no try - let it percolate up to a handler that does something productive.

0
通常情况下,您只会捕获您预期、可以处理并让应用程序继续正常工作的异常。如果您想要进行一些额外的错误日志记录,您可以捕获异常、进行日志记录并使用 "throw;" 重新抛出它,以便不修改堆栈跟踪。通常会创建自定义异常来报告应用程序特定的错误。

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