如何处理可能抛出任何异常的接口?

4

我经常遇到需要捕获实现接口时抛出的异常情况。当不同的实现处理完全不同类型的设备等时,这通常会变得棘手:根据具体实现,抛出的异常种类和数量都大相径庭。以下是一个典型的例子:

interface DataStream
{
  Data ReadNext();
}

class DeserializingFileDataStream : DataStream
{
  //this one does file operations + serialization,
  //so can throw eg IOException, SerializationException, InvalidOperationException, ...
}

class NetworkDataStream : DataStream
{
  //get data over tcp
  //so throws IOException, SocketException
}

class HardwareDeviceDataStream : DataStream
{
  //read from a custom hardware device implemented in unmanaged code
  //so throws mainly custom exceptions
}

很可能所有这些也会抛出ArgumentException等异常,但我不感兴趣捕获这些异常:在这种情况下,它们会表示编程错误。然而,当文件损坏、网络电缆被拔掉或自定义设备失控时,我不希望程序崩溃,因此其他异常应该被处理。
我已经尝试了一些解决方案,但都没有特别满意,所以问题是:有没有一些常见的做法/模式来处理这种情况?请具体说明,不要告诉我去“看看ELMAH”。以下是我过去使用过的一些方法:
- `catch( Exception )` 很明显知道问题在哪里。 - `TryAndCatchDataStream( Action what, Action<Exception> errHandler )` 方法由一个 try 后跟任何实现的任何感兴趣的异常的 catch 组成。这意味着当实现更改或添加/删除时必须更新。 - 在接口上放置一个注释,说“只能抛出 DataStreamExceptions”,并在所有实现中确保遵循此规则,通过捕获任何感兴趣的异常并将其包装在 DataStreamException 中,然后抛出。不太糟糕,但会给每个实现增加噪音。
然后我有一个关于异常处理的第二个更一般的问题(不认为有必要为此发布一个单独的帖子):我有几种情况是方法 A 调用 B,B 调用 C,C 调用 D,而 D 抛出 SomeException 但是 A 的调用者捕获了异常。这不是代码有问题的信号吗?因为为了能够这样做,A 的调用者需要知道 A 最终会调用 D。除非 A 记录它可以抛出 SomeException;但在两种情况下,这都意味着当更新 A 的仅仅是实现细节(即让它调用与 D 不同的东西)时,这个变化对 A 的用户可见。

我会选择第三个选项。至于你的第二个问题,只要有注释说明它可以抛出异常,我认为这不是问题。如何抛出异常?封装! - gdoron
1
这是无关紧要的,因为你只应该捕获你知道如何处理的异常。如果你甚至不知道异常是什么,那么显然你无法处理它。 - Cody Gray
5个回答

4
没有办法“知道”未知异常是什么。你所能做的就是捕获你已知的异常,可能记录下未知的异常并重新抛出它们。
你可以捕获Exception,然后调用一个在另一个程序集中确定如何处理异常的方法。然后,你可以根据需要更新程序集,而不必更新其余代码。
关于你的第二个问题,A需要记录可能从它那里冒出来的任何异常,包括由依赖对象抛出的异常。B需要记录它抛出的异常,这样A就可以知道它们,等等。
如果你更改了A以执行导致异常变化的操作,则必须更改文档。从调用应用程序的角度来看,它只知道A。A可以执行任何操作,调用应用程序并不关心它是否是B、C或D。A负责向呼叫者产生泡沫。
可以这样想。假设你雇用一家建筑公司为你建造一座房子。他们又雇用了子承包商。这些子承包商可能会雇用自己的工人。如果底层的工人搞砸了,最终你雇佣的承包商会受到指责,无论是哪个公司雇佣了他们。你不在乎,你只想按规格建造你的房子,建筑公司对此负责。

+1 很好的比喻。不过这让我想到了另外一件事情:将我的两个问题结合起来,所以A调用B、C、D,而D是接口方法抛出任何异常。我猜在这种情况下,C应该尽可能地处理D抛出的异常? - stijn
2
@stijn - 并非所有异常都需要处理。至少在低层级别上不需要。如果顶层调用者想要一个文件,而文件不存在,则必须将其传递上去。只有特定于实现的异常应被处理。 - Erik Funkenbusch
@MystereMan:如果调用者想从文件中读取文档,并且在反序列化其中一部分时发生ArgumentRangeException异常,那么应该通过什么方式处理异常,以便调用者知道放弃部分加载的文档将使一切恢复正常?是否有任何方法可以避免在应用程序的所有级别上大量膨胀代码或使用Poke'mon异常处理? - supercat

