Существуют ли законные причины для возврата объектов-исключений вместо их выброса?

45

Этот вопрос предназначен для применения к любому языку программирования ОО, который поддерживает обработку исключений; Я использую C # только в иллюстративных целях.

Исключения обычно предназначены для возникновения, когда возникает проблема, что код не может быть обработан немедленно, а затем для перехвата в catchпредложении в другом месте (обычно это кадр внешнего стека).

Вопрос: Существуют ли допустимые ситуации, когда исключения не генерируются и перехватываются, а просто возвращаются из метода, а затем передаются как объекты ошибок?

Этот вопрос возник у меня, потому что System.IObserver<T>.OnErrorметод .NET 4 предполагает именно это: исключения передаются как объекты ошибок.

Давайте посмотрим на другой сценарий, проверка. Допустим, я следую общепринятому мнению и поэтому я различаю тип объекта ошибки IValidationErrorи отдельный тип исключения, ValidationExceptionкоторый используется для сообщения о непредвиденных ошибках:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(Пространство System.Component.DataAnnotationsимен делает нечто очень похожее.)

Эти типы могут быть использованы следующим образом:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Теперь мне интересно, не мог бы я упростить вышесказанное до этого:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

В: Каковы преимущества и недостатки этих двух разных подходов?

stakx
источник
Если вы ищете два отдельных ответа, вам следует задать два отдельных вопроса. Сочетание их просто затрудняет ответ.
Бобсон
2
@ Бобсон: я рассматриваю эти два вопроса как взаимодополняющие: первый вопрос должен определять общий контекст проблемы в общих чертах, в то время как последний требует конкретной информации, которая должна позволить читателям сделать свой собственный вывод. Ответ не должен давать отдельных, явных ответов на оба вопроса; ответы «между» так же приветствуются.
stakx
5
Мне неясно, в чем причина: какова причина получения ValidationError из Exception, если вы не собираетесь его бросать?
фунтовые
@pdr: Вы имеете в виду в случае броска AggregateExceptionвместо? Хорошая точка зрения.
stakx
3
На самом деле, нет. Я имею в виду, что если вы собираетесь бросить это, используйте исключение. Если вы собираетесь вернуть его, используйте объект ошибки. Если вы ожидали исключения, которые по своей природе являются ошибками, перехватите их и поместите в объект ошибки. Независимо от того, учитывает ли один из них несколько ошибок, не имеет значения.
фунтовые

Ответы:

31

Существуют ли допустимые ситуации, когда исключения не генерируются и не перехватываются, а просто возвращаются из метода, а затем передаются как объекты ошибок?

Если его никогда не бросают, то это не исключение. Это объект derivedиз класса Exception, хотя он не соответствует поведению. Называть это Исключением - это чисто семантика на данный момент, но я не вижу ничего плохого в том, чтобы не бросать его. С моей точки зрения, не выбрасывать исключение - это то же самое, что и внутренняя функция, которая его перехватывает и предотвращает распространение.

Допустимо ли для функции возвращать объекты исключений?

Абсолютно. Вот краткий список примеров, где это может быть уместно:

  • Фабрика исключений.
  • Объект контекста, который сообщает о наличии предыдущей ошибки в виде готового к использованию исключения.
  • Функция, которая хранит ранее обнаруженное исключение.
  • Сторонний API, который создает исключение внутреннего типа.

Разве это не плохо?

Бросок исключения - это что-то вроде: «Иди в тюрьму. Не сдавайся!» в настольную игру монополия. Он говорит компилятору перепрыгнуть через весь исходный код до улова без выполнения какого-либо из этого исходного кода. Это не имеет ничего общего с ошибками, сообщением или устранением ошибок. Мне нравится думать о создании исключения как оператора «супер возврата» для функции. Он возвращает выполнение кода куда-то намного выше, чем исходный вызывающий объект.

