15

首先,免责声明:我有其他语言的经验,但仍在学习 C# 的精妙之处

关于问题...我正在查看一些代码,它以与我有关的方式使用 try/catch 块。当调用解析例程时,程序员使用以下逻辑而不是返回错误代码

catch (TclException e) {
  throw new TclRuntimeError("unexpected TclException: " + e.Message,e);
}  

这被调用者捕获,它抛出相同的错误......
被调用者捕获,它抛出相同的错误......
...... 被调用者捕获,它抛出相同的错误...

备份大约 6 个级别。

我认为所有这些 catch/throw 块都会导致性能问题,或者这是 C# 下的合理实现吗?

4

8 回答 8

16

在任何语言下都是一个糟糕的设计。

异常被设计为在您可以处理它们的级别被捕获。捕获异常,然后再次抛出它只是浪费时间(它还会导致您丢失有关原始错误位置的宝贵信息)。

显然,编写该代码的人曾经使用错误代码,然后在没有真正理解它们如何工作的情况下切换到异常。如果在某一级别没有捕获,异常会自动“冒泡”堆栈。

另外,请注意,异常是针对异常事件。不该发生的事情。它们不应该用于正常的有效性检查(即,不要捕获被零除的异常;预先检查除数是否为零)。

于 2009-03-05T18:30:59.873 回答
15

根据msdn:Performance Tips and Tricks,您可以使用 try 和 catch 而不会出现任何性能问题,直到真正的 throw 发生

于 2009-03-05T18:36:37.940 回答
12

投掷(而不是接球)是昂贵的。

除非您打算做一些有用的事情(即转换为更有用的异常,处理错误),否则不要放入 catch 块。

只是重新抛出异常(不带参数的 throw 语句),或者更糟糕的是,抛出与刚刚捕获的对象相同的对象,这绝对是错误的事情。

编辑:为避免歧义:

重新抛出:

catch (SomeException) {
  throw;
}

从先前的异常对象创建异常,其中所有运行时提供的状态(特别是堆栈跟踪)都被覆盖:

catch (SomeException e) {
  throw e;
}

后一种情况是丢弃有关异常的信息的毫无意义的方法。并且在 catch 块中的 throw 之前没有任何东西是毫无意义的。情况可能更糟:

catch (SomeException e) {
  throw new SomeException(e.Message);
}

它丢失了几乎所有有用的状态信息 e 包含(包括最初抛出的内容。)

于 2009-03-05T18:31:14.600 回答
2

通常,在 .NET 中引发异常的成本很高。简单地有一个 try/catch/finally 块不是。所以,是的,从性能的角度来看,现有代码很糟糕,因为当它抛出时,它会抛出 5-6 个臃肿的异常,而不会增加任何价值,而不是简单地让原始异常自然地冒出 5-6 个堆栈帧。

更糟糕的是,从设计的角度来看,现有代码确实很糟糕。异常处理的主要好处之一(与返回错误代码相比)是您不需要到处检查异常/返回代码(在调用堆栈中)。您只需要在您真正想要处理它们的少数地方捕获它们。忽略异常(与忽略返回码不同)不会忽略或隐藏问题。这只是意味着它将在调用堆栈的更高层处理。

于 2009-03-05T19:00:22.780 回答
2

这本身并不可怕,但人们应该真正注意筑巢。

可接受的用例如下:

我是一个低级组件,可能会遇到许多不同的错误,但是我的消费者只对特定类型的异常感兴趣。因此我可以这样做:

catch(IOException ex)
{
    throw new PlatformException("some additional context", ex);
}

现在,这允许消费者执行以下操作:

try
{
    component.TryThing();
}
catch(PlatformException ex)
{
   // handle error
}

是的,我知道有些人会说,但是消费者应该捕获 IOException ,但这取决于消费代码的实际抽象程度。如果 Impl 正在将某些内容保存到磁盘并且消费者没有正当理由认为他们的操作会触及磁盘怎么办?在这种情况下,将此异常处理放在消费代码中是没有意义的。

通过使用这种模式,我们通常试图避免的是在业务逻辑代码中放置一个“包罗万象”的异常处理程序,因为我们想找出所有可能的异常类型,因为它们可能会导致需要解决的更基本的问题进行了调查。如果我们没有捕捉到,它就会冒泡,到达“顶级”级别的处理程序,并且应该停止应用程序继续运行。这意味着客户报告该异常,您将有机会对其进行调查。当您尝试构建强大的软件时,这一点很重要。您需要找到所有这些错误情况并编写特定的代码来处理它们。

