Являются ли исключения в качестве контрольного потока серьезным антипаттерном? Если так, то почему?

129

Еще в конце 90-х я довольно много работал с базой кода, которая использовала исключения в качестве управления потоком. В нем реализован конечный автомат для управления приложениями телефонии. В последнее время мне вспоминаются те дни, потому что я занимался веб-приложениями MVC.

У них обоих есть Controllers, которые решают, куда идти дальше, и предоставляют данные логике назначения. Действия пользователя из домена телефона старой школы, такие как тоны DTMF, стали параметрами для методов действия, но вместо того, чтобы возвращать что-то вроде a ViewResult, они бросали a StateTransitionException.

Я думаю, что основным отличием было то, что методы действия были voidфункциями. Я не помню всех вещей, которые я сделал с этим фактом, но я не решался даже пойти по дороге, вспоминая многое, потому что с той работы, как 15 лет назад, я никогда не видел этого в рабочем коде на любой другой работе . Я предположил, что это был знак того, что это был так называемый анти-паттерн.

Так ли это, и если да, то почему?

Обновление: когда я задавал вопрос, я уже имел в виду ответ @ MasonWheeler, поэтому я выбрал ответ, который больше всего увеличил мои знания. Я думаю, что это хороший ответ.

Аарон Анодид
источник
2
Связанный вопрос: programmers.stackexchange.com/questions/107723
Эрик Кинг
25
Нет. В Python использование исключений в качестве потока управления считается «питоническим».
user16764
4
Если бы я занимался такими вещами, как Java, я бы, конечно, не исключил это. Я бы вывел из некоторой иерархии без исключений, без ошибок, Throwable.
Томас Эдинг
2
Чтобы добавить к существующим ответам, вот краткое руководство, которое мне очень помогло: - Никогда не используйте исключения для «счастливого пути». Удачным путем может быть как (для сети) весь запрос, так и просто один объект / метод. Все остальные нормальные правила все еще применяются, конечно :)
Houen
8
Разве исключения не всегда контролируют поток приложения?

Ответы:

109

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

В качестве краткого изложения, почему, как правило, это анти-шаблон:

  • Исключением являются, по сути, сложные заявления GOTO
  • Следовательно, программирование с исключениями приводит к более сложному чтению и пониманию кода.
  • Большинство языков имеют существующие структуры управления, предназначенные для решения ваших проблем без использования исключений.
  • Аргументы в пользу эффективности, как правило, являются спорными для современных компиляторов, которые стремятся оптимизировать с предположением, что исключения не используются для потока управления.

Прочитайте обсуждение в вики Ward для получения более подробной информации.


Смотрите также дубликат этого вопроса, здесь

blueberryfields
источник
18
Ваш ответ звучит так, как будто исключения вредны для всех обстоятельств, в то время как вопрос сосредоточен на исключениях как управлении потоком.
whatsisname
14
@MasonWheeler Разница заключается в том, что циклы for / while содержат четкие изменения управления потоком и упрощают чтение кода. Если в вашем коде вы видите оператор for, вам не нужно пытаться выяснить, какой файл содержит конец цикла. Гото не плохи, потому что какой-то бог сказал, что они были, они плохи просто потому, что им труднее следовать, чем зацикливанию. Исключения похожи, не невозможны, но достаточно сложны, чтобы запутать вещи.
Билл К
24
@BillK, тогда спорьте об этом, и не делайте слишком упрощенных заявлений о том, что исключения являются готовыми.
Уинстон Эверт
6
Хорошо, но серьезно, что происходит с ошибками на стороне сервера и разработчиками приложений с пустыми операторами catch в JavaScript? Это неприятный феномен, который стоил мне много времени, и я не знаю, как спрашивать без разглагольствования. Ошибки твой друг.
Эрик Реппен
12
@mattnz: ifа foreachтакже сложные GOTOs. Честно говоря, я думаю, что сравнение с goto не полезно; это почти как уничижительный термин. Использование GOTO не является по сути злым - оно имеет реальные проблемы, и исключения могут разделять их, но они не могут. Было бы более полезно услышать эти проблемы.
Имон Нербонн
110

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

Дополнительный вариант использования: «Я только что столкнулся с серьезной ошибкой, и сейчас выход из этого потока управления для предотвращения повреждения данных или другого повреждения важнее, чем попытка продолжить».

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

