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 |
(引发自定义异常,以便不传递对数据提供程序的依赖项)然后简单地
1 | //error code |
(根本不做任何事情,让错误自己冒出来)
当然,在catch块中也会发生一些日志记录。
我更喜欢3号,虽然我的同事使用方法1,但我们两个都不能真正激发为什么。
使用每种方法的优点/缺点是什么?有没有更好的方法我不知道?有公认的最佳方法吗?
如果你什么都不做,你应该简单地把它放在上面,让别人来处理它。
您总是可以处理其中的一部分(如日志记录),然后重新抛出它。只需发送
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 |
仅在预期和可以处理的异常周围使用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 } |
在本例中,如果最内部的异常是
这可以称为"乐观"异常处理:假设除了一组已知的黑名单类型之外,所有异常都是可以接受的。另一种方法(由C 5和更早版本支持)是"悲观"方法,其中只有已知的白名单异常被认为是可容忍的,其他的都是未处理的。
几年前,悲观的态度是官方建议的立场。但是最近clr本身在
您还可以登记AppDomain.UnhandledException事件,尽可能多地保存用于支持目的的信息(至少是堆栈跟踪),然后调用environment.failfast在执行任何
我不确定是否真的有一个公认的最佳实践,但在我看来
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";进行日志记录并重新执行,这样就不会修改堆栈跟踪。自定义异常通常是为了报告应用程序特定的错误而创建的。