使用泛型类型参数的异常是好还是坏

时间:2011-07-12 22:31:03

标签: .net vb.net generics exception

在vb.net中,可能是其他.net语言,可以定义和抛出带有泛型类参数的异常类。例如,可以合法地定义SomeThingBadHappenedException(Of T),然后抛出并捕获SomethingBadHappened(Of SomeType)。这似乎提供了一种制作异常族的便捷方式,而无需为每个异常手动定义构造函数。精炼异常类型似乎有助于确保捕获的异常实际上是预期的异常,而不是在调用堆栈中向下抛出的异常。微软可能不会特别喜欢使用严格详细的自定义异常,但由于许多预先存在的异常可能来自意外的地方(例如“FileNotFoundException”第一次调用一个应该从DLL加载的函数抛出和捕获自定义异常似乎比使用现有异常更安全。

我看到的自定义异常的最大限制是,由于泛型类型参数既不是协变也不是逆变(*),“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”属性,并且尝试捕获Exception的衍生U也会捕获任何IException(Of U),那么微软可能会将这种能力强加给.net。但它可能太复杂而不值得。

附录

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

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

' Define some interfaces which are used to indicate which faults derive from others
Interface IFault(Of T)
End Interface
Interface IFault(Of T, U As IFault(Of T))
End Interface
Interface IFault(Of T, U As IFault(Of T), V As IFault(Of T, U))
End Interface

' Derive the exceptions themselves.  Real code should include all the constructors, of course.
Class CleanFailureException
    Inherits Exception
    Sub New(ByVal Msg As String, ByVal innerException As Exception)
        MyBase.New(Msg, innerException)
    End Sub
End Class
Class CleanFailureException(Of T)
    Inherits CleanFailureException
    Sub New(ByVal Msg As String, ByVal innerException As Exception)
        MyBase.New(Msg, innerException)
    End Sub
End Class
Class CleanFailureException(Of T, FaultType As IFault(Of T))
    Inherits CleanFailureException(Of T)
    Sub New(ByVal Msg As String, ByVal innerException As Exception)
        MyBase.New(Msg, innerException)
    End Sub
End Class
Class CleanFailureException(Of T, FaultType As IFault(Of T), FaultSubType As IFault(Of T, FaultType))
    Inherits CleanFailureException(Of T, FaultType)
    Sub New(ByVal Msg As String, ByVal innerException As Exception)
        MyBase.New(Msg, innerException)
    End Sub
End Class
Class CleanFailureException(Of T, FaultType As IFault(Of T), FaultSubType As IFault(Of T, FaultType), FaultSubSubType As IFault(Of T, FaultType, FaultSubType))
    Inherits CleanFailureException(Of T, FaultType, FaultSubType)
    Sub New(ByVal Msg As String, ByVal innerException As Exception)
        MyBase.New(Msg, innerException)
    End Sub
End Class

' Now a sample class to use such exceptions
Class FileLoader
    Class Faults ' Effectively used as a namespace within a class
        Class FileParsingError
            Implements IFault(Of FileLoader)
        End Class
        Class InvalidDigit
            Implements IFault(Of FileLoader, FileParsingError)
        End Class
        Class GotADollarSignWhenIWantedAZeroOrOne
            Implements IFault(Of FileLoader, FileParsingError, InvalidDigit)
        End Class
        Class GotAPercentSignWhenIWantedASix
            Implements IFault(Of FileLoader, FileParsingError, InvalidDigit)
        End Class
        Class InvalidSeparator
            Implements IFault(Of FileLoader, FileParsingError)
        End Class
        Class SomeOtherError
            Implements IFault(Of FileLoader)
        End Class
    End Class

    ' Now a test routine to throw the above exceptions

    Shared Sub TestThrow(ByVal WhichOne As Integer)
        Select Case WhichOne
            Case 0
                Throw New CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidDigit, Faults.GotADollarSignWhenIWantedAZeroOrOne) _
                  ("Oops", Nothing)
            Case 1
                Throw New CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidDigit, Faults.GotAPercentSignWhenIWantedASix) _
                  ("Oops", Nothing)
            Case 2
                Throw New CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidDigit) _
                  ("Oops", Nothing)
            Case 2
                Throw New CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidSeparator) _
                  ("Oops", Nothing)
            Case 4
                Throw New CleanFailureException(Of FileLoader, Faults.FileParsingError) _
                  ("Oops", Nothing)
            Case 5
                Throw New CleanFailureException(Of FileLoader, Faults.SomeOtherError) _
                  ("Oops", Nothing)
            Case 6
                Throw New CleanFailureException(Of FileLoader) _
                  ("Oops", Nothing)
            Case 7
                Throw New CleanFailureException(Of Integer) _
                  ("Oops", Nothing)
        End Select
    End Sub

    ' A routine to see how each exception type gets caught
    Shared Sub TestFaults()
        For i As Integer = 0 To 7
            Try
                TestThrow(i)
            Catch ex As CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidDigit, Faults.GotADollarSignWhenIWantedAZeroOrOne)
                Debug.Print("Caught {0} as GotADollarSignWhenIWantedAZeroOrOne", ex.GetType)
            Catch ex As CleanFailureException(Of FileLoader, Faults.FileParsingError, Faults.InvalidDigit)
                Debug.Print("Caught {0} as InvalidDigit", ex.GetType)
            Catch ex As CleanFailureException(Of FileLoader, Faults.FileParsingError)
                Debug.Print("Caught {0} as FileParsingError", ex.GetType)
            Catch ex As CleanFailureException(Of FileLoader)
                Debug.Print("Caught {0} as FileLoader", ex.GetType)
            Catch ex As CleanFailureException
                Debug.Print("Caught {0} as CleanFailureException", ex.GetType)
            End Try
        Next
    End Sub
