【问题标题】:Would it be good or bad to use exceptions with generic type parameters对泛型类型参数使用异常是好是坏
【发布时间】:2011-10-04 01:26:27
【问题描述】:

在 vb.net 和可能的其他 .net 语言中,可以定义和抛出采用通用类参数的异常类。例如,可以合法地定义 SomeThingBadHappenedException(Of T),然后抛出并捕获 SomethingBadHappened(Of SomeType)。这似乎提供了一种方便的方法来生成一系列异常,而无需为每个异常手动定义构造函数。优化异常类型似乎有助于确保捕获的异常实际上是预期的异常,而不是被进一步抛出调用堆栈的异常。 Microsoft 可能并不特别喜欢使用详细的自定义异常的想法,但由于许多预先存在的异常可能来自意想不到的地方(例如,第一次调用应该从 DLL 加载的函数时出现“FileNotFoundException” ) 抛出和捕获自定义异常似乎比使用现有异常更安全。

我看到的自定义异常的最大限制是,由于泛型类类型参数既不是协变也不是逆变(*),“Catch Ex as SomethingBadHappened(Of SomeBaseType)”不会捕捉到 SomethingBadHappened(Of SomeDerivedType)。可以将“Catch Ex As SomethingBadHappened(Of T,U)”定义为从 SomethingBadHappened(Of U) 派生,从而抛出“SomethingBadHappened(Of SomeDerivedType, SomeBaseType) 但这有点笨拙,并且必须始终使用其中一个笨拙的形式或省略基本类型的形式(并且不能作为基本类型异常捕获)。

人们如何看待使用通用类型异常的想法?除了上面提到的还有什么陷阱吗?

(*) 如果可以捕获 IException(Of Out T As Exception) 的派生类,而不仅仅是 Exception 的派生类,则可以定义协变的泛型异常类型。如果 IException(Of T) 包含 T 类型的“Self”属性,Microsoft 可能会将这种能力强加到 .net 中,并且尝试捕获 Exception 的派生 U 也会捕获任何 IException(Of U),但这可能太复杂了,不值得。

附录

在现有的异常层次结构中,如果类 Foo 抛出例如InvalidOperationException 在某些不应该发生但调用者可能必须处理的特定条件下,调用者没有很好的方法来捕获这些异常而不捕获由不可预见的条件导致的大量异常,其中一些应该被捕获,而其他的这不应该。让类 Foo 定义它自己的异常可以避免这种危险,但是如果每个“真正的”类都定义了一个自定义异常类,那么自定义异常类可能很快就会变得不堪重负。有类似 CleanFailureException(Of Foo) 的东西似乎更干净,表明请求的操作由于某种原因没有发生但状态没有受到干扰,或者 CleanFailureException(Of Foo, Foo.Causes.KeyDuplicated),这将继承自 CleanFailureException(Of Foo),表示更准确的失败原因。

我还没有想出任何使用通用异常的方法,它最终不会让人觉得有点笨拙,但这并不意味着没有其他人能找到更好的方法。请注意,派生的异常类型可以正常工作;唯一真正的烦恼是必须在任何时候抛出或捕获错误时指定派生链上的所有内容。

