【问题标题】:Why don't .NET exceptions work against an interface rather than a base class?为什么 .NET 异常不能针对接口而不是基类?
【发布时间】:2011-11-17 20:18:15
【问题描述】:

.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 框架的未来版本中需要的东西吗?

【问题讨论】:

  • 您知道您可以简单地在catch (Exception ex) 中使用is 运算符,对吧? if (ex is ILoggedException)
  • @xanatos : 但这是一个 hack/workaround,而不是内置功能
  • @xanatos 是的,在发布后也想到了这一点,但我个人非常希望在未来的版本中看到类似的东西......也许我们可以实现它!
  • 我能想到相当多的案例,人们不禁想知道为什么会做出这样那样的设计决定,有些甚至设计师/MS 开发人员也可能想重新审视 (@987654321 @),但是像异常处理这样的东西需要一个比你提供的更强大的案例。
  • @sll 这不是 hack,还有另一个 C# keywordas, ILoggerException l = ex as ILoggerException; if (l != null) { l.WriteLog();扔前; }

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


【解决方案1】:

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

class MyException : IExceptionB, IExceptionA
{
}

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

现在这造成了关于如何决定调用哪个 catch 处理程序的歧义,因为异常实现了两个接口,即就层次结构而言,两者都处于同一级别,这与基类不同,在同一级别不会有两个类。

如果允许基于接口的捕获,代码是假设的显示问题

【讨论】:

  • 如果你把两个catch都放进去,你如何解决ArgumentNullException和ArgumentException之间的区别?这是同样的问题。 (第二个继承第一个)。 catches 的顺序定义了它们的相对优先级。
  • 但是,这可能发生在父异常类型和子异常类型中。在这种情况下,框架使用第一个 catch 语句,但如果第一个 catch 语句抛出的类型也与第二个类型匹配,则将执行第二个 catch 语句。
  • 你不能捕获接口,IExceptionA/B 不能是一个类,这种情况下你没有多重继承。一个 catch 内的新 throw 不会由同一级别的下一个 catch 处理,即使它与异常类型匹配。这里的答案和其中一个 cmets 都是错误的。每个 try/catch 只使用一个 catch 块,它是第一个与实际抛出的异常类型匹配的块。所以没有歧义。
  • 它将选择第一个块。与正常异常类型的逻辑相同。如果您的第一个 catch 块捕获 Exception,则所有其他代码块都将被忽略。看我的回答。我认为你的想法是对的,但主要问题是性能。
  • 将使用第一个匹配的 catch 块,因为它现在使用 Exception 类型继承实现..
【解决方案2】:

尚未提及的一种可能更简洁的解决方法是使用扩展方法。通过利用 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/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();
}

我同意这绝对不如基于已实现接口匹配异常的能力那么简洁,但我认为该模式非常简洁易读。不过,必须记住将对 SetLogged() 的调用添加到定义的每个新公司异常中是一个缺陷。

【讨论】:

  • 非常有帮助,我昨天只是在查看数据收集,正在起草一个新问题来检查该方法的有效性。我确实喜欢添加扩展方法以方便访问。我想的唯一其他补充是使用单例对象作为键,因为它是一个对象 -> 对象集合而不是字符串,以避免与使用相同键的其他人发生任何可能的冲突。
  • 重构了提取字符串键的答案——这个值应该用扩展方法静态类封装,因为它应该在这个上下文之外访问,所以私有常量
  • 如何在 vb.net 中使用“Catch...When”编写类似上述的内容,并将其编码为方法包装 DLL,从而解决 C# 缺乏异常过滤器的问题?
【解决方案3】:

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

【讨论】:

  • +1 这是一个很好的论点。我不知道这是否真的会成为一个问题。我认为对象的实现类/接口列表保存在其类型中,类似于字典,因此访问它仅比检查父列表贵一点,但仍然......
  • +1 查看我的帖子。同意,但总的来说,我认为在编译器中实现它并不难,我很难将它引入语言的标准(参见“唯一接口”原因)
  • 有趣的想法,有人参考了父类和接口的类型检查之间的性能差异吗?
  • @info_dev 完成了几次。 4 个实现接口的异常类。 pastebin.com/6RAE8dCW 相同时间(Release + Ctrl-F5 不带调试器运行)。唯一需要注意的是第一次运行会慢一些(顺序并不重要)。一定是因为如果我没记错的话,类型层次结构仅根据请求构建,然后被缓存。
