【问题标题】:How to deal with interfaces that can throw just about anything?如何处理几乎可以抛出任何东西的接口?
【发布时间】:2012-02-21 13:41:08
【问题描述】:

我经常发现自己处于必须处理接口实现者抛出的一些异常的情况。当不同的实现处理完全不同类型的设备等时,这通常会出现问题:根据实现的不同,抛出的异常的数量和类型会有很大差异。这是一个典型的例子:

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
}

可能所有这些也会抛出 ArgumentExceptions 等等,但我对捕捉这些不感兴趣:在这种情况下,它们将指示编程错误。但是我不希望程序在文件损坏、网线拔出或自定义设备失控时崩溃,所以应该处理其他异常。

我已经尝试了几个解决方案,但没有一个是我特别满意的,所以问题是:是否有一些常见的做法/模式来处理此类情况?请具体一点,不要告诉我“看看 ELMAH”。以下是我过去使用的一些东西:

  • catch( Exception ) 嗯,问题很明显
  • TryAndCatchDataStream( Action what, Action<Exception> errHandler ) 方法由一个 try 和一个 catch 组成,用于任何实现中感兴趣的任何异常。这意味着它必须在实现更改或添加/删除时进行更新。
  • 在接口上添加注释说“应该只抛出 DataStreamExceptions”,并且在所有实现中确保遵循此规则,捕获任何感兴趣的内容并将其包装在 DataStreamException 中,然后将其抛出。还不错,但会给每个实现增加噪音。

然后我有第二个关于异常处理的更一般的问题(认为不需要为此单独发帖):我有一些情况,方法 A 调用 B,它调用 C 调用 D,而 D 抛出一个SomeException 但 A 的调用者捕获了异常。这不是代码有问题的迹象吗?因为要做到这一点,A 的调用者需要知道 A 最终会调用 D。除非 A 记录它可以抛出 SomeException;但在这两种情况下,这意味着当更新 A 的单纯实现细节时(即让它调用 D 以外的其他东西),此更改对 A 的用户是可见的。

【问题讨论】:

  • 我会选择第三个选项。对于你的第二个问题,我认为这不是问题,只要它被评论它可以抛出那个异常。怎么能扔? 封装
  • 这无关紧要,因为您应该只捕获您知道如何处理的异常。如果你连异常是什么都不知道,那么你显然无法处理它。

标签: c# exception-handling error-handling


【解决方案1】:

没有办法“知道”未知异常可能是什么。你所能做的就是捕捉你知道的异常,也许记录你不知道的异常并重新抛出它们。

您可以捕获异常,然后在单独的程序集中调用一个方法来确定如何处理异常。然后您可以根据需要更新程序集,而无需更新其余代码。

关于您的第二个问题,A 需要记录可能从中冒出的任何异常,包括依赖对象引发的异常。 B 需要记录它抛出的异常,以便 A 可以了解它们,等等.. 等等..

如果您更改 A 以执行导致引发的异常发生更改的操作,那么您必须更改文档。从调用应用程序的角度来看,它所知道的只是 A。A 可以做任何事情,而调用应用程序并不关心它是 B、C 还是 D。A 负责向调用者提供任何信息。

这样想。假设您聘请了一家建筑公司为您建造房屋。反过来,他们雇用分包商。这些分包商可能会雇用自己的劳动力。如果底层工人搞砸了,最终你雇佣的承包商有过错,不管是不是其他公司雇佣了他们。你不在乎,你只希望你的房子按照规格建造,建筑公司对此负责。

【讨论】:

  • +1 很好的比喻。然而,这让我想到了别的东西:结合我的两个问题,所以 A 调用 B、C、D 和 D 是抛出任何东西的接口方法。我想在这种情况下,C 应该尽可能多地处理 D 抛出的问题?
  • @stijn - 并非所有异常都需要处理。至少不在低水平。如果顶级调用者想要一个文件,而该文件不存在,则必须传递。应该只处理特定于实现的异常。
  • @MystereMan:如果调用者想要从文件中读取文档,并且在反序列化部分文件时发生 ArgumentRangeException,那么应该通过什么方式处理异常,以便调用者知道放弃部分-loaded 文件会把一切都放好?有什么办法可以避免在应用程序的所有级别上大量膨胀代码或不得不使用 Poke'mon 异常处理?
【解决方案2】:

您可以使用抽象类而不是接口将来自各种实现的异常包装在基类中,以便能够以通用方式包装底层异常。这样,您就有一个中心点来改变您的异常包装和分析逻辑。一些异常可能很严重,仍然会导致您的进程终止,例如 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();
        }
    }

【讨论】:

    【解决方案3】:

    从根本上说,捕获异常的代码需要知道很多事情:

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

    不幸的是,包括 C++、Java 和 .net 在内的许多语言和框架都试图使用一种相当笨拙的信息(抛出的异常对象的类型)来传达所有三种信息,尽管实际上它们“ re 通常在很大程度上是正交的。这种设计的一个特别值得注意的困难是,有可能同时出现多个异常情况,因此适用于其中任何一个的处理程序应该运行,但异常应该继续向上堆栈,直到适用于所有它们的处理程序都运行。

    尽管基于类型的设计存在局限性,但必须使用它。在设计自己的新类/接口以及从中泄漏的异常时,应该尝试使用不同的异常类型来回答上面的问题#3-#4,因为这些问题最有可能引起调用者的兴趣。我会进一步建议,如果从方法内抛出的异常可能使类处于无效状态,则可能需要类具有状态变量或标志,该状态变量或标志可能会使用类似以下的模式:

    ...在做任何可能导致对象暂时进入无效状态的事情之前... if (state != MyClassState.HappyIdle) throw new StateCorruptException(....); state = MyClassState.UnhappyOrBusy; ... 以一种在完成时会以有效状态结束的方式操作状态... 状态 = MyClassState.HappyIdle;

    如果始终使用这种模式,调用者就不必太担心导致异常的操作可能会使对象处于损坏状态,从而尝试继续操作可能会导致进一步的数据丢失.如果对象已损坏但调用代码忽略了发生该异常时发生的异常,则进一步尝试使用该对象将完全失败。

    不幸的是,许多类并没有很好地使用此类防护,因此假设未知异常是“无害的”可能并不安全。另一方面,从实际的角度来看,没有任何方法既健壮又安全。要么冒着让一个人的代码因一个实际上无害的异常而不必要地死掉的风险,要么冒着破坏数据结构失控并破坏其他数据结构的风险。在设计更好的异常系统中,会有更好的替代方案,但在现有的系统中却没有。

    【讨论】:

      【解决方案4】:

      您可以实现自己的自定义异常。如果接口捕获自己的异常并抛出您的自定义异常,同时将真正的异常设置为内部,则拥有每个实现。根据您的情况,这可能没问题。

      因为异常应该是异常的。这样,您至少可以知道您是否正在捕获实现知道要注意的异常。

      不确定这是否是个好主意。只是想我会提供这个想法。

      【讨论】:

      • 嗯,这基本上是我提到的第三种解决方案:]
      【解决方案5】:

      Java 强制您声明要抛出的异常,这是一个真正令人讨厌的痛苦,几乎没有什么好处 - 通常是因为异常是在特殊情况下抛出的,而且很难提前计划这些异常。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-25
        • 1970-01-01
        • 2020-12-13
        • 1970-01-01
        • 2016-07-19
        相关资源
        最近更新 更多