' 定义一些接口,用于指示哪些故障源自其他故障 接口 IF(Of T) 端接口 接口 IFault(Of T, U As IFault(Of T)) 端接口 接口 IFault(Of T, U As IFault(Of T), V As IFault(Of T, U)) 端接口 ' 自己推导出异常。当然,真正的代码应该包括所有的构造函数。 类 CleanFailureException 继承异常 Sub New(ByVal Msg As String, ByVal innerException As Exception) MyBase.New(消息,innerException) 结束子 结束类 类 CleanFailureException(Of T) 继承 CleanFailureException Sub New(ByVal Msg As String, ByVal innerException As Exception) MyBase.New(消息,innerException) 结束子 结束类 类 CleanFailureException(Of T, FaultType As IFault(Of T)) 继承 CleanFailureException(Of T) Sub New(ByVal Msg As String, ByVal innerException As Exception) MyBase.New(消息,innerException) 结束子 结束类 类 CleanFailureException(Of T, FaultType As IFault(Of T), FaultSubType As IFault(Of T, FaultType)) 继承 CleanFailureException(Of T, FaultType) Sub New(ByVal Msg As String, ByVal innerException As Exception) MyBase.New(消息,innerException) 结束子 结束类 类 CleanFailureException(Of T, FaultType As IFault(Of T), FaultSubType As IFault(Of T, FaultType), FaultSubSubType As IFault(Of T, FaultType, FaultSubType)) 继承 CleanFailureException(Of T, FaultType, FaultSubType) Sub New(ByVal Msg As String, ByVal innerException As Exception) MyBase.New(消息,innerException) 结束子 结束类 ' 现在是使用此类异常的示例类 类文件加载器 Class Faults ' 有效地用作类中的命名空间 类 FileParsingError 实现 IFault(Of FileLoader) 结束类 类 InvalidDigit 实现 IFault(Of FileLoader, FileParsingError) 结束类 类 GotAdollarSignWhenIWantedAZero 或 One 实现 IFault(Of FileLoader, FileParsingError, InvalidDigit) 结束类 类 GotAPercentSignWhenIWantedASix 实现 IFault(Of FileLoader, FileParsingError, InvalidDigit) 结束类 类 InvalidSeparator 实现 IFault(Of FileLoader, FileParsingError) 结束类 类 SomeOtherError 实现 IFault(Of FileLoader) 结束类 结束类 ' 现在是一个抛出上述异常的测试例程 Shared Sub TestThrow(ByVal WhichOne As Integer) 选择案例whichOne 案例 0 抛出新的 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidDigit,Faults.GotADollarSignWhenIWantedAZeroOrOne)_ (“哎呀”,什么都没有) 情况1 抛出新的 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidDigit,Faults.GotAPercentSignWhenIWantedASix)_ (“哎呀”,什么都没有) 案例2 抛出新的 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidDigit)_ (“哎呀”,什么都没有) 案例2 抛出新的 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidSeparator)_ (“哎呀”,什么都没有) 案例4 抛出新的 CleanFailureException(Of FileLoader, Faults.FileParsingError) _ (“哎呀”,什么都没有) 案例5 抛出新的 CleanFailureException(Of FileLoader, Faults.SomeOtherError) _ (“哎呀”,什么都没有) 案例6 抛出新的 CleanFailureException(Of FileLoader) _ (“哎呀”,什么都没有) 案例7 抛出新的 CleanFailureException(Of Integer) _ (“哎呀”,什么都没有) 结束选择 结束子 ' 查看每个异常类型如何被捕获的例程 共享子 TestFaults() 对于 i 作为整数 = 0 到 7 尝试 测试投掷(一) 捕获 ex 作为 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidDigit,Faults.GotADollarSignWhenIWantedAZeroOrOne) Debug.Print("Caught {0} as GotADollarSignWhenIWantedAZeroOrOne", ex.GetType) 捕获 ex 作为 CleanFailureException(FileLoader,Faults.FileParsingError,Faults.InvalidDigit) Debug.Print("Caught {0} as InvalidDigit", ex.GetType) 将 ex 捕获为 CleanFailureException(FileLoader,Faults.FileParsingError) Debug.Print("Caught {0} as FileParsingError", ex.GetType) 将 ex 捕获为 CleanFailureException(Of FileLoader) Debug.Print("Caught {0} as FileLoader", ex.GetType) 将 ex 作为 CleanFailureException 捕获 Debug.Print("Caught {0} as CleanFailureException", ex.GetType) 结束尝试 下一个 结束子 结束类

附录 2

使用泛型异常的至少一个优点是,虽然不可能有一个有用的异常工厂,它有一个泛型类型参数来定义要创建的异常,除非以某种令人反感的方式使用反射,但可以有一个工厂创建一个包含泛型类型参数的异常类。如果有人感兴趣,我可以更新一个代码示例来包含它。

