Было бы хорошо или плохо использовать исключения с параметрами общего типа - PullRequest
3 голосов
/ 13 июля 2011

В vb.net и, предположительно, в других языках .net можно определять и генерировать классы исключений, которые принимают универсальные параметры класса.Например, можно юридически определить SomeThingBadHappenedException (Of T), а затем бросить и поймать SomethingBadHappened (Of SomeType).Казалось бы, это обеспечивает удобный способ создания семейства исключений без необходимости вручную определять конструкторы для каждого из них.Уточнение типов исключений может показаться полезным для гарантии того, что исключение, которое один перехватывает, на самом деле является ожидаемым исключением, а не исключением, генерируемым дальше по стеку вызовов.Microsoft, возможно, не особенно нравится идея использования тщательно детализированных пользовательских исключений, но так как многие ранее существующие исключения могут возникать из неожиданных мест (например, «FileNotFoundException» при первом вызове функции, которая должна быть загружена из DLL)) бросать и перехватывать пользовательские исключения кажется более безопасным, чем использование существующих.

Самое большое ограничение, которое я вижу с пользовательскими исключениями, заключается в том, что, поскольку параметры типа универсального класса не являются ни ковариантными, ни контравариантными (*), «Catch Ex какSomethingBadHappened (Of SomeBaseType) "не будет перехватывать SomethingBadHappened (Of SomeDerivedType).Можно определить «Catch Ex As SomethingBadHappened (Of T, U)» как производный от SomethingBadHappened (Of U), и, таким образом, выдать «SomethingBadHappened (Of SomeDerivedType, SomeBaseType), но это немного неуклюже, и нужно будет последовательно использовать либонеуклюжая форма или форма, которая пропускает базовый тип (и не может быть поймана как исключение базового типа).

Что люди думают об идее использования исключений общего типа? Есть ли какие-то другие ошибки?чем упомянуто выше?

(*) Если можно поймать производные IException (Out Out T As Exception), а не просто производные Exception, можно определить ковариантные универсальные типы исключений. Возможно, Microsoft сможетдобавьте такую ​​возможность в .net, если IException (Of T) включает свойство «Self» типа T, и попытка перехватить производную U от Exception также перехватит любое IException (Of U), но это, вероятно, будет слишком сложнобыть стоящим.

Приложение

На выходеsting иерархия исключений, если класс 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 в несколько неприятном виде, но возможно иметь Фабрика создает класс исключений, который включает параметр общего типа. Если кому-то интересно, я мог бы обновить пример кода, чтобы включить это.

Иначе, существует ли какой-либо достойный шаблон кодирования для определения пользовательских исключений без необходимости повторять один и тот же код конструктора для каждого другого производного класса? Я действительно хотел бы, чтобы vb.net и / или c # включали синтаксис для определения одного безпараметрического конструктора, специфичного для класса, и автоматически создавали публичные конструкторы для каждой родительской перегрузки.

Приложение 3

При дальнейшем рассмотрении кажется, что во многих случаях действительно необходимо не привязывать выброшенное исключение к классу, а иметь определенную связь между исключением и экземпляром объекта. К сожалению, нет четкого способа определить концепцию «catch SomeExceptionType (ThisParticularFoo)». Лучше всего было бы определить пользовательский базовый класс исключений с предикатом «NoCorruptionOutisde», а затем сказать «Catch Ex As CorruptObjectException When Ex.NoCorruptionOutside (MyObjectInstance)». Как это звучит?

Ответы [ 3 ]

2 голосов
/ 13 июля 2011

Похоже, вы действительно делаете это сложнее, чем нужно.Я бы порекомендовал создать класс исключений следующим образом:

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 голос
/ 13 июля 2011

Что если вам нужно поймать все типы исключений из какой-либо семьи?Вы не могли этого сделать.Итак, суть обобщения заключается в том, чтобы уловить некоторое семантическое сходство между объектами, которые используют разные типы.Семантика семантики, семантика поиска и т. Д. И вы хотите сделать прямо противоположное, потому что вы не можете catch (MyGenericException<T> e) для всех возможных T - вам нужно указать точный тип.Таким образом, я просто не вижу никаких преимуществ в большинстве случаев.

Возможно, единственное правильное использование, как для меня, - FaultException<T> в WCF, где T - это некоторый тип данных, который заранее неизвестен авторам WCF.и может быть что угодно.Но все еще неудобно использовать - на самом деле у нас когда-то была автоматически сгенерированная функция, которая перехватывает эти исключения FaultException и переводит их в те из них, которые были тщательно собраны вручную в значимой иерархии.

Так что я не могу сказать, что этоИдея "большого нет-нет", но я пока не вижу подходящего приложения для этого.

ОБНОВЛЕНИЕ: Что касается перехвата интерфейсов.Это просто невозможно, так как вы можете только поймать что-то, полученное из исключения.Интерфейсы явно не очень.

ОБНОВЛЕНИЕ для Дополнения:

Для меня это не очень хорошая идея.

Прежде всего, это безумно сложно.Ментальная нагрузка для понимания такой смеси вложенных дженериков, наследования и классов ... ну, это слишком много для меня.Наоборот, преимущества весьма скромны.

Во-вторых, вам очень-очень редко нужны такие точные знания обо всем.Точное понимание, какой класс выдал ошибку?Вы получаете это из стека вызовов в худшем случае - на самом деле у вас есть еще больше в стеке вызовов.Точный подтип ошибки?Зачем?Если вы ожидаете какого-то исключения - обычно вы знаете точные детали из окружающего кода.Иначе эти дженерики вам немного не помогут.

В-третьих, такой подход почти громко плачет от плохого-плохого кода, такого как catch (Exception) везде.Потому что другого пути просто нет.

Позвольте привести пример.Если вы используете файловые операции - вы обычно можете предполагать, что все хорошо и просто catch (IOException e) где-то в начале.Однажды.Теперь предположим, что мы используем ваш подход.Предположим также, что у нас есть некоторый статический класс util FileUtil, который реализует что-то полезное.Он выдаст исключения вроде Fault<FileUtil> в соответствии с вашим примером (по крайней мере, если я правильно понял).Таким образом, вызывающий код должен либо: 1) знать, что используется FileUtil, и ожидать соответствующих исключений, либо 2) поймать общий Exception, который известен как «ПЛОХАЯ вещь».Но наша FileUtil - это деталь реализации.Мы хотим иметь возможность изменить его и не затрагивать код вызова!Это просто невозможно.

Это напоминает мне о проверенной функции исключений Java.Любое изменение в предложении throws приведет к изменению каждого фрагмента вызывающего кода.Это не хорошо, правда.

0 голосов
/ 13 июля 2011

Я не уверен, что именно вы хотите достичь с помощью этих общих исключений, но я предполагаю, что вы говорите .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 к BaseClass или DerivedClass.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...