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

What is the right way to pass on an exception? (C#)

本问题已经有最佳答案,请猛点这里访问。

我想知道将异常从一个方法传递到另一个方法的正确方法是什么。

我正在处理一个项目,它被划分为表示层(Web)、业务层和逻辑层,并且错误(例如,sqlExceptions)需要通过链传递,以便在出现问题时通知Web层。

我见过三种基本方法:

1
2
3
4
5
6
7
8
try  
{  
    //error code
}
catch (Exception ex)
{
    throw ex;
}

(只是简单地重排)

1
2
3
4
5
6
7
8
try  
{  
    //error code
}
catch (Exception ex)
{
    throw new MyCustomException();
}

(引发自定义异常,以便不传递对数据提供程序的依赖项)然后简单地

1
//error code

(根本不做任何事情,让错误自己冒出来)

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

我更喜欢3号,虽然我的同事使用方法1,但我们两个都不能真正激发为什么。

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


如果你什么都不做,你应该简单地把它放在上面,让别人来处理它。

您总是可以处理其中的一部分(如日志记录),然后重新抛出它。只需发送throw;就可以重新抛出,而无需显式地显示前名。

1
2
3
4
5
6
7
8
try
{

}
catch (Exception e)
{
    throw;
}

处理它的好处是,您可以确保存在某种机制来通知您有一个错误,而您不怀疑其中有一个错误。

但是,在某些情况下,比如说第三方,你想让用户来处理它,在这种情况下,你应该让它继续冒泡。


我认为你应该从一个稍微不同的问题开始

How do I expect other components to interact with exceptions thrown from my module?

如果消费者能够很好地处理由较低层/数据层抛出的异常,那么完全不做任何事情。上层能够处理异常,您应该只做维持状态所需的最小量,然后再执行。

如果使用者不能处理低级异常,而需要更高级的异常,那么创建一个他们可以处理的新异常类。但请确保传递原始异常A和内部异常。

1
throw new MyCustomException(msg, ex);


仅在预期和可以处理的异常周围使用try/catch块。如果捕获到无法处理的内容,则会破坏Try/Catch的目的,即处理预期的错误。

抓住大的例外很少是个好主意。当你第一次发现内存不足时,你真的能够优雅地处理它吗?大多数API都记录了每个方法可以抛出的异常,并且这些异常应该是唯一处理过的异常,并且只有当您能够适当地处理它时。

如果你想在链条上进一步处理这个错误,就让它自己冒泡,而不去捕捉和重发它。唯一的例外是出于日志记录的目的,但是每个步骤的日志记录都会做大量的工作。最好只记录那些可以期望您的公共方法允许冒泡的异常,并让您的API的使用者决定如何处理它。


在C中重新抛出异常的正确方法如下:

1
2
3
4
5
6
7
8
try
{
    ....
}
catch (Exception e)
{
    throw;
}

有关详细信息,请参阅此线程。


还没有人指出你应该考虑的第一件事:什么是威胁?

当后端层抛出异常时,发生了可怕和意外的事情。由于该层受到恶意用户的攻击,可能会发生意外的可怕情况。在这种情况下,您最不想做的就是向攻击者提供一个详细的列表,列出所有出错的地方和原因。当业务逻辑层出错时,正确的做法是仔细记录有关异常的所有信息,并将异常替换为一个通用的"抱歉,出错了,管理已收到警报,请重试"页面。

要跟踪的一件事是您拥有的关于用户的所有信息,以及异常发生时他们正在做什么。这样,如果您检测到同一个用户似乎总是导致问题,那么您可以评估他们是否可能在探测您的弱点,或者只是使用应用程序中一个未经充分测试的异常角落。

首先正确地进行安全设计,然后才担心诊断和调试。


我已经看到(并持有)关于这一点的各种强烈意见。答案是,我认为目前在C中没有理想的方法。

在某种程度上,我觉得(以Java的方式)异常是一个方法的二进制接口的一部分,就像返回类型和参数类型一样。但在C中,它只是不存在。这从没有throw规范系统的事实中可以清楚地看出。

换句话说,如果您希望采取这样的态度,即只有您的异常类型才会从库的方法中飞出,这样您的客户机就不依赖于库的内部细节。但很少有图书馆为此费心。

官方的C团队建议是捕获方法可能抛出的每个特定类型,如果您认为可以处理它们的话。不要抓住任何你不能真正处理的事情。这意味着不在库边界封装内部异常。

但反过来,这意味着您需要一个关于给定方法可能抛出的内容的完美文档。现代应用程序依赖于大量的第三方库,快速发展。如果静态类型系统都试图捕获特定的异常类型,而这些异常类型在将来的库版本组合中可能不正确,并且没有编译时检查,那么这就使得拥有静态类型系统成为一种嘲弄。

所以人们这样做:

1
2
3
4
5
6
7
try
{
}
catch (Exception x)
{
    // log the message, the stack trace, whatever
}

问题是,这捕获了所有异常类型,包括从根本上指示严重问题的异常,例如空引用异常。这意味着程序处于未知状态。一旦被检测到,它就应该在对用户的持久数据(开始破坏文件、数据库记录等)造成某些损坏之前关闭。

这里隐藏的问题是Try/Finally。这是一个很好的语言特性——事实上它是必不可少的——但是如果一个足够严重的异常正在往上飞,那么它是否真的会导致finally块运行呢?你真的希望证据在有漏洞的时候被销毁吗?如果程序处于未知状态,任何重要的东西都可能被最后的块破坏。

所以你真正想要的是(更新为C 6!):

1
2
3
4
5
6
7
8
try
{
    // attempt some operation
}
catch (Exception x) when (x.IsTolerable())
{
    // log and skip this operation, keep running
}

在本例中,如果最内部的异常是NullReferenceExceptionIndexOutOfRangeExceptionInvalidCastException或您决定的任何其他异常类型,则将IsTolerable写为Exception的扩展方法,该方法返回false,这必须指示必须停止执行并需要调查的低级错误。这是"无法容忍"的情况。

这可以称为"乐观"异常处理:假设除了一组已知的黑名单类型之外,所有异常都是可以接受的。另一种方法(由C 5和更早版本支持)是"悲观"方法,其中只有已知的白名单异常被认为是可容忍的,其他的都是未处理的。

几年前,悲观的态度是官方建议的立场。但是最近clr本身在Task.Run中捕获了所有异常,因此它可以在线程之间移动错误。这会导致finally块执行。所以crl在默认情况下是非常乐观的。

您还可以登记AppDomain.UnhandledException事件,尽可能多地保存用于支持目的的信息(至少是堆栈跟踪),然后调用environment.failfast在执行任何finally块之前关闭您的进程(这可能会破坏调查错误所需的宝贵信息,或引发其他异常)。把原来的藏起来)。


