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

时间:2010-02-05 22:17:37

标签: c# exception

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

我正在开发一个分为Presentation(web),Business和Logic层的项目,并且需要在链中传递错误(例如SqlExceptions)以在出现问题时通知Web层。

我见过3种基本方法:

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

(简单地重新抛出)

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

(抛出自定义异常,以便不传递对数据提供者的依赖性)
然后简单地

//error code

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

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

我更喜欢3号,而我的同事使用方法1,但我们都不能真正激发原因。

使用每种方法有哪些优点/缺点?有一种我不知道的更好的方法吗?有没有被接受的最佳方式?

8 个答案:

答案 0 :(得分:15)

如果你什么都不做,你应该把它放在一个人可以处理的地方。

你总是可以处理它的一部分(比如记录)并重新抛出它。只需发送throw;而不必明确ex名称,即可重新投掷。

try
{

}
catch (Exception e)
{
    throw;
}

处理它的好处是你可以确保有一些机制可以通知你有一个你不怀疑有错误的错误。

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

答案 1 :(得分:9)

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

  

我如何期望其他组件与我的模块抛出的异常进行交互?

如果消费者完全有能力处理较低/数据层引发的异常,则完全不做任何事情。上层能够处理异常,你应该只做最小的维持你的状态然后重新抛出的数量。

如果消费者无法处理低级异常,而是需要更高级别的异常,那么创建一个他们可以处理的新异常类。但请确保将原始异常传递给内部异常。

throw new MyCustomException(msg, ex);

答案 2 :(得分:6)

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

try
{
    ....
}
catch (Exception e)
{
    throw;
}

有关详细信息,请参阅此thread

答案 3 :(得分:6)

仅在您期望和可以处理的异常周围使用try / catch块。如果你抓住了一些你无法处理的东西,它就会破坏try / catch的目的,即处理预期的错误。

捕获大异常很少是一个好主意。第一次捕获OutOfMemoryException是否真的能够优雅地处理它?大多数API记录了每个方法可以抛出的异常,并且这些异常应该是唯一可以处理的异常,只有当您可以优雅地处理它时。

如果你想在链条上进一步处理错误,那就让它自己冒出来而不用捕捉并重新抛出它。唯一的例外是日志记录,但每一步的日志记录都会做大量的工作。最好只记录您的公共方法可能出现的异常情况,并让API的消费者决定如何处理它。

答案 4 :(得分:4)

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

当后端层抛出异常时,发生了一些可怕而意外的事情。 意外的可怕情况可能已经发生,因为该级别受到敌对用户的攻击。在这种情况下,您想要做的最后事情是向攻击者提供所有出错的详细列表以及原因。当业务逻辑层出现问题时,正确的做法是仔细记录有关异常的所有信息,并用通用替换异常“我们很抱歉,出现问题,管理已收到警报,请再试一次”页面。

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

首先获取安全设计,然后再担心诊断和调试。

答案 5 :(得分:3)

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

有一次,我觉得(以Java思维方式)异常是方法的二进制接口的一部分,与返回类型和参数类型一样多。但在C#中,它根本就不是。事实上,没有投掷规范系统这一点很清楚。

换句话说,如果您希望采取只有您的例外类型应该从图书馆的方法中消失的态度,那么您的客户就不会依赖图书馆的内部细节。但很少有图书馆愿意这样做。

官方C#团队的建议是捕获方法可能引发的每种特定类型,如果您认为可以处理它们。不要抓到任何你无法处理的东西。这意味着不会在库边界处封装内部异常。

但反过来,这意味着您需要完整记录给定方法可能抛出的内容。现代应用程序依赖于第三方库的庞大,迅速发展。如果他们都试图捕获在未来的库版本组合中可能不正确的特定异常类型而没有编译时检查,那么它就是一个静态类型系统的嘲弄。

所以人们这样做:

try
{
}
catch (Exception x) 
{
    // log the message, the stack trace, whatever
}

问题是这会捕获所有异常类型,包括那些从根本上表明存在严重问题的异常类型,例如空引用异常。这意味着程序处于未知状态。检测到的那一刻,它应该在对用户的持久性数据造成一些损害之前关闭(开始丢弃文件,数据库记录等)。

这里隐藏的问题是try / finally。这是一个很棒的语言功能 - 实际上它是必不可少的 - 但是如果一个足够严重的异常会在堆栈中飞起来,它真的会导致最终的块运行吗?你是否真的希望在有进展中的错误时销毁证据?如果程序处于未知状态,任何重要的东西都可以被那些最终的块破坏。

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

try
{
    // attempt some operation
}
catch (Exception x) when (x.IsTolerable())
{
    // log and skip this operation, keep running
}

在此示例中,您将在IsTolerable上将Exception写为扩展方法,如果最内层的异常为falseNullReferenceException,{IndexOutOfRangeException,则返回InvalidCastException {1}}或已决定的任何其他异常类型必须指出必须停止执行并需要调查的低级错误。这些是“无法忍受的”情况。

这可能被称为“乐观”异常处理:假设除了一组已知的黑名单类型外,所有异常都是可以容忍的。替代方案(由C#5及更早版本支持)是“悲观”方法,其中只有已知的白名单异常被认为是可以容忍的,而其他任何异常都是未处理的。

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

您还可以使用AppDomain.UnhandledException事件进行登记,尽可能多地保存信息以用于支持目的(至少是堆栈跟踪),然后调用Environment.FailFast以在任何{{3}}之前关闭您的进程1}}块可以执行(这可能会破坏调查错误所需的有价值的信息,或者抛出隐藏原始错误的其他异常)。

答案 6 :(得分:2)

我不确定是否确实存在可接受的最佳做法,但IMO

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.

答案 7 :(得分:0)

通常情况下,您只会捕获您期望的异常,可以处理并让应用程序以正常方式进一步工作。如果您希望有一些额外的错误记录,您将捕获异常,请使用“throw”进行记录并重新抛出它。所以堆栈跟踪不会被修改。通常会创建自定义异常,以报告特定于应用程序的错误。