否则,是否有任何体面的编码模式来定义自定义异常,而不必为每个不同的派生类重复相同的构造函数代码?我真的希望 vb.net 和/或 c# 包含一种语法来指定单个无参数的特定于类的构造函数,并为每个父重载自动创建公共构造函数。

附录 3

进一步考虑,在许多情况下,似乎真正需要的不是将抛出的异常绑定到类,而是在异常和对象实例之间建立已定义的关系。不幸的是,没有明确的方法来定义“catch SomeExceptionType(ThisParticularFoo)”的概念。最好的选择可能是使用“NoCorruptionOutisde”谓词定义自定义基异常类,然后说“当 Ex.NoCorruptionOutside(MyObjectInstance) 时捕获 Ex As CorruptObjectException”。听起来怎么样?

【问题讨论】:

  • 这是什么意思?一个代码示例会很快把它弄清楚。
  • @Chris Shouts:编辑示例。

标签: .net vb.net generics exception


【解决方案1】:

如果您需要捕获某个家族的所有类型的异常怎么办?你不能那样做。因此,泛型的重点是捕捉使用不同类型的对象之间的一些语义相似性。集合语义、查找语义等。 而你想做相反的事情,因为你不能catch (MyGenericException<T> e) 为所有可能的T - 你必须指定确切的类型。如此说,在大多数情况下,我只是看不到任何优势。

对我来说,唯一正确的用法可能是 WCF 中的 FaultException<T>,其中 T 是 WCF 编写者事先不知道的某种数据类型,可以是任何东西。但是使用起来仍然很不方便——实际上我们曾经有一个自动生成的函数来捕获这些 FaultExceptions 并将它们转换为我们在有意义的层次结构中精心手动构建的函数。

所以,我不能说这是一个“大禁忌”的想法,但我目前也没有看到它的好应用。

更新:关于捕获接口。这是不可能的,因为您只能捕获从 Exception 派生的东西。接口显然不是。

附录更新:

这对我来说似乎不是一个好主意。

首先,它非常复杂。理解嵌套泛型、继承和类的这种混合的精神负担……嗯,这对我来说太多了。相反的优势是相当微不足道的。

第二——你很少需要对每件事都如此精确的了解。准确理解哪个类引发了错误?在最坏的情况下,您可以从调用堆栈中获得它 - 实际上您在调用堆栈中还有更多。错误的确切子类型?为什么?如果您期望某种异常 - 您通常会从周围的代码中知道确切的细节。否则这些泛型不会帮助你一点。

第三个是这种方法几乎到处都在大声呼喊像catch (Exception)这样的糟糕代码。因为没有别的办法。

让我举个例子。如果您正在使用文件操作 - 您通常可以假设一切都很好,只是 catch (IOException e) 在开始的某个地方。一次。现在假设我们正在使用您的方法。假设我们还有一些实用的静态类FileUtil,它实现了一些有用的东西。根据您的示例(如果我至少理解正确的话),它将引发Fault<FileUtil> 之类的异常。因此,调用代码应该 1) 知道使用了 FileUtil 并期待相应的异常,或者 2) 捕获一般的 Exception,这被称为“坏事”。但是我们的FileUtil 是实现细节。我们希望可以选择更改它并且不影响调用代码!这是不可能的。

这让我想起了 java 的检查异常特性。对throws 子句的任何更改都将导致更改每一段调用代码。确实不好。