End Class

附录2

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

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

附录3

在进一步的考虑中,似乎在许多情况下真正需要的是不要将抛出的异常绑定到类,而是在异常和对象实例之间定义关系。不幸的是,没有简洁的方法来定义“catch SomeExceptionType(ThisParticularFoo)”的概念。最好的办法可能是定义一个带有“NoCorruptionOutisde”谓词的自定义基本异常类,然后说“当Ex.NoCorruptionOutside(MyObjectInstance)”时,“将Ex Exper当CorruptObjectException”。这听起来怎么样?

3 个答案:

答案 0 :(得分:2)

看起来你真的比实际需要的更难。我建议像这样制作一个异常类:

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的第二个属性并提供另一个构造函数。如果需要在某个FaultTypes的对象上存储一些额外的数据,只需继承CleanFailureException并为该FaultType特定的额外数据添加一个属性。然后,如果在异常处理程序中需要额外的data属性,则可以捕获该子类异常,如果不需要,则捕获CleanFailureException。我从来没有见过用于异常的泛型,我相信即使有充分的理由在那里做某些事情,你所解释的不是它。

答案 1 :(得分:1)

如果您需要捕获某些家庭的所有类型的例外,该怎么办?你不能这样做。因此,泛型的要点是捕获使用不同类型的对象之间的一些语义相似性。集合语义,查找语义等 并且你想做的恰恰相反,因为你不能catch (MyGenericException<T> e)所有可能的T - 你必须指定确切的类型。因此说我在大多数情况下都没有看到任何优势。

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

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

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

附录更新:

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

首先,它非常复杂。理解嵌套泛型,继承和类的混合物的心理负担......嗯,这对我来说太过分了。相反的优点非常谦逊。

第二 - 你很少需要关于一切的准确知识。准确理解哪个类抛出错误?在最坏的情况下,你有来自调用堆栈的东西 - 实际上你在调用堆栈中有更多。错误的确切子类型?为什么?如果你期望某种例外 - 你通常会知道周围代码的确切细节。否则这些仿制药对你没有帮助。

第三种方法是,这种方法几乎都在大声呼喊,如catch (Exception)这样糟糕的代码。因为别无他法。

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

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

答案 2 :(得分:0)

我不确定你想用这些通用异常完成什么,但我想你是说.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