Здесь важно понять, что истинное значение исключений заключается в try/catchшаблоне, а не в экземпляре объекта исключения. Объект исключения - это просто сообщение о состоянии.

В вашем вопросе вы, кажется, путаете использование этих двух вещей: переход к обработчику исключения и состояние ошибки, которое представляет исключение. То, что вы приняли состояние ошибки и поместили его в исключение, не означает, что вы следуете try/catchшаблону или его преимуществам.

Reactgular
источник
4
«Если он никогда не генерируется, то это не исключение. Это объект, производный от Exceptionкласса, но не соответствующий поведению». - И это как раз та проблема: законно ли использовать Exception-приведенный объект в ситуации, когда он вообще не будет брошен ни создателем объекта, ни каким-либо вызывающим методом; где он служит только как «сообщение о состоянии», передаваемое без потока управления «супер возврат»? Или я так сильно нарушаю невысказанный try/ catch(Exception)контракт, что никогда не должен этого делать, а вместо этого использую отдельный, не Exceptionтип?
Stakx
10
Программисты @stakx имеют и будут продолжать делать вещи, которые вводят в заблуждение других программистов, но технически некорректны. Вот почему я сказал, что это семантика. Юридический ответ: Noвы не нарушаете никаких контрактов. Было popular opinionбы то, что вы не должны этого делать. Программирование полно примеров, когда ваш исходный код не делает ничего плохого, но всем это не нравится. Исходный код всегда должен быть написан так, чтобы было ясно, какова цель. Вы действительно помогаете другому программисту, используя исключение таким образом? Если да, то сделай так. В противном случае не удивляйтесь, если это не нравится.
Reactgular
@cgTag Ваше использование встроенных блоков кода для курсива делает мой мозг болит ..
Frayt
51

Возврат исключений вместо их выдачи может иметь семантический смысл, когда у вас есть вспомогательный метод для анализа ситуации и возврата соответствующего исключения, которое затем вызывается вызывающим (вы можете назвать это «фабрикой исключений»). Создание исключения в этой функции анализатора ошибок будет означать, что во время самого анализа что-то пошло не так, а возврат исключения означает, что тип ошибки был успешно проанализирован.

Одним из возможных вариантов использования может быть функция, которая превращает коды ответов HTTP в исключения:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Обратите внимание, что выбрасывание исключения означает, что метод использовался неправильно или имел внутреннюю ошибку, а возврат исключения означает, что код ошибки был успешно идентифицирован.

Philipp
источник
Это также помогает компилятору понять поток кода: если метод никогда не возвращает корректно (потому что он выдает «желаемое» исключение) и компилятор не знает об этом, то иногда вам могут понадобиться лишние returnоператоры, чтобы компилятор прекратил жаловаться.
Иоахим Зауэр
@JoachimSauer Да. Я не раз хотел бы, чтобы они добавили возвращаемый тип «никогда» в C # - это указывало бы на то, что функция должна завершиться с помощью броска, а добавление возврата - ошибка. Иногда у вас есть код, чья единственная работа - это исключение.
Лорен Печтел
2
@LorenPechtel Функция, которая никогда не возвращается, не является функцией. Это терминатор. Вызов такой функции очищает стек вызовов. Это похоже на звонок System.Exit(). Код после вызова функции не выполняется. Это не похоже на хороший шаблон дизайна для функции.
Reactgular
5
@MathewFoscarini Рассмотрим класс, который имеет несколько методов, которые могут ошибаться подобными способами. Вы хотите сбросить некоторую информацию о состоянии как часть исключения, чтобы указать, что она жевала, когда она вышла из бума. Вы хотите одну копию красивого кода из-за СУХОГО. Это оставляет либо вызов функции, которая выполняет выравнивание и использование результата в вашем броске, либо это означает функцию, которая создает предварительно скомпонованный текст, а затем выбрасывает его. Я обычно нахожу последнее лучше.
Лорен Печтел
1
@LorenPechtel ах, да, я тоже это сделал. Хорошо, теперь я понимаю.
Reactgular
5