3
您可以使用抽象类代替接口,将各种实现的异常包装在基类中,以便能够以一种普遍的方式包装底层异常。这样,您就有一个中心点来改变和分析异常包装逻辑。某些异常可能非常严重,仍应导致进程终止,例如OutOfMemoryExcepptions,在这种情况下,继续运行可能不太安全。
如果您坚持要使用接口,则应等待某个未来的CLR版本,其中接口可以具有基础实现。有一段视频Vance Morrison讲述了这种功能,但我再也没有听说过。
如果您无法等待CLR vNext,则可以创建一个包装器类(称其为DataStreamReader),该类采用实现IDataStream的IDataStream接口,但按您需要进行包装。如果您确保在读取时只使用DataStreamReader实例的代码中,那么应该没问题。
该设计强制任何继承者实现ReadNextImpl方法,在此处,您的消费者执行其逻辑。您像以前一样调用ReadNext,并始终使用当前简单实现返回DataStreamException。这是我能想到的最简单的解决方案。
   class Data { }
    public class DataStreamException : Exception
    {
        public DataStreamException(string message, Exception inner)
            : base(message, inner)
        {   }
    }

    abstract class DataStream
    {
        protected abstract Data ReadNextImpl();
        public Data ReadNext()
        {
            try
            {
                return ReadNextImpl();
            }
            catch (Exception ex)
            {
                throw new DataStreamException("Could not read from stream. See inner exception for details.", ex);
            }
        }
    }

    class DeserializingFileDataStream : DataStream
    {
        protected override Data ReadNextImpl()
        {
            throw new NotImplementedException();
        }

    }

    class NetworkDataStream : DataStream
    {
        protected override Data ReadNextImpl()
        {
            throw new Exception();
        }
    }

1

从根本上说,捕获异常的代码需要知道以下几个方面:

  1. 发生了什么?
  2. 该条件是否需要任何特定操作?
  3. 一旦完成上述任何操作(如果有),应将异常条件视为已解决。
  4. 任何适用对象的状态将是什么?

不幸的是,许多语言和框架,包括C ++,Java和.NET,都试图使用一种相当笨重的信息(所抛出异常对象的类型)来传达所有三个信息,即使在实践中它们通常是大部分正交的。这样设计的一个明显难点是,可能会同时出现多个异常条件,使得适用于任何条件处理程序都应运行,但异常应该继续上升到堆栈直到所有条件的处理程序都运行。

尽管类型设计存在局限性,但我们必须与之共事。在设计自己的新类/接口和从它们泄漏的异常时,应尝试使用不同的异常类型来回答上述第3-4个问题,因为这些问题最有可能引起调用者的兴趣。我进一步建议类具有状态变量或标志,如果从方法中抛出的异常可能使类处于无效状态,则可以将其无效化,可能使用以下模式:
  ... 在执行任何可能导致对象暂时进入无效状态之前 ...
  if (state != MyClassState.HappyIdle) throw new StateCorruptException(....);
  state = MyClassState.UnhappyOrBusy;
  ... 以一种能够在完成时得到有效状态的方式操作状态 ...
  state = MyClassState.HappyIdle;
如果始终使用此类模式,调用者就不必过多担心操作可能导致对象处于损坏状态,从而尝试继续操作可能会导致进一步数据丢失的可能性。如果对象已损坏但调用代码忽略了发生的异常,那么进一步使用对象的尝试将干净地失败。

不幸的是,许多类并没有使用这些保护措施来进行很好的保护,因此不能安全地假设未知异常是“无害”的。另一方面,从实际角度来看,没有一种方法既强大又安全。要么冒着代码不必要地死亡的风险,而这个异常实际上是无害的,要么冒着数据结构失控并破坏其他数据结构的风险。在设计更好的异常系统中,会有更好的选择,但现有的系统中却没有。


0
您可以实现自己的定制异常。每个接口的实现都要捕获其自己的异常并抛出您的定制异常,同时将真正的异常设置为内部异常。根据情况而定,这可能是可行的。
由于异常应该是特殊情况,这样做至少可以让您知道是否捕获了实现已知要监视的异常。
我不太确定这是否是一个好主意,只是想提供这个想法。

这基本上就是我提到的第三个解决方案 :] - stijn

0

Java 强制你声明抛出的异常,这是一个真正让人头疼的问题,而且好处很少 - 通常因为异常是在特殊情况下抛出的,很难提前计划。

Java 程序员经常会做的事情(在被教导不要使用 throws Exception 后)是选择第三个选项。对于你不希望调用代码知道如何处理的异常,这个第三个选项是有意义的 - 如果你希望调用代码处理它们(比如,在 IOException 或 NetworkException 的情况下重复三次),最好坚持人们已经知道的方法。

所以简而言之,我建议你在调用代码中捕获你知道如何处理的异常,并报告那些你无法处理并退出(或失败操作)。


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