抛出异常的性能注意事项
-
08-06-2019 - |
题
我多次遇到以下类型的代码,我想知道这是否是一个好的实践(从性能角度来看):
try
{
... // some code
}
catch (Exception ex)
{
... // Do something
throw new CustomException(ex);
}
基本上,编码人员所做的是将异常包含在自定义异常中并再次抛出。
这在性能上与以下两者有何不同:
try
{
... // some code
}
catch (Exception ex)
{
.. // Do something
throw ex;
}
或者
try
{
... // some code
}
catch (Exception ex)
{
.. // Do something
throw;
}
抛开任何功能或编码最佳实践参数不谈,这 3 种方法之间是否存在性能差异?
解决方案
@布拉德·塔特罗
在第一种情况下,异常不会丢失,而是被传递给构造函数。不过,我会同意你的其余观点,第二种方法是一个非常糟糕的主意,因为会丢失堆栈跟踪。当我使用 .NET 时,我遇到了许多其他程序员这样做的情况,当我需要查看异常的真正原因时,却发现它被从一个巨大的 try 块中重新抛出,这让我感到非常沮丧。我现在不知道问题出在哪里。
我也同意布拉德的评论,即你不应该担心性能。这种微观优化是一个可怕的想法。除非您正在谈论在长时间运行的 for 循环的每次迭代中抛出异常,否则您很可能不会因异常使用而遇到性能问题。
当您有指标表明您需要优化性能时,请始终优化性能,然后找到被证明是罪魁祸首的地方。
拥有易于调试功能的可读代码(IE 不隐藏堆栈跟踪)比让某些东西运行得更快纳秒要好得多。
关于将异常包装到自定义异常中的最后一点注意...这是一个非常有用的构造,尤其是在处理 UI 时。您可以将每个已知且合理的异常情况包装到某个基本自定义异常(或从所述基本异常扩展而来的异常)中,然后 UI 就可以捕获此基本异常。捕获异常后,异常需要提供向用户显示信息的方法,例如 ReadableMessage 属性或类似的内容。因此,任何时候 UI 错过异常,都是因为需要修复错误,而任何时候捕获异常,都是已知的错误情况,UI 可以而且应该正确处理。
其他提示
显然,您会因创建新对象(新异常)而受到惩罚,因此,正如您对附加到程序中的每一行代码所做的那样,您必须确定更好的异常分类是否值得额外的工作。
作为做出该决定的一条建议,如果您的新对象没有携带有关异常的额外信息,那么您可能会忘记构造新的异常。
然而,在其他情况下,拥有异常层次结构对于类的用户来说非常方便。假设您正在实现 Facade 模式,到目前为止所考虑的场景都不好:
- 将每个异常作为 Exception 对象引发并不好,因为您正在丢失(可能)有价值的信息
- 举起你捕捉到的每一种物体都不好,因为这样做你就无法创建外观
在这种假设的情况下,更好的做法是创建异常类的层次结构,将用户从系统的内部复杂性中抽象出来,使他们能够了解所产生的异常类型。
作为旁注:
我个人不喜欢使用异常(从 Exception 类派生的类的层次结构)来实现逻辑。就像这个案例一样:
try {
// something that will raise an exception almost half the time
} catch( InsufficientFunds e) {
// Inform the customer is broke
} catch( UnknownAccount e ) {
// Ask for a new account number
}
和大卫一样,我认为第二个和第三个表现更好。但这三个人中的任何一个会表现不佳到需要花时间去担心吗?我认为还有比性能更大的问题需要担心。
FxCop 始终推荐使用第三种方法而不是第二种方法,这样原始堆栈跟踪就不会丢失。
编辑:删除了完全错误的内容,迈克很友善地指出了这一点。
不要这样做:
try
{
// some code
}
catch (Exception ex) { throw ex; }
因为这将丢失堆栈跟踪。
相反,做:
try
{
// some code
}
catch (Exception ex) { throw; }
只要抛出就可以了,如果你希望它成为新的自定义异常的内部异常,你只需要传递异常变量。
正如其他人所说,最好的性能来自底部的一个,因为您只是重新抛出一个现有的对象。中间的一个是最不正确的,因为它会丢失堆栈。
如果我想解耦代码中的某些依赖关系,我个人会使用自定义异常。例如,我有一个从 XML 文件加载数据的方法。这可能会以多种不同的方式出现错误。
它可能无法从磁盘读取(FileIOException),用户可能尝试从不允许的地方访问它(SecurityException),文件可能已损坏(XmlParseException),数据可能采用错误的格式(DeserializationException)。
在这种情况下,调用类更容易理解这一切,所有这些异常都会重新抛出一个自定义异常 (FileOperationException),这意味着调用者不需要引用 System.IO 或 System.Xml,但仍然可以通过枚举访问发生的错误以及任何重要信息。
如前所述,不要尝试对这样的事情进行微观优化,抛出异常的行为是这里发生的最慢的事情。最好的改进就是尽量避免异常。
public bool Load(string filepath)
{
if (File.Exists(filepath)) //Avoid throwing by checking state
{
//Wrap anyways in case something changes between check and operation
try { .... }
catch (IOException ioFault) { .... }
catch (OtherException otherFault) { .... }
return true; //Inform caller of success
}
else { return false; } //Inform caller of failure due to state
}
第一个示例中的 throw 具有创建新 CustomException 对象的开销。
第二个示例中的重新抛出将抛出 Exception 类型的异常。
第三个示例中的重新抛出将抛出与“某些代码”抛出的相同类型的异常。
因此第二个和第三个示例使用的资源较少。
等待....如果抛出异常,我们为什么要关心性能?除非我们使用异常作为正常应用程序流程的一部分(这违背了最佳实践)。
我只看到了与成功有关的性能要求,但从未见过与失败有关的性能要求。
从纯粹的性能角度来看,我猜第三种情况是性能最好的。另外两个需要提取堆栈跟踪并构造新对象,这两者都可能相当耗时。
话虽如此,这三个代码块已经 非常 不同的(外部)行为,因此比较它们就像询问快速排序是否比向红黑树添加项目更有效。这并不像选择正确的事情那么重要。