Концептуально, если объект исключения является ожидаемым результатом операции, тогда да. Однако случаи, о которых я могу вспомнить, в какой-то момент всегда связаны с броском:

  • Вариации шаблона «Try» (когда метод инкапсулирует второй метод, генерирующий исключение, но перехватывает исключение и вместо этого возвращает логическое значение, указывающее на успешность). Вместо того, чтобы возвращать логическое значение, вы можете возвратить любое выброшенное исключение (ноль, если оно выполнено успешно), что позволяет конечному пользователю получить больше информации, сохраняя при этом указатель успеха или сбоя без перехвата.

  • Обработка ошибок рабочего процесса. Практически аналогично инкапсуляции метода «try pattern», вы можете иметь шаг рабочего процесса, абстрагированный в шаблоне цепочки команд. Если ссылка в цепочке выдает исключение, часто проще отловить это исключение в абстрагирующем объекте, а затем вернуть его в результате указанной операции, чтобы механизм рабочего процесса и фактический код, выполняемый как шаг рабочего процесса, не требовали тщательной попытки. -поймать логику самостоятельно.

Keiths
источник
Я не думаю, что кто-либо из известных деятелей отстаивал такую ​​вариацию модели «попробовать», но «рекомендуемый» подход к возвращению логического значения кажется довольно анемичным. Я думаю, что может быть лучше иметь абстрактный OperationResultкласс со boolсвойством «Successful», GetExceptionIfAnyметодом и AssertSuccessfulметодом (который выдает исключение, GetExceptionIfAnyесли оно не равно NULL). Наличие GetExceptionIfAnyметода, а не свойства позволило бы использовать статические неизменяемые объекты ошибок в тех случаях, когда было не так уж много полезного.
суперкат
5

Допустим, вы поставили задачу, которую нужно выполнить в пуле потоков. Если эта задача выдает исключение, это другой поток, поэтому вы не можете его перехватить. Нить, где он был исполнен, просто умрет

Теперь учтите, что что-то (либо код в этой задаче, либо реализация пула потоков) перехватывает это исключение, сохраняет его вместе с задачей и считает задачу выполненной (безуспешно). Теперь вы можете спросить, завершена ли задача (и либо спросить, сгенерировала ли она, либо она может быть снова добавлена ​​в ваш поток (или, лучше, новое исключение с исходным в качестве причины)).

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

