开发者

Would it be good or bad to use exceptions with generic type parameters

In vb.net, and presumably other .net languages, it is possible to define and throw exception classes which take generic class parameters. For example, one could legally define a SomeThingBadHappenedException(Of T) and then throw and catch a SomethingBadHappened(Of SomeType). This would seem to provide a convenient way of making a family of exceptions without having to manually define constructors for each of them. Refining exception types would seem helpful for ensuring that the exception one catches is actually the exception one is expecting, rather than being an exception thrown further down the call stack. Microsoft may not particularly like the idea of using tightly-detailed custom exceptions, but since many pre-existing exceptions can come from unexpected places (e.g. "FileNotFoundException" the first time a call is made to a function t开发者_开发百科hat's supposed to be loaded from a DLL) throwing and catching custom exceptions seems safer than using existing ones.

The biggest limitation I see with custom exceptions is that, since generic-class type parameters are neither covariant nor contravariant(*), a "Catch Ex as SomethingBadHappened(Of SomeBaseType)" will not catch a SomethingBadHappened(Of SomeDerivedType). One could define "Catch Ex As SomethingBadHappened(Of T,U)" as deriving from SomethingBadHappened(Of U), and thus throw a "SomethingBadHappened(Of SomeDerivedType, SomeBaseType) but that's a bit clunky, and one would have to consistently use either the clunky form or the form that omits the base type (and could not be caught as a base-type exception).

What do people think of the idea of using Generic-typed exceptions? Are there any gotchas other than mentioned above?

(*) If one could catch derivatives of IException(Of Out T As Exception), rather than just derivatives of Exception, one could define covariant generic exception types. It might be possible for Microsoft to shoehorn such an ability into .net if IException(Of T) included a "Self" property of type T, and an attempt to catch a derivative U of Exception would also catch any IException(Of U), but it would probably be too complicated to be worthwhile.

Addendum

In the existing exception hierarchy, if class Foo throws e.g. an InvalidOperationException under some particular condition which shouldn't happen but which a caller might have to handle, there's no nice way a caller to catch those exceptions without also catching lots of exceptions resulting from unforeseen conditions, some of which should be caught and others of which shouldn't. Having class Foo define its own exceptions would avoid that danger, but if every "real" class defined even one custom exception class, the custom exception classes could quickly become overwhelming. It would seem much cleaner to have something like CleanFailureException(Of Foo), indicating that the requested operation didn't happen for some reason but the state wasn't disturbed, or CleanFailureException(Of Foo, Foo.Causes.KeyDuplicated), which would inherit from CleanFailureException(Of Foo), indicating the more precise reason for failure.

I haven't figured out any means of using generic exceptions that doesn't end up feeling a little clunky, but that doesn't mean that nobody else could find a better way. Note that derived exception types work as they should; the only real annoyance is having to specify everything up the chain of derivation any time a fault is thrown or caught.

' 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

Addendum 2

At least one advantage of using generic exceptions is that while it's not possible to have a useful exception factory with a generic type parameter defining the exception to be created unless one uses Reflection in somewhat distasteful fashion, but it is possible to have a factory create an exception class which includes a generic type parameter. If anyone's interested, I could update a code example to include that.

Otherwise, is there any decent coding pattern for defining custom exceptions without having to repeat the same constructor code for every different derived class? I really wish vb.net and/or c# would include a syntax to specify a single parameterless class-specific constructor and automatically create public constructors for each parent overload.

Addendum 3

On some further consideration, it seems like what's really needed in many cases is not to have a thrown exception be tied to a class, but rather have a defined relationship between an exception and an object instance. Unfortunately, there's no clean way to define the concept of "catch SomeExceptionType(ThisParticularFoo)". The best bet might be to define a custom base exception class with a "NoCorruptionOutisde" predicate and then say "Catch Ex As CorruptObjectException When Ex.NoCorruptionOutside(MyObjectInstance)". How does that sound?


It looks like you're really making this harder than it needs to be. I'd recommend making an exception class like this:

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

Then, in your exception handling code, do this:

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

If you need an error type/subtype, you can add a second property called SecondaryFaultReason and provide another constructor. If you need to store some extra data on the object for certain FaultTypes, just inherit from CleanFailureException and add a property for that extra data specific to that FaultType. Then, you can either catch that subclass exception if you need the extra data property in your exception handler, or just catch CleanFailureException if you don't. I've never seen generics used for exceptions, and I'm pretty convinced that even if there were a good reason to do that somewhere out there, what you've explained isn't it.


What if you need to catch all types of exceptions from some family? You couldn't do that. So, the point of generics is to catch some semantic similarity between objects that uses different types. Collection semantic, lookup semantic, etc. And you want to do just the opposite because you can't catch (MyGenericException<T> e) for all possible T - you have to specify exact type. Thus said I just don't see any advantages in most cases.

Possible the single correct usage as for me is FaultException<T> in the WCF where T is some data type that is unknown beforehand to WCF writers and could be anything. But it is still inconvenient to use - actually we had once autogenerated function that catches these FaultExceptions and translates them to our ones that were carefully built manually in the meaningful hierarchy.

So, I can't say that this is "big no-no" idea, but I don't see currently good application for it either.

UPDATE: As for catching interfaces. It is just impossible as you can only catch something derived from Exception. Interfaces are clearly not.

UPDATE for Addendum:

It doesn't seem like a good idea for me.

First of all it is insanely complicated. Mental burden for understanding such sort of mixture of nested generics, inheritance and classes... well, it is just too much for me. Advantages on the contrary are quite humble.

Second - you're very-very rarely need such precise knowledge about everything. Exact understanding which class thrown an error? You have that from call stack in the worst case - in fact you have even more in the call stack. Exact subtype of an error? Why? If you expect an exception of some kind - you know exact details from the surrounding code usually. Otherwise these generics wouldn't help you a bit.

The third is that such approach almost cries loudly for bad-bad code like catch (Exception) everywhere. Because there's just no other way.

Let me give you an example. If you're using file operations - you could usually assume everything is good and just catch (IOException e) somewhere up in the beginning. Once. Now suppose we are using your approach. Suppose also we have some util static class FileUtil that implements something useful. It will throw exceptions like Fault<FileUtil> according to your example (if I understood correctly at least). So, calling code should either 1) know that FileUtil is used and expect corresponding exceptions or 2) catch general Exception which is known to be "BAD THING". But our FileUtil is implementation detail. We want to have option to change it and have no calling code affected! It is just impossible.

It reminds me of java's checked exception feature. Any change to throws clause would lead to changing every single piece of calling code. It is not good, really.


I'm not sure exactly what you want to accomplish with these generic exceptions, but I guess you're saying .NET doesn't recognize covariance in catch clauses? If so, a partial solution to this problem would be to define a non-generic base:

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

Now at least you have the option to catch MyException and try to cast Data to a BaseClass or a DerivedClass.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