为什么.NET异常不能用于接口而不是基类?

时间:2011-09-15 12:21:58

标签: c# .net exception interface exception-handling

.Net框架try-catch实现只允许您捕获从基类“System.Exception”继承的类型。为什么这不是“System.IException”这样的界面?

用例

我们在继承System.Exception的每个API中使用自定义基类。只有在记录异常后才会抛出此异常,因此我们可以通过以下方式轻松避免重新记录:

try
{
    // Do something.
}
catch (LoggedException)
{
    // Already logged so just rethrow.
    throw;
}
catch (Exception ex)
{
    // TODO: Log exception.
    throw new LoggedException("Failed doing something.", ex);
}

这很棒,直到你想要一个继承了另一个系统异常类型的自定义异常,比如System.FormatException

现在处理这两种方法的唯一方法是拥有两个自定义基类型并复制每个catch语句。

重构

如果.net框架只是简单地寻找诸如System.IException之类的东西,那么你可以简单地拥有一个自定义异常接口,例如CompanyName.ILoggedException,继承System.IException,所有自定义异常类型都会实现。因此,您的新catch代码如下所示:

try
{
    // Do something.
}
catch (ILoggedException)
{
    // Already logged so just rethrow.
    throw;
}
catch (IException ex)
{
    // TODO: Log exception.
    throw new CustomException("Failed doing something.", ex);
}

框架是否有这样的实际原因?或者这将是未来版本的.Net框架中要求的东西吗?

8 个答案:

答案 0 :(得分:17)

您可能知道在基类情况下我们只有单继承但是在接口的情况下,一个类可以实现许多接口,所以如果您有类似的东西:

class MyException : IExceptionB, IExceptionA
{
}

try
{
 throw new MyException();
}
catch(IExceptionB b){}
catch(IExceptionA b){}

现在这会产生关于如何决定调用哪个catch处理程序的模糊性,因为异常实现了两个接口,即在层次结构方面两者都处于同一级别,而基类不会在同一级别上有两个类。

代码是假设,显示允许基于接口的catch时的问题

答案 1 :(得分:7)

尚未提及的一个可能更简洁的解决方法是使用扩展方法。通过利用Exception.Data字段,您可以从单个catch块中巧妙地发现当前异常是否已被记录,并根据需要采取操作。这将允许您构建许多不同的公司特定异常(隐式已经记录)。

需要扩展方法:

private const string ExceptionLoggedKey = "IsExceptionLogged";

public static bool IsLogged(this Exception exception)
{
    if (exception.Data.Contains(ExceptionLoggedKey))
    {
        return (bool)exception.Data[ExceptionLoggedKey];
    }
    return false;
}

public static void SetLogged(this Exception exception)
{
    exception.Data.Add(ExceptionLoggedKey, true);
}

公司例外采用以下格式,在构造函数中设置IsLogged标志:

public class CompanysLoggedException : InvalidOperationException  //Could be any Exception
{
    public CompanysLoggedException()
    {
        this.SetLogged();
    }
}

尝试/捕获用法:

try
{
    throw new ArgumentException("There aren't any arguments.");
}
catch (Exception ex)
{
    if (ex.IsLogged())
        //Nothing additional to do - simply throw the exception
        throw;
    else
        //TODO Log the exception
        throw new CompanysLoggedException();
}

我同意这肯定不如基于实现的接口匹配异常的能力那么整洁,但我认为该模式非常简洁和可读。必须记住将SetLogged()的调用添加到每个定义的新公司异常,但这有点不足。

答案 2 :(得分:5)

我真的不知道原因,但我认为这是关于性能的。如果发生异常,每个catch块都必须检查它是否能够处理异常。如果只允许类型,那么在.Net的情况下这非常简单,因为你只有一个继承。实现的接口的继承树可能变得更加复杂。

答案 3 :(得分:2)

在VB中可以通过使用类似的东西来捕获接口:

  ' Doesn't require external function, but will will require typecast
  ' if it needs to actually use Ex as IMyExceptionInterface
  Catch Ex As Exception When TypeOf(Ex) Is IMyExceptionInterface
  ' Alternate form: requires pre-declared variable and a function:
  ' Function TryCastIntoSecondParam(Of TSource As Class, TDest As Class) _
  '                                (ByVal Thing As TSource,  ByRef Dest As TDest)
  '   Dim Result As TDest
  '   Result = TryCast(Thing, TDest)
  '   Dest = Result
  '   Return Dest IsNot Nothing
  ' End Function
  Catch Ex As Exception When TryCastIntoSecondParam(Ex, myIMyException)

如果VB或C#编译器实现者想这样做,他们可以允许一个人使用语法

  Catch Ex As IMyExceptionInterface  ' vb
  catch IExceptionInterface ex       ' C#