我不确定是否真的有一个公认的最佳实践,但在我看来

1
2
3
4
5
6
7
8
try  // form 1: useful only for the logging, and only in debug builds.
{  
    //error code
}
catch (Exception ex)
{
    throw;// ex;
}

除了日志方面之外没有任何实际意义,因此我只在调试构建中执行此操作。捕捉一个重发是昂贵的,所以你应该有一个理由支付这个费用,而不仅仅是你喜欢看代码。

1
2
3
4
5
6
7
8
try  // form 2: not useful at all
{  
    //error code
}
catch (Exception ex)
{
    throw new MyCustomException();
}

这个完全没有意义。它放弃了真正的异常,用一个包含较少实际问题信息的异常代替它。我可以看到,如果我想用一些关于正在发生的事情的信息来增加异常,那么可能会这样做。

1
2
3
4
5
6
7
8
try  // form 3: about as useful as form 1
{  
    //error code
}
catch (Exception ex)
{
    throw new MyCustomException(ex, MyContextInformation);
}

但我想说,在几乎所有没有处理异常的情况下,最好的形式是让更高级别的处理程序处理它。

1
2
// 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.

通常情况下,您只捕获期望的异常,这些异常可以以正常的方式处理并让应用程序进一步工作。如果您希望进行一些额外的错误日志记录,您将捕获一个异常,请使用"throw";进行日志记录并重新执行,这样就不会修改堆栈跟踪。自定义异常通常是为了报告应用程序特定的错误而创建的。