Мейсон Уилер
источник
18
Это не отвечает на вопрос, хотя. Для чего они были разработаны, не имеет значения; единственное, что имеет отношение к делу, - это то, что использование их для управления потоком плохо, и это тема, которую вы не затрагивали. Например, шаблоны C ++ были спроектированы с одной стороны, но они прекрасно подходят для метапрограммирования, чего никогда не ожидали дизайнеры.
Томас Бонини
6
@Krelp: Дизайнеры никогда не ожидали многого, например , случайно попали в систему шаблонов, полную по Тьюрингу! Шаблоны C ++ вряд ли являются хорошим примером для использования здесь.
Мейсон Уилер
15
@Krelp - шаблоны C ++ НЕ «идеально подходят» для метапрограммирования. Они становятся кошмаром, пока вы не понимаете их правильно, а затем они стремятся к коду только для записи, если вы не гений шаблона. Вы можете выбрать лучший пример.
Майкл Коне
Исключения наносят вред составу функций в частности и краткости кода в целом. Например, в то время как я был неопытным, я использовал исключение в проекте, и это вызывало проблемы долгое время, потому что 1) люди должны помнить, чтобы ловить это, 2) вы не можете писать, const myvar = theFunctionпотому что myvar должен быть создан из try-catch, таким образом, оно больше не является постоянным. Это не значит, что я не использую его в C #, так как он по какой-то причине является мейнстримом, но я все равно пытаюсь уменьшить их.
Привет, Ангел,
2
@AndreasBonini Что они предназначены / предназначены для вопросов, потому что это часто приводит к решениям по реализации компилятора / фреймворка / среды выполнения, приведенным в соответствие с дизайном. Например, создание исключения может быть намного «дороже», чем простой возврат, потому что он собирает информацию, предназначенную для того, чтобы помочь кому-то отладить код, такой как следы стека и т. Д.
binki
29

Исключения так же сильны, как и продолжения GOTO. Они являются универсальной конструкцией потока управления.

В некоторых языках они являются единственной универсальной конструкцией потока управления. В JavaScript, например, нет ни продолжения, ни GOTOдаже правильных вызовов. Итак, если вы хотите реализовать сложный поток управления в JavaScript, вы должны использовать исключения.

Проект Microsoft Volta был (в настоящее время прекращен) исследовательским проектом по компиляции произвольного кода .NET в JavaScript. В .NET есть исключения, чья семантика точно не соответствует JavaScript, но, что более важно, в ней есть потоки , и вы должны каким-то образом сопоставить их с JavaScript. Вольта сделал это, внедрив Volta Continuations с использованием исключений JavaScript, а затем реализовав все конструкции потока управления .NET в терминах Volta Continuations. Они должны были использовать исключения в качестве потока управления, потому что нет другой конструкции потока управления, достаточно мощной.

Вы упомянули государственные машины. SM являются тривиальными для реализации с надлежащим вызовом хвоста: каждое состояние - это подпрограмма, каждый переход состояния - это вызов подпрограммы. SM также могут быть легко реализованы с помощью GOTOили сопрограмм или продолжений. Однако, Java не имеет какого - либо из этих четырех, но это действительно есть исключения. Таким образом, вполне приемлемо использовать их в качестве потока управления. (Ну, на самом деле, правильный выбор, вероятно, заключался бы в использовании языка с правильной конструкцией потока управления, но иногда вы можете застрять в Java.)

Йорг Миттаг
источник
7
Если бы только у нас была правильная рекурсия хвостового вызова, возможно, мы могли бы доминировать на веб-разработке на стороне клиента с помощью JavaScript, а затем перейти к распространению на стороне сервера и на мобильные устройства в качестве окончательного пан-платформенного решения, такого как wildfire. Увы, но это было не так. Черт побери наши недостаточные первоклассные функции, замыкания и управляемую событиями парадигму. То, что нам действительно нужно было реальным.
Эрик Реппен
20
@ErikReppen: я понимаю, что ты просто саркастичен, но. , , Я действительно не думаю, что тот факт, что мы «доминировали на стороне клиента при разработке веб-приложений на JavaScript», имеет какое-то отношение к возможностям языка. У него есть монополия на этом рынке, поэтому он смог избежать многих проблем, которые не могут быть списаны с сарказмом.
Руах
1
Хвостовая рекурсия была бы хорошим бонусом (они отказываются от функциональных возможностей, чтобы иметь ее в будущем), но да, я бы сказал, что она выиграла у VB, Flash, Applets и т. Д. По причинам, связанным с функциями. Если бы это не уменьшило сложность и не нормализовало бы так легко, как это было бы, в какой-то момент имело бы реальное соревнование. В настоящее время я использую Node.js для обработки переписывания 21 конфигурационного файла для отвратительного стека C # + Java, и я знаю, что я не единственный, кто делает это. Это очень хорошо в том, что хорошо.
Эрик Реппен
1
Во многих языках нет ни gotos (Goto считается вредным!), Ни сопрограмм, ни продолжений, и они реализуют совершенно корректное управление потоком, просто используя ifs, whiles и вызовы функций (или gosubs!). Большинство из них могут фактически использоваться для подражания друг другу, так же как продолжение может использоваться для представления всего вышеперечисленного. (Например, вы можете использовать время для выполнения if, даже если это вонючий способ кодирования). Таким образом, никаких исключений не требуется для выполнения расширенного управления потоком.
Шейн
2
Ну да, вы можете быть правы, если. «For» в его форме в стиле C, однако, может использоваться как неловко заявленное while, а затем может быть использовано время вместе с if для реализации конечного автомата, который затем может эмулировать все другие формы управления потоком. Опять вонючий способ кодирования, но да. И снова, Гото считается вредным. (И я бы поспорил, также с использованием исключений в не исключительных обстоятельствах). Имейте в виду, что есть совершенно допустимые и очень мощные языки, которые не предоставляют ни goto, ни исключений и работают просто отлично.
Шейн
19

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