不是很漂亮的是嵌套过多,这就是你应该用这段代码解决的问题。

正如另一张海报所说,例外是合理的异常行为,但不要走得太远。基本上,代码应该表达“正常”操作,异常应该处理您可能遇到的潜在问题。

在性能异常方面很好,如果您使用调试器对嵌入式设备进行测试,您将得到可怕的结果,但在没有调试器的版本中,它们实际上非常快。

人们在讨论异常的性能时忘记的主要事情是,在错误情况下,一切都会变慢,因为用户遇到了问题。当网络中断并且用户无法保存他们的工作时,我们真的关心速度吗?我非常怀疑以几毫秒的速度将错误报告返回给用户是否会有所作为。

讨论异常时要记住的主要准则是异常不应发生在正常的应用程序流程中(正常意味着没有错误)。其他一切都源于该声明。

在您给出的确切示例中,我不确定。在我看来,将看似通用的 tcl 异常包装在另一个通用的听起来 tcl 异常中并没有真正获得任何好处。如果有的话,我建议追踪代码的原始创建者并了解他的想法背后是否有任何特定的逻辑。虽然你可能会杀死捕获物。

于 2009-03-05T20:18:43.840 回答
1

Try / Catch / Throw 很慢 - 一个更好的实现是在捕获它之前检查值,但如果你绝对不能继续,你最好只在它重要时投掷和捕捉。否则,检查和记录会更有效。

于 2009-03-05T18:33:17.853 回答
0

如果堆栈的每一层都只是用相同的信息重新抛出相同的类型,没有添加任何新内容,那么这完全是荒谬的。

如果它发生在独立开发的库之间的边界,这是可以理解的。有时库作者想要控制从他们的库中逃逸的异常,以便他们以后可以更改其实现,而不必弄清楚如何模拟以前版本的异常抛出行为。

在没有充分理由的情况下,在任何情况下接住并重新投掷通常是一个坏主意。这是因为一旦找到 catch 块,就会执行 throw 和 catch 之间的所有 finally 块。只有在可以从中恢复异常时才会发生这种情况。在这种情况下没关系,因为正在捕获特定类型,因此代码的作者(希望)知道他们可以安全地撤消自己的任何内部状态更改以响应该特定异常类型。

因此,这些 try/catch 块可能在设计时产生成本——它们使程序更加混乱。但是在运行时,它们只会在抛出异常时产生很大的成本,因为将异常向上传输到堆栈已经变得更加复杂。

于 2009-03-05T19:01:49.173 回答
-1

异常很慢,尽量不要使用它们。

请参阅我在这里给出的答案。

基本上,Chris Brumme(CLR 团队的成员)说它们是作为 SEH 异常实现的,所以当它们被抛出时你会受到很大的打击,当它们在操作系统堆栈中冒泡时,你必须承受惩罚。这是一篇真正优秀的文章,深入探讨了抛出异常时会发生什么。例如。:


当然,异常的最大代价是你实际抛出异常的时候。我会在博客快结束时回到这个话题。


表现。当您实际抛出和捕获异常时,异常会产生直接成本。它们还可能具有与在方法入口上推送处理程序相关的间接成本。通过限制代码生成机会,它们通常会付出不小的代价。


但是,有一个严重的长期性能问题,例外情况必须考虑到您的决定。

考虑抛出异常时发生的一些事情:

  • 通过解释编译器发出的元数据来获取堆栈跟踪,以指导我们的堆栈展开。

  • 在堆栈上运行一系列处理程序,调用每个处理程序两次。

  • 补偿 SEH、C++ 和托管异常之间的不匹配。

  • 分配一个托管的 Exception 实例并运行它的构造函数。这很可能涉及查找各种错误消息的资源。

  • 可能通过操作系统内核进行一次旅行。经常出现硬件异常。

  • 通知任何附加的调试器、分析器、向量异常处理程序和其他相关方。

这距离从您的函数调用返回 -1 仅数光年。异常本质上是非本地的,如果今天的架构有一个明显且持久的趋势,那就是你必须保持本地以获得良好的性能。

有些人会声称异常不是问题,没有性能问题,通常是一件好事。这些人得到了很多普选票,但他们完全错了。我已经看到微软员工提出了同样的要求(通常具有通常为营销部门保留的技术知识),但从马口中得出的底线是要谨慎使用它们。

古老的格言,异常应该只用于特殊情况,这对于 C# 和其他任何语言一样都是正确的。

于 2009-03-05T19:02:42.993 回答