【解决方案4】:

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】:

    在 VB 中可以通过以下方式捕获接口:

    ' 不需要外部函数,但需要类型转换 ' 如果它需要实际使用 Ex 作为 IMyExceptionInterface 当 TypeOf(Ex) 为 IMyExceptionInterface 时将 Ex 作为异常捕获 ' 替代形式:需要预先声明的变量和函数: ' 函数 TryCastIntoSecondParam(TSource 作为类,TDest 作为类)_ '(ByVal 事物作为 TSource,ByRef Dest 作为 TDest) ' 将结果暗淡为 TDest ' 结果 = TryCast(事物,TDest) ' 目标 = 结果 ' 返回目标不是什么 ' 结束函数 TryCastIntoSecondParam(Ex, myIMyException) 时将 Ex 捕获为异常

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

    捕获 Ex 作为 IMyExceptionInterface ' vb 捕获 IExceptionInterface ex ' C#

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

    说了这么多,我怀疑原始问题的答案可能是“设计人员想不出任何好的用例来处理这样的事情”,或者可能是“接口需要比实际更复杂的类型解析类,从而导致仅决定是否捕获异常的行为可能会因自身的异常而失败。”

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

    编辑/附录

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

    顺便说一句,包装器可以提供的另一个 C# 中缺少的功能是能够报告双重错误情况(主线中发生异常,并且在随后的“finally”块中发生另一个异常),而无需捕获最初的异常。当 finally 块中发生异常时,这通常是比主线中发生的异常更大的问题,因此不应被扼杀,但允许“finally 块”异常渗入调用堆栈通常会破坏任何证据的原始异常。虽然与原始异常相比,周围的代码可能对清理失败更感兴趣,但记录这两个异常往往比扼杀原始异常有用得多。

    【讨论】:

    • +1 这是一个非常巧妙的解决方案。很遗憾这在 C# 中不受支持,以前从未见过。关于这个想法的其他人的有用参考:blogs.msdn.com/b/jaredpar/archive/2008/10/09/…
    • @info_dev:见上面的附录。
    • 我认为确定异常严重性的问题不是您可以用您描述的方式概括的问题(假设我的解释正确)。您是否可以从错误中恢复非常依赖于实现。如果由于用户输入了不存在的路径而无法加载文件,您只需通知用户他/她的错误即可轻松地从中恢复。但是,如果您尝试加载不存在的配置文件,则恢复要复杂得多。相同的错误/异常,但严重程度完全不同。
    • @CPX:尝试加载文件的代码会将导致系统处于与尝试之前相同的状态的加载失败视为“干净失败”。尝试加载配置文件并遇到任何故障的代码——即使是“干净的”——应该将“干净的故障”升级为更“严重”的故障。理想情况下,异常机制将允许升级异常的严重性,而无需捕获和重新抛出,但是对于任何标准异常,.net 中都不存在类似的情况,并且使用过滤器块实现它会很笨重。
    【解决方案6】:

    异常不仅仅是让您的代码知道出现问题的一种方式。它包含各种调试信息(堆栈跟踪、目标、内部异常、hresult、Watson 存储桶等),必须在某处收集。最符合逻辑和最简单的解决方案就是让基类收集它。

    【讨论】:

    • 是的,因此拥有接口的默认实现对于重用很有用,但任何实现接口的人都必须实现所有相关功能,因此可以简单地自己重新实现。
    • 将它放在基类中可确保所述调试信息不​​会因开发人员错误而不正确。当然,你可以在你的实现中调用“默认”实现,但你也可以不这样做。如果我们将您的论点更进一步,我们为什么要使用类继承呢?我们可以只实现接口并在适合我们的时候使用基础实现。
    • @CPX:定义一个 ExceptionInfo 对象会非常有用,并要求 IException 提供一个 ExceptionInfo 类型的 Info 属性。
    • @supercat:是的,但这并不能保证 ExceptionInfo 包含任何内容。我认为将异常信息留给实施者自行决定似乎是个坏主意...
    • @CPX:任何未能提供有效 ExceptionInfo 的异常实现都会被破坏。今天,Exception 类完全有可能以完全奇怪和不合理的方式覆盖现有属性,这会破坏大多数“捕获”代码,但这似乎不是一个特别的问题。实际上,正如我在回答中指出的那样,我认为捕获接口比捕获类要弱,但从根本上说,我不喜欢使用类型作为捕获内容的主要决定因素的概念。
    【解决方案7】:

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

    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();
    }
    

    【讨论】:

    • 捕获并重新抛出与不捕获异常不同。重新抛出“ex”将丢失堆栈跟踪中的所有信息;不指定“ex”的投掷会好一点;它将丢失当前方法调用失败例程的行号,但保留堆栈跟踪的其余部分。这仍然意味着任何嵌套的清理代码都将在异常被捕获和重新抛出之前运行,从调试的角度来看,这通常很烦人,并且偶尔会导致生产代码出现问题。
    【解决方案8】:

    原来从框架 2.0 开始就有一个接口:

    System.Runtime.InteropServices._Exception
    

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

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

    【讨论】:

    • 这个接口存在,但如果 System.Exception 没有从它继承,那么任何地方的任何异常类都没有多态性。是这样的。
    • 只有非托管代码才能访问异常数据,而不是从托管代码中调用。
    猜你喜欢
    • 2010-10-29
    • 1970-01-01
    • 2012-08-20
    • 2011-05-31
    • 1970-01-01
    • 2017-08-12
    • 2011-09-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多