Следующий пост в блоге объясняет довольно сложный, но также довольно интересный вариант использования для нелокального ControlFlowException :

Он объясняет, как внутри jOOQ (библиотеки абстракций SQL для Java) (отказ от ответственности: я работаю для поставщика) такие исключения иногда используются для прерывания процесса рендеринга SQL на ранних этапах, когда выполняется какое-то «редкое» условие.

Примеры таких условий:

  • Слишком много значений связывания. Некоторые базы данных не поддерживают произвольное количество значений связывания в своих операторах SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). В этих случаях jOOQ прерывает фазу рендеринга SQL и повторно отображает SQL-оператор со встроенными значениями связывания. Пример:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Если бы мы явно извлекали значения связывания из запроса AST, чтобы подсчитывать их каждый раз, мы бы тратили ценные циклы ЦП на те 99,9% запросов, которые не страдают от этой проблемы.

  • Некоторая логика доступна только косвенно через API, который мы хотим выполнить только «частично». UpdatableRecord.store()Метод генерирует INSERTили UPDATEзаявление, в зависимости от Record«с внутренними флагами. Со стороны мы не знаем, в какой логике содержится store()(например, оптимистическая блокировка, обработка прослушивателя событий и т. Д.), Поэтому мы не хотим повторять эту логику, когда мы храним несколько записей в пакетном операторе, где мы хотели бы store()только генерировать оператор SQL, а не выполнять его на самом деле. Пример:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Если бы мы вывели store()логику в «повторно используемый» API, который можно настроить так, чтобы он не выполнял SQL, мы бы искали создание довольно сложного в обслуживании, едва ли повторно используемого API.

Заключение

По сути, мы используем эти нелокальные gotos только в соответствии с тем, что Мейсон Уилер сказал в своем ответе:

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

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

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

Лукас Эдер
источник
11

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

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

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

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

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

Но другие языки, например Ruby, имеют синтаксис, напоминающий исключения, для потока управления. Исключительные ситуации обрабатываются операторами raise/ rescue. Но вы можете использовать throw/ catchдля исключительных конструкций управления потоком **.

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

* Пример высокопроизводительного использования исключений: однажды я был настроен на оптимизацию плохо работающего приложения веб-формы ASP.NET. Оказалось, что рендеринг большой таблицы вызывал int.Parse()ок. тысяча пустых строк на средней странице, в результате ок. обрабатывается тысяча исключений. Заменив код на int.TryParse()Я сбрил одну секунду! За каждый запрос страницы!

** Это может быть очень запутанным для программиста подходит к Руби из других языков, как throwи catchключевые слова , связанные с исключениями во многих других языках.

Пит
источник
1
+1 за «Используя исключения для чего-то, что не является исключительным, вы используете неподходящие абстракции для проблемы, которую вы пытаетесь решить».
Джорджио
8

Вполне возможно обработать условия ошибки без использования исключений. Некоторые языки, особенно C, даже не имеют исключений, и людям все еще удается создавать довольно сложные приложения с его помощью. Причина, по которой исключения полезны, заключается в том, что они позволяют вам кратко указать два по существу независимых потока управления в одном и том же коде: один, если возникает ошибка, и один, если нет. Без них вы получите код повсюду, который выглядит следующим образом:

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

Или эквивалентный на вашем языке, например, возвращение кортежа с одним значением в качестве состояния ошибки и т. Д. Часто люди, которые указывают, какова «дорогая» обработка исключений, пренебрегают всеми дополнительными ifутверждениями, подобными приведенным выше, которые вам необходимо добавить, если вы не использовать исключения.

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

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

Карл Билефельдт
источник
2
Пока эти операторы if терпят неудачу только в «исключительных» обстоятельствах, логика предсказания ветвлений современных процессоров делает их стоимость незначительной. Это одно из мест, где макросы действительно могут помочь, если вы будете осторожны и не будете пытаться делать с ними слишком много.
Джеймс
5

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

