异常层次结构真的有用吗?

10

我突然意识到,我从来没有见过一个单个异常层次结构,其中创建子类但捕捉父类实际上是有用的(当然,除了必须派生的基本异常类Exception)。

异常层次结构真的有用吗,还是所有异常都应该派生自语言的基本异常类?


在普通句子中使用异或(xor)操作,这是个好问题!加一分! - jyoungdev
你在括号注释中是否忘记了一个“不”字? - Wolf
3个回答

5

异常层次结构对于将相关的异常分组很有用,当您需要在不同的位置使用不同的粒度进行捕获时。

将所有应用程序异常放在一个地方是最常见的用例。这允许您在适当的时候捕获更具体的异常,但仍然可以随时捕获MyAppException来捕获来自应用程序的所有错误。(在.NET中,ApplicationException类就是为此而设计的,但由于各种原因已被弃用。)

但是,您还可以按模块边界或任何其他有意义的方式将异常分组。使用FooModuleException处理来自Foo模块的异常,但在Foo内部特别处理FooModuleMustFrobnicate. 或任何等效情况。

我曾经在不同的时间使用过所有这些模式。


4
你有没有一个例子,可以说明在不同的粒度上捕捉异常是有实际用途的?我的意思是,如果我们抛开所有理论,看看实际情况,我从来没有见过任何地方使用异常捕获的粒度。但另一方面,我见过很多人在打开文件时捕获 IOException,而他们能得到的仅仅是一个 FileNotFoundException。他们使用 IOException 是因为继承关系让他们认为可能会发生其他相关的事情,但实际上,IOException 的子类在出现的上下文方面相当独立。 - zneak

2
TL;DR
1. 异常层次结构对于捕获异常来说是无用的(除了在非可恢复系统错误(Java Error)和“正常”的运行时异常之间进行基本区分)。认为“在不同的地方需要不同的捕获粒度”这一观念在我看来根本是有缺陷的。(除了区分异常、断言和其他系统范围内无法恢复的东西之外。) 2. 在当今的语言中,小型异常层次结构作为构建具有尽可能丰富信息的不同错误的手段是有意义的。
我正在逐渐考虑整个“任意”类型异常的概念,因为我发现它们在C++、C#和Java中的实现和使用是“完全无用”的。
问题并不在于异常层次结构本身,而是当我编写代码时,异常是“异常”的,我“不关心抛出的异常类型”,“为了捕获它”。
在我所有的C++、Java和C#编程中,我发现——对于那些没有被滥用于控制流的异常——从未有过一个例外情况,我想要通过它们的“分层”类型来筛选(即捕获或不捕获)异常。也就是说,有些情况下函数会抛出3种不同的异常,我想特别处理其中一种,并将另外两种作为一般的“失败”处理(如下所述),但我从未遇到过这种情况,在这种情况下,异常层次结构有用的是我可以通过基类有意义地将这两种情况分组。
如果我的程序中任何一个操作(我关心的操作)以“任何”异常失败,则认为这个操作已失败,并且:
  • 我从捕获的对象中获取尽可能多的信息
  • 继续进行上下文化的错误处理(重新抛出、显示错误、记录、重试、恢复等)

我认为Java是这三种语言中唯一具有正确方法的层次结构,即:

  • Throwable 不在用户代码中使用
  • Error 您永远不想捕获的东西 - 只需核心转储
  • Exception - 用于基本上始终想要捕获的所有内容(除非接口出错并在实际上不应该使用异常时)

//

C++中的<stdexcept>层次结构在区分logic_error(基本上是断言)和runtime_error(只是意外失败的东西)方面有正确的基本方法。(这些子类在catching时大多不太相关。)
当然,除了太多的内容直接从std::exception派生,因此您无法利用由运行时错误和逻辑错误提供的区分。 (当然,任何代码都可以抛出任何他们想要的异常,因此很有可能有一堆应该是runtime_errors而实际上是logic_error导出类,反之亦然。)
引用另一个答案的话:

异常层次结构适用于将相关异常分组在一起,当您需要在不同的地方具有不同的捕获粒度时。

但是根据我的经验,我从来都不想要“在不同的地方捕获”,因为这根本没有意义。
我要么想要捕获所有异常(除了像Java中的Error类异常之类的东西),要么就不想要捕获,此时异常层次不需要,因为根本没有什么可以捕获的。

执行打开和读取文件操作:

如果有我特别关注的异常类型,因为我可以做一些处理(“恢复”),那么这根本不应该被报告为异常,因为这是正常的控制流程。(如果错误是某种类型,我可以在本地执行某些操作。)

另一方面,如果我对错误无能为力,只能记录文件操作失败,那么任何异常捕获粒度都毫无用处。


1
我认为总的来说,我想出了4种类型的错误:FATAL:整个系统都是一场灾难。无法恢复。 USER_MANAGED_SYSTEM_ERROR:例如磁盘空间不足或网络中断。用户需要修复的问题,然后系统才能正常工作。程序可能有一些监控方式来检测情况何时会改变,但现在不应该重试任何操作。 TRANSIENT:系统繁忙或其他原因,但可以在暂停一段时间后再次尝试。 BAD REQUEST:系统没有问题,只是你的请求有误。不要再尝试,每次都会失败,但你可以继续进行其他请求。 - CashCow

1

我从未为整个应用程序创建过单一的异常层次结构,因为一般情况下异常可能会带来不同的语义,因此需要以不同的方式处理。

有些异常是关于系统故障的,另一些异常是关于错误的,还有一些异常是关于某些特殊情况的,这些情况可以在更高级别上优雅地恢复。

对于这些“业务逻辑”异常,异常层次结构可以帮助您在覆盖后代中的某些方法时不违反开闭原则。

考虑以下示例:

public abstract class Base
{
   /// Perform some business-logic operation
   /// and throw BaseFooException in certain circumstances
   public abstract void Foo();
}

public class Derived1 : Base
{
   /// Perform some business-logic operation
   /// and throw DerivedFooException in certain circumstances.
   /// But caller could catch only BaseFooException
   public abstract void Foo() {}
}

class BaseFooException : Exception {}

class DerivedFooException : BaseFooException {}

异常层次结构在自定义框架或特定库中可能会有所帮助,但在一般情况下(在业务应用中),我认为创建一个深而广泛的异常层次结构不是一个好主意。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接