并使用上面的代码实现它。即使没有编译器支持,vb用户也可以使用上面的代码获得正确的语义。在C#中,有必要捕获异常,测试它是否是所需类型,如果不是则重新抛出;其语义与使用过滤器的语义不同,以避免首先捕获异常。请注意,对于C#编译器来实现上述结构,它必须使用过滤器块,但它不必向程序员公开过滤器块的所有功能 - 这是C#实现者故意拒绝的。

所有这一切,我怀疑原始问题的答案可能是“设计师无法想到任何用于此类事情的好用例”,或者它可能是“接口需要更复杂的类型分辨率而不是这些类,创造了仅仅决定是否捕获异常的行为的可能性,除了它自己的例外之外可能会失败。“

实际上,我碰巧不喜欢使用类类型来决定捕获哪些异常,因为是否捕获异常的问题通常与导致异常的问题大致正交。如果加载文档的尝试失败,我对于某个参数是否超出范围或某个索引超出范围的问题几乎不感兴趣,因为我在是否可以安全地从尝试中恢复的问题通过假装我从来没有成功。真正需要的是异常的“严重性”度量,可以随着异常在呼叫链中冒泡而增加或减少。这样的事情在vb.net中可能有点实用(它有异常过滤器),但可能不在C#中(但没有),但在任何情况下都会因内置异常中缺少任何支持而受到限制。 / p>

修改/附录

如果使用用vb编写的DLL来实现调用某些委托的try / filter / catch / finally包装器,则可以在C#项目中使用异常过滤器。不幸的是,使用这样的DLL需要在运行时效率和代码易读性之间进行一些权衡。我没有想过为捕获任意数量的接口的特定优化目的实现这样的DLL;我不知道在DLL中包含这样的功能是否有任何好处,而不是传递lambda表达式或匿名方法来测试是否应该捕获异常。

顺便说一下,包装器可以提供的另一个特性是C#中缺少的功能是能够报告双故障情况(主线发生异常,而后续“最后”块发生另一个异常)而不必赶上最初的例外。当finally块中发生异常时,这通常是比主线中发生的异常更大的问题,因此不应该被扼杀,但是允许“最终阻塞”异常来渗透调用堆栈通常会破坏任何证据最初的例外。虽然周围的代码可能比原始异常对清理失败更感兴趣,但记录这两个异常比扼杀原始异常更有用。

答案 4 :(得分:2)

C#6引入了exception filtering所以你现在要求的是C#(it has long been possible in VB.Net)。我们现在可以使用when关键字。

以下是您使用新语法重构的代码:

try
{
      ...
}
catch (Exception ex) when (!(ex is ILoggedException))
{
    // TODO: Log exception.
    throw new Exception("Failed doing something.", ex);
}

请注意,我们不再需要第一个catch块,因为它本质上只是一个过滤器,所有它都是throw

类和接口定义:

public interface ILoggedException { }

public class CustomLoggedException : Exception, ILoggedException { ... }

答案 5 :(得分:1)

异常不仅仅是让代码知道某些内容出错的方法。它包含各种调试信息(堆栈跟踪,目标,内部异常,hresult,Watson存储桶等),这必须在某个地方收集。最合乎逻辑且最简单的解决方案是让基类收集它。

答案 6 :(得分:1)

这几乎是你提到的,没有一些“代码糖”的想法:

try
{
}
catch(LoggerException ex) 
{ 
    ex.WriteLog(); 
} 
catch(Exception ex)
{
    ILoggerException1 l1 = ex as ILoggerException1; 
    if (l1 != null)
    {
        l1.WriteLog1();
    }
    else
    {
        ILoggerException2 l2 = ex as ILoggerException2; 
        if (l2 != null)
        {
            l2.WriteLog2();
        }
        else
        {
            ILoggerException3 l3 = ex as ILoggerException3; 
            if (l3 != null)
            {
                l3.WriteLog3();
            }
            else
            {
                throw ex;
            }
        }
    }
}

在编译器的支持下,它应该写成:

try
{
}
catch(LoggerException ex)
{
    ex.WriteLog();
}
// no more classes is allowed by the compiler be here, only interfaces in a tail catch recursion
catch(ILoggerException1 ex1)
{
    ex1.WriteLog();
}
catch(ILoggerException2 ex2)
{
    ex2.WriteLog();
}
catch(ILoggerException3 ex3)
{
    ex3.WriteLog();
}

答案 7 :(得分:0)

自框架2.0以来,有一个界面:

System.Runtime.InteropServices._Exception

有关此界面的更多信息,请转到here

我使用了一种解决方法,并使用了一个抽象类并实现了该类。对我来说感觉很脏,但它实际上更容易实现我需要的东西。