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

Dan*_*ley 17 .net c# exception-handling interface exception

.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);
}
Run Code Online (Sandbox Code Playgroud)

这很棒,直到您需要一个继承了另一个系统异常类型(如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);
}
Run Code Online (Sandbox Code Playgroud)

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

Ank*_*kur 17

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

class MyException : IExceptionB, IExceptionA
{
}

try
{
 throw new MyException();
}
catch(IExceptionB b){}
catch(IExceptionA b){}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 如果你把两个捕获量放在一起,你如何解决ArgumentNullException和ArgumentException之间的区别?这是同样的问题.(第二个继承了第一个).`catch`的顺序定义了它们的相对优先级. (11认同)
  • 但是,父和子异常类型可能会发生这种情况.在这种情况下,框架使用第一个catch语句,但如果第一个引发与第二个类型匹配的类型,则会执行第二个catch语句. (2认同)
  • 将使用第一个匹配的 catch 块,因为它现在是通过 Exception 类型继承实现的。 (2认同)

Ale*_*lex 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);
}
Run Code Online (Sandbox Code Playgroud)

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

public class CompanysLoggedException : InvalidOperationException  //Could be any Exception
{
    public CompanysLoggedException()
    {
        this.SetLogged();
    }
}
Run Code Online (Sandbox Code Playgroud)

try/catch用法:

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();
}
Run Code Online (Sandbox Code Playgroud)

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


Ach*_*him 5

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


Ste*_*edy 5

C#6 引入了异常过滤,所以你所要求的现在在 C# 中是可能的(它在 VB.Net 中早已成为可能)。我们现在可以使用when关键字。

这是重构为使用新语法的代码:

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

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

类和接口定义:

public interface ILoggedException { }

public class CustomLoggedException : Exception, ILoggedException { ... }
Run Code Online (Sandbox Code Playgroud)