Из-за отсутствия многоуровневых разрывов или оператора goto в Python я иногда использовал исключения:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error
gahooa
источник
6
Если вам нужно завершить вычисление где-то в глубоко вложенном операторе и перейти к какому-то общему коду продолжения, вполне вероятно, что этот конкретный путь выполнения может быть разложен как функция returnвместо goto.
9000
3
@ 9000 Я собирался прокомментировать одно и то же ... Держите try:блоки в 1 или 2 строки, пожалуйста, никогда try: # Do lots of stuff.
Вим
1
@ 9000, в некоторых случаях, конечно. Но затем вы теряете доступ к локальным переменным и перемещаете свой код в другое место, когда процесс представляет собой последовательный, линейный процесс.
gahooa
4
@gahooa: Раньше я думал, как ты. Это был признак плохой структуры моего кода. Когда я больше думал об этом, я заметил, что локальные контексты могут быть распутаны, и весь беспорядок превратился в короткие функции с несколькими параметрами, несколькими строками кода и очень точным значением. Я никогда не оглядывался назад.
9000
4

Давайте нарисуем такое исключение:

Алгоритм ищет рекурсивно, пока что-то не найдено. Поэтому, возвращаясь из рекурсии, нужно проверить результат на предмет обнаружения и затем вернуться, в противном случае продолжить. И это неоднократно возвращалось с некоторой глубины рекурсии.

Помимо необходимости в дополнительном логическом значении found(для упаковки в классе, где в противном случае, возможно, было бы возвращено только int), и для глубины рекурсии происходит то же самое.

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

Joop Eggen
источник
Хм, это голосование за действительно противоположную позицию или за недостаточную или короткую аргументацию? Мне действительно любопытно.
Joop Eggen
Я сталкиваюсь именно с этим затруднительным положением, я надеялся, что вы понимаете, что вы думаете о моем следующем вопросе? Рекурсивный использованием исключений аккумулировать причину (сообщение) для верхнего уровня исключения codereview.stackexchange.com/questions/107862/...
CL22
@Jodes Я только что прочитал вашу интересную технику, но я пока занят. Надеюсь, что кто-то еще прояснит этот вопрос.
Joop Eggen
4

Программирование - это работа

Я думаю, что самый простой способ ответить на это, это понять прогресс, достигнутый ООП за эти годы. Все, что делается в ООП (и в большинстве случаев в парадигмах программирования), смоделировано вокруг необходимости работы .

Каждый раз, когда вызывается метод, вызывающий абонент говорит: «Я не знаю, как выполнить эту работу, но вы знаете, как, поэтому вы делаете это для меня».

Это представляло сложность: что происходит, когда вызываемый метод обычно знает, как выполнять работу, но не всегда? Нам нужен был способ общения: «Я хотел помочь тебе, я действительно помог, но я просто не могу этого сделать».

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

Исключительная аналогия

Допустим, у вас есть плотник, сантехник и электрик. Вы хотите, чтобы сантехник починил вашу раковину, поэтому он на это смотрит. Это не очень полезно, если он говорит только вам: «Извините, я не могу это исправить. Он сломан». Черт, еще хуже, если бы он посмотрел, ушел и отправил вам письмо, в котором говорилось, что он не может это исправить. Теперь вам нужно проверить свою почту, прежде чем вы даже узнаете, что он не сделал то, что вы хотели.

Что бы вы предпочли, так это попросить его сказать: «Послушайте, я не смог это исправить, потому что, похоже, ваш насос не работает»

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

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

Это то, что являются исключениями моделирования: сложные режимы отказов в развязанном виде. Сантехнику не нужно знать об электрике - ему даже не нужно знать, что кто-то в цепи может решить проблему. Он просто сообщает о проблеме, с которой столкнулся.

Так что ... анти-паттерн?

Итак, понимание точки исключений является первым шагом. Следующее - понять, что такое анти-паттерн.

Чтобы считаться анти-паттерном, необходимо

  • решать проблему
  • иметь категорически негативные последствия

Первый пункт легко соблюдается - система работала, верно?

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

Но это не окончательный вред. Это плохой способ сделать что-то, и странный, но анти-паттерн? Нет ... Просто ... странно.

MirroredFate
источник
Есть одно плохое последствие, по крайней мере, в языках, обеспечивающих полную трассировку стека. Поскольку код сильно оптимизирован с большим количеством вставок, реальная трассировка стека и та, которую разработчик хочет видеть, сильно отличаются, и, следовательно, генерация трассировки стека стоит дорого. Чрезмерное использование исключений очень плохо сказывается на производительности в таких языках (Java, C #).
Maaartinus
«Это плохой способ делать вещи» - разве этого не достаточно, чтобы классифицировать его как анти-паттерн?
Maybe_Factor
@Maybe_Factor В соответствии с определением муравейника, нет.
MirroredFate