【讨论】:

  • 查看已编辑的问题。希望你能阅读 vb.net;如果不是,我可能可以解释 C# 中的东西是什么意思。
  • 我添加了附录 2,它可能具有优势(通用异常工厂)。如果异常与正在使用的显式类型一起使用,它确实就像 Java 的检查异常,并且需要应用程序层捕获异常并将它们作为 CleanFailure 或不同程度的 CorruptFailure 转换为上层。可以说,如果中间层永远不会让异常逃逸而不进行转换,则不需要将异常转换为自定义类型,但像 InvalidOperationException 这样的东西似乎有点脏。
  • 如果某些可能引发异常的代码除了 VB 之外永远不会被使用,那么可以简单地使用单个通用异常,其中包括决定是否应该捕获它以及是否应该考虑它的方法使满意。如果异常过滤器符合 CLS,那么这种方法可能比尝试使用继承来解决真正的双重调度问题要干净得多。事实上,一个设计合理的通用异常对象与过滤器一起使用可以处理一些普通继承不能处理的情况,包括从 finally 块中抛出的异常。
  • 我希望看到的异常处理视图是让“catch”块指示他们“感兴趣”的事物类型以及他们已经解决的事物。然后,异常可以指示任何处理程序是否对它感兴趣,以及执行的任何解决方案是否足够。有时会同时存在多个问题条件;当存在任何感兴趣的条件时,异常处理程序应该捕获,但异常应该传播到所有条件都已处理完毕。
  • InvalidOperationException 是您在正常运行中永远不应该捕获的异常。我的意思是,如果使用得当,它总是表明代码中存在错误,某种调用合同违规。 NullReferenceException 等也一样。相反,您应该始终捕获和处理一些异常 - 例如IOException。而且通常你知道它们可以被扔到哪里,所以你不需要任何额外的机制。
【解决方案2】:

我不确定你想用这些通用异常来完成什么,但我猜你是说 .NET 不能识别 catch 子句中的协变?如果是这样,这个问题的部分解决方案是定义一个非泛型基:

public class MyException : Exception
{
    protected MyException() {}
    public abstract object Data { get; }
}
public class MyException<T> : MyException
{
    private T _data;
    public MyException(T data) { _data = data; }
    // Oops, .NET doesn't allow return type covariance, so... define 2 properties?
    public override object Data { get { return _data; } }
    public               T DataT { get { return _data; } }
}

现在至少您可以选择捕捉MyException 并尝试将Data 转换为BaseClassDerivedClass

【讨论】:

  • 是的,有可能。问题是为什么首先在这里有泛型?如果您有两个且只有两个不同的案例(您可以将 Data 转换为) - 为什么不拥有两个不同的非泛型类和两个 catch 部分?
【解决方案3】:

看起来你真的让这件事变得比需要的更难。我建议创建一个这样的异常类:

Public Class CleanFailureException
    Inherits Exception

    Public Enum FaultType
        Unknown
        FileParsingError
        InvalidDigit
        WhateverElse
    End Enum

    Public Property FaultReason As FaultType

    Public Sub New(msg As String, faultReason As FaultType, innerException As Exception)
        MyBase.New(msg, innerException)
        Me.FaultReason = faultReason
    End Sub
End Class

然后,在您的异常处理代码中,执行以下操作:

Try
   SomeAction()
Catch cfex As CleanFailureException
   Select Case cfex.FaultReason
       Case CleanFailureException.FaultType.FileParsingError
           ' Handle error
       Case Else
           Throw ' don't throw cfex so you preserve stack trace
   End Select
Catch ex As AnyOtherException
    ' Handle this somehow
End Try

如果您需要错误类型/子类型,您可以添加第二个名为SecondaryFaultReason 的属性并提供另一个构造函数。如果您需要在对象上为某些 FaultType 存储一些额外数据,只需从 CleanFailureException 继承并为特定于该 FaultType 的额外数据添加一个属性。然后,如果您需要异常处理程序中的额外数据属性,则可以捕获该子类异常,或者如果不需要,则只捕获 CleanFailureException。我从未见过用于异常的泛型,而且我非常相信即使有充分的理由在某处这样做,您所解释的也不是。

【讨论】:

  • 如上面附录 2 中所述,我想到了使用泛型异常类型的至少一个优势(泛型工厂可以创建它)。对于 vb.net,更好的方法可能是简单地拥有一个包罗万象的异常类型,其中包括决定是否应该捕获它的方法。与使用继承来决定是否捕获异常相比,这种方法要干净得多,但它完全不符合 CLS。
猜你喜欢
  • 2020-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-06
  • 1970-01-01
  • 1970-01-01
  • 2023-01-19
  • 1970-01-01
相关资源
最近更新 更多