PS: это написано с опытом работы с Java, где информация о трассировке стека создается при создании исключения (в отличие от C #, где она создается при генерировании). Таким образом, подход «не выбрасывать исключения» в Java будет медленнее, чем в C # (если только он не создан и не используется повторно), но будет иметь доступную информацию трассировки стека.

В общем, я бы держался подальше от создания исключений и никогда не выбрасывал их (разве что в качестве оптимизации производительности после профилирования указывалось как узкое место). По крайней мере, в Java, где создание исключения стоит дорого (трассировка стека). В C # это возможно, но IMO это удивительно, и поэтому его следует избегать.

user470365
источник
Это часто происходит и с объектами прослушивания ошибок. Очередь выполнит задачу, перехватит любое исключение, а затем передаст это исключение ответственному слушателю ошибок. Обычно в этом случае нет смысла уничтожать поток задач, который мало знает о работе. Эта идея также встроена в API Java, где один поток может получить пропущенные исключения из другого: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Ланс Нанек,
1

Это зависит от вашего дизайна, обычно я бы не возвращал исключения вызывающей стороне, скорее, я бы бросил и поймал их и оставил это на этом. Как правило, код написан так, чтобы быстро и быстро проваливаться. Например, рассмотрим случай открытия файла и его обработки (это C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

В этом случае мы выдали бы ошибку и прекратили обработку файла, как только он обнаружил плохую запись.

Но допустим, что мы хотим продолжить обработку файла, даже если в одной или нескольких строках есть ошибка. Код может начать выглядеть примерно так:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Таким образом, в этом случае мы возвращаем исключение обратно вызывающей стороне, хотя я, вероятно, не вернул бы исключение обратно, а вместо этого использовал какой-либо объект ошибки, поскольку исключение было перехвачено и уже обработано. Это не вредно при возврате исключения, но другие вызывающие могут повторно вызвать исключение, которое может быть нежелательным, так как объект исключения имеет такое поведение. Если вы хотите, чтобы вызывающие абоненты имели возможность перебрасывать, возвращайте исключение, в противном случае извлеките информацию из объекта исключения и создайте меньший облегченный объект и верните его взамен. Быстрый сбой обычно более чистый код.

Для вашего примера проверки я бы, вероятно, не наследовал от класса исключений и не генерировал исключения, потому что ошибки валидации могут быть довольно распространенными. Возвращать объект было бы меньше, чем выдавать исключение, если 50% моих пользователей не смогли правильно заполнить форму с первой попытки.

Джон Рейнор
источник
1

Вопрос: Существуют ли допустимые ситуации, когда исключения не генерируются и перехватываются, а просто возвращаются из метода, а затем передаются как объекты ошибок?

Да. Например, недавно я столкнулся с ситуацией, когда обнаружил, что сгенерированные исключения в доноте веб-службы ASMX включают элемент в получающееся сообщение SOAP, поэтому мне пришлось сгенерировать его.

Иллюстративный код:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
Том Том
источник
1

В большинстве языков (afaik) исключения сопровождаются некоторыми дополнительными преимуществами. В основном, снимок текущей трассировки стека хранится в объекте. Это хорошо для отладки, но может иметь последствия для памяти.

Как уже говорилось @ThinkingMedia, вы действительно используете исключение в качестве объекта ошибки.

В вашем примере кода кажется, что вы в основном делаете это для повторного использования кода и во избежание композиции. Лично я не думаю, что это хорошая причина для этого.

Другая возможная причина - обмануть язык, чтобы получить объект ошибки с трассировкой стека. Это дает дополнительную контекстную информацию к вашему коду обработки ошибок.

С другой стороны, мы можем предположить, что хранение трассировки стека требует затрат памяти. Например, если вы начнете собирать эти объекты где-то, вы можете увидеть неприятное влияние на память. Конечно, это во многом зависит от того, как трассировки стека исключений реализованы на соответствующем языке / движке.

Так это хорошая идея?

До сих пор я не видел, чтобы его использовали или рекомендовали. Но это не много значит.

Вопрос в том, действительно ли трассировка стека полезна для обработки ошибок? Или, в целом, какую информацию вы хотите предоставить разработчику или администратору?

Типичный шаблон throw / try / catch делает необходимым иметь трассировку стека, потому что в противном случае вы не знаете, откуда он берется. Для возвращаемого объекта это всегда происходит из вызываемой функции. Трассировка стека может содержать всю информацию, которая нужна разработчику, но, возможно, подойдет что-то менее тяжелое и более конкретное.

Дон Кихот
источник
-4

Ответ - да. См. Http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions и откройте стрелку для руководства по стилю Google C ++ для исключений. Вы можете видеть там аргументы, против которых они раньше принимали решение, но говорите, что если бы им пришлось сделать это снова, они, вероятно, решили бы.

Однако стоит отметить, что язык Go также не использует идиоматически исключения по аналогичным причинам.

btilly
источник
1
Извините, но я не понимаю, как эти рекомендации помогают ответить на вопрос: они просто рекомендуют полностью избегать обработки исключений. Это не то, о чем я спрашиваю; Мой вопрос заключается в том, является ли законным использование типа исключения для описания состояния ошибки в ситуации, когда результирующий объект исключения никогда не будет выброшен. В этих руководящих принципах, похоже, ничего не говорится об этом.
stakx