Какие и почему вы предпочитаете исключения или коды возврата?

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

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

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


person Robert Gould    schedule 19.09.2008    source источник
comment
Проблема курицы или яйца из мира программного обеспечения ... вечно спорная. :)   -  person Gishu    schedule 19.09.2008
comment
Извините за это, но, надеюсь, наличие различных мнений поможет людям (в том числе и мне) сделать правильный выбор.   -  person Robert Gould    schedule 19.09.2008


Ответы (24)


Для некоторых языков (например, C ++) утечка ресурсов не должна быть причиной

C ++ основан на RAII.

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

Коды возврата более подробны

Они многословны и имеют тенденцию развиваться во что-то вроде:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

В конце концов, ваш код представляет собой набор идентифицированных инструкций (я видел такой код в производственном коде).

Этот код можно было бы перевести на:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Которые четко разделяют код и обработку ошибок, что может быть хорошо.

Коды возврата более хрупкие

Если не какое-то неясное предупреждение от одного компилятора (см. Комментарий phjr), их можно легко проигнорировать.

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

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

Коды возврата иногда необходимо переводить

Допустим, у нас есть следующие функции:

  • doSomething, который может возвращать int с именем NOT_FOUND_ERROR
  • doSomethingElse, который может возвращать логическое значение false (для ошибки)
  • doSomethingElseAgain, который может возвращать объект Error (как с __LINE__, __FILE__, так и с половиной переменных стека.
  • doTryToDoSomethingWithAllThisMess, который, ну ... Используйте вышеуказанные функции и возвращайте код ошибки типа ...

Каков тип возврата doTryToDoSomethingWithAllThisMess, если одна из его вызываемых функций не работает?

Коды возврата - не универсальное решение

Операторы не могут вернуть код ошибки. Конструкторы C ++ тоже не могут.

Коды возврата означают, что вы не можете связывать выражения

Следствие из вышеизложенного. Что, если я хочу написать:

CMyType o = add(a, multiply(b, c)) ;

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

Исключение набирается

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

Затем каждый улов может быть специализирован.

Никогда не используйте catch (...) без повторного броска

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

Исключение составляют ... NUKE

Проблема с исключением заключается в том, что их чрезмерное использование приведет к созданию кода, полного попыток / уловок. Но проблема в другом: кто пытается / поймать свой код с помощью контейнера STL? Тем не менее, эти контейнеры могут отправлять исключение.

Конечно, в C ++ никогда не позволяйте исключению выйти из деструктора.

Исключение составляют ... синхронные

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

Решением может быть их смешивание?

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

Итак, единственный вопрос: чего не должно происходить?

Это зависит от контракта вашей функции. Если функция принимает указатель, но указывает, что указатель должен быть отличным от NULL, тогда нормально генерировать исключение, когда пользователь отправляет указатель NULL (вопрос в том, в C ++, когда автор функции не использовал ссылки вместо этого указателей, но ...)

Другое решение - показать ошибку

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

В своей работе мы используем что-то вроде Assert. Это будет, в зависимости от значений файла конфигурации, независимо от параметров компиляции отладки / выпуска:

  • записать ошибку
  • откройте почтовый ящик, нажав "Привет, у вас проблема"
  • откройте окно сообщения с помощью Эй, у вас проблема, вы хотите отладить

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

Его легко добавить в унаследованный код. Например:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

ведет своего рода код, похожий на:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(У меня есть похожие макросы, которые активны только при отладке).

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

Вывод

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

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

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

person paercebal    schedule 21.09.2008
comment
Да, НО, что касается вашего первого примера, это можно легко записать как: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() } - person bobobobo; 30.01.2012
comment
Почему бы и нет ... Но я все же считаю doSomething(); doSomethingElse(); ... лучше, потому что если мне нужно добавить if / while / etc. операторы для нормального выполнения, я не хочу, чтобы они смешивались с if / while / etc. операторы, добавленные для исключительных целей ... И поскольку реальное правило использования исключений - бросать, а не ловить, операторы try / catch обычно не являются инвазивными. - person paercebal; 30.01.2012
comment
Ваш первый пункт показывает, в чем проблема с исключениями. Ваш поток управления становится странным и отделен от реальной проблемы. Он заменяет некоторые уровни идентификации каскадом уловок. Я бы использовал оба кода возврата (или возвращаемые объекты с тяжелой информацией) для возможных ошибок и исключений для вещей, которые не ожидаются. - person Peter; 03.01.2013
comment
@ Питер Вебер: Это не странно. Он отделен от реальной проблемы, потому что не является частью нормального потока выполнения. Это исключительное исполнение. И опять же, суть исключения состоит в том, чтобы в случае исключительной ошибки генерировать часто и ловить редко, если вообще. Таким образом, блоки catch даже редко появляются в коде. - person paercebal; 03.01.2013
comment
Примеры такого рода дебатов очень упрощены. Обычно doSomething () или doSomethingElse () действительно что-то выполняют, например, изменяют состояние некоторого объекта. Код исключения не гарантирует возврата объекта в предыдущее состояние и даже меньше, когда ловушка очень далеко от выброса ... В качестве примера представьте, что doSomething вызывается дважды и увеличивает счетчик перед выбросом. Как при перехвате исключения узнать, что нужно уменьшить один или два раза? В общем, написать безопасный код для всего, что не является игрушечным примером, очень сложно (невозможно?). - person xryl669; 27.09.2016
comment
@ xryl669: `В общем, написать безопасный для исключений код для всего, что не является игрушечным примером, очень сложно (невозможно?)` Написание безопасного для исключений кода требует практики и знания конкретных шаблонов, но это вряд ли сложно. Я написал два объекта STL-подобных деревьев, поддерживающих изменения порядка во время выполнения, и оба были полностью безопасными в отношении исключений. Вопрос в том, какую гарантию вы хотите предоставить. Ваш встречный пример показывает, что вам нужна сильная гарантия, что почти легко, но обычно дорого (по сути, вы копируете данные перед фиксацией). Предоставить базовую гарантию так же просто, как RAII. - person paercebal; 28.09.2016
comment
@ xryl669: ... Дело в том, что если ваш код зависит от кода, который не является безопасным в отношении исключений, то гораздо сложнее сделать его безопасным в отношении исключений. Само написание не так уж и сложно. Это просто изменение мышления в том, как вы воспринимаете код. После того, как я познакомился с методами, всякий раз, когда я смотрел на код, который я написал до этого, мне было трудно игнорировать инструкции по исключению небезопасных ситуаций и желание исправить их. Прочтите мой ответ по адресу: stackoverflow.com/questions/1853243/ - person paercebal; 28.09.2016
comment
@paercebal: Вы совершенно правы. Я хочу сказать, что дебаты по поводу использования исключения или кода ошибки возврата носят упрощенный характер, потому что настоящая дискуссия заключается в том, как вы справляетесь с тем, чтобы ваша программа работала должным образом (либо как вы показали с исключением = ›модель на основе транзакции), либо просто с отменить при первой ошибке. Написание кода, безопасного для исключений, если это возможно, является лучшим решением. НО весь приведенный ниже код должен иметь такую ​​же строгость, иначе он вас побьет. Найти хороший безопасный код исключений бывает редко, что, как правило, способствует тому, чтобы вообще не использовать исключения, поскольку в этом случае вы не можете гарантировать свой код. - person xryl669; 30.09.2016
comment
@ xryl669: Если приведенный ниже код не может гарантировать даже базовую безопасность исключений (RAII, более или менее), тогда проблема не в предпочтении исключений или кода возврата: это сам код C ++, который является хрупким и ненадежным. В любой момент может произойти утечка всего: любое использование break, return или даже if может вызвать утечку. Любое обновление, сделанное другим разработчиком, может что-то сломать. Поэтому вашим приоритетом будет как можно больше и как можно скорее применить RAII (с использованием стека, интеллектуальных указателей и т. Д.). - person paercebal; 02.10.2016
comment
Он отделен от реальной проблемы, потому что не является частью нормального потока выполнения. Это исключительное исполнение. Большинство людей продолжают упоминать об этом, но я думаю, что большинство также не согласны с определением исключительного. Например, я мог бы считать deactivatedUser.deactivate() исключительным и выдать DomainException, когда другие могут сказать, что это ожидаемый бизнес-сценарий, и поэтому исключение не следует использовать. - person plalx; 27.03.2019
comment
Честно говоря, я не вижу преимуществ для возвращаемых значений ошибок, если только цель не состоит в том, чтобы собрать очень подробную информацию обо всем, что пошло не так, что чаще всего делается на пользовательском интерфейсе и / или пограничном уровне службы. Например, возможность сообщать о состоянии действительности всей структуры данных, но в этом случае единственная роль вызванной операции будет сообщать об ошибках. Обычно errors = validateForX(data) будет возвращать ошибки, но doX(data) будет выдавать, если они недействительны. - person plalx; 27.03.2019

На самом деле я использую оба.

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

Исключения используются исключительно для вещей, которых я НЕ жду.

person Stephen Wrighton    schedule 19.09.2008
comment
+1 за очень простой путь. По крайней мере, оно достаточно короткое, чтобы я мог очень быстро его прочитать. :-) - person Ashish Gupta; 11.05.2010
comment
Исключения используются исключительно для вещей, которых я НЕ ожидаю. Если вы их не ожидаете, то почему и как вы можете их использовать? - person anar khalilov; 04.12.2013
comment
То, что я этого не ожидаю, не означает, что я не понимаю, как это могло произойти. Я ожидаю, что мой SQL Server будет включен и отвечает. Но я по-прежнему кодирую свои ожидания, чтобы изящно потерпеть неудачу в случае неожиданного простоя. - person Stephen Wrighton; 04.12.2013
comment
Разве неотвечающий SQL Server нельзя легко отнести к категории известных возможных ошибок? - person tkburbidge; 19.10.2020

В соответствии с главой 7, озаглавленной «Исключения» в Руководстве по разработке фреймворка: соглашения, идиомы и шаблоны для многоразовых библиотек .NET, дается множество объяснений того, почему использование исключений вместо возвращаемых значений необходимо для объектно-ориентированных фреймворков, таких как C #.

Возможно, это самая веская причина (стр. 179):

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

person hurst    schedule 19.09.2008
comment
Настоятельно рекомендую прочитать эту главу. В этой главе дается очень систематическое руководство по обработке ошибок / исключений с множеством правил, которые можно и что нельзя делать в Интернете. - person Zhe; 23.02.2021

Я предпочитаю (в C ++ и Python) использовать исключения. Предоставляемые языком средства делают четко определенным процесс как для вызова, так и для перехвата и (при необходимости) повторного вызова исключений, что упрощает просмотр и использование модели. По сути, это чище, чем коды возврата, поскольку конкретные исключения могут определяться по их именам и иметь дополнительную информацию, сопровождающую их. С кодом возврата вы ограничены только значением ошибки (если вы не хотите определить объект ReturnStatus или что-то в этом роде).

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

person Jason Etheridge    schedule 19.09.2008
comment
Помните, что использование исключений затрудняет анализ программы. - person Paweł Hajdan; 19.09.2008

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

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

Исключения также могут предоставить гораздо больше информации и, в частности, хорошо разобрать: «Что-то пошло не так, вот что, трассировка стека и некоторая вспомогательная информация для контекста»

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

person johnc    schedule 19.09.2008

В Java я использую (в следующем порядке):

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

  2. Возврат кодов ошибок во время обработки работы (и выполнение отката при необходимости).

  3. Исключения, но они используются только в непредвиденных случаях.

person paxdiablo    schedule 19.09.2008
comment
Не было бы правильнее использовать утверждения для контрактов? Если контракт разорван, спасать уже не на что. - person Paweł Hajdan; 19.09.2008
comment
@ PawełHajdan, я полагаю, утверждения по умолчанию отключены. Это та же проблема, что и у C assert, в том, что он не обнаружит проблем в производственном коде, если вы не запускаете все время с утверждениями. Я склонен рассматривать утверждения как способ выявления проблем во время разработки, но только для вещей, которые будут утверждать или не утверждать последовательно (например, вещи с константами, не вещи с переменными или что-нибудь еще, что может измениться во время выполнения). - person paxdiablo; 10.03.2020
comment
И двенадцать лет, чтобы ответить на ваш вопрос. Мне нужно запустить службу поддержки :-) - person paxdiablo; 10.03.2020

Используйте исключения! Для меня ответ действительно ясен. Когда контекст диктует (например, интенсивный процессор или общедоступный API) шаблон Try или Tester-Doer, я ДОПОЛНИТЕЛЬНО предоставлю эти методы для версии с выбросом исключения. Я считаю, что общие правила избежания исключений ошибочны, не поддерживаются и, вероятно, приводят к гораздо большим расходам с точки зрения ошибок, чем любые проблемы с производительностью, которые, по их утверждениям, предотвращаются.

Нет, Microsoft НЕ ЗАПРЕЩАЕТСЯ использовать исключения (распространенное неправильное толкование).

В нем говорится, что если вы разрабатываете API, предоставьте способы помочь пользователю этого API избежать ВЫБРОСА исключений, если это необходимо (шаблоны Try и Tester-Doer).

❌ По возможности НЕ используйте исключения для нормального потока управления.

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

Здесь подразумевается, что реализация не-тестировщик-исполнитель / не-пытаться ДОЛЖНА генерировать исключение при сбое, а затем пользователь МОЖЕТ изменить это на один из ваших тестеров-исполнителей или попробовать методы для повышения производительности. Яма успеха поддерживается для безопасности, и пользователь ОПТИМЛЯЕТ более опасный, но более производительный метод.

Microsoft НЕ СКАЗЫВАЕТ использовать коды возврата ДВАЖДЫ, здесь:

❌ НЕ возвращайте коды ошибок.

Исключения являются основным средством сообщения об ошибках в рамках.

✔️ НЕОБХОДИМО сообщать о сбоях выполнения, выдавая исключения.

и здесь:

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

Для повышения производительности можно использовать либо шаблон Tester-Doer, либо шаблон Try-Parse, описанный в следующих двух разделах.

Если вы не используете исключения, вы, вероятно, нарушаете это другое правило возврата кодов возврата или логических значений из реализации, не являющейся тестером / не пытающейся. Опять же, TryParse не заменяет Parse. Предоставляется в дополнение к Parse

ОСНОВНАЯ ПРИЧИНА: коды возврата не соответствуют Pit of Success почти каждый раз проверяет меня.

  • It is far too easy to forget to check a return code and then have a red-herring error later on.
    • var success = Save()? How much performance is worth someone forgetting an if check here?
    • var success = TrySave ()? Лучше, но не будем ли мы злоупотреблять паттерном TryX? Вы все еще предоставляли метод сохранения?
  • Коды возврата не содержат важной отладочной информации, такой как стек вызовов, внутренние исключения.
  • Коды возврата не распространяются, что, наряду с указанным выше, приводит к чрезмерному и взаимосвязанному ведению журнала диагностики вместо ведения журнала в одном централизованном месте (обработчики исключений на уровне приложения и потока).
  • Коды возврата имеют тенденцию приводить к беспорядку в виде вложенных блоков if.
  • Время, затрачиваемое разработчиками на отладку неизвестной проблемы, которая в противном случае была бы очевидным исключением (яма успеха), - это дорого.
  • Если бы команда, стоящая за C #, не намеревалась использовать исключения для управления потоком управления, исключения не вводились бы, не было бы фильтра when по операторам catch и не было бы необходимости в выражении throw без параметров.

Что касается производительности:

  • Исключения могут быть дорогостоящими в вычислительном отношении ОТНОСИТЕЛЬНО отсутствия выброса, но они не зря называются ИСКЛЮЧЕНИЯМИ. При сравнении скорости всегда удается предположить 100% -ную частоту исключений, чего никогда не должно быть. Даже если исключение происходит в 100 раз медленнее, какое это имеет значение, если оно происходит только в 1% случаев?

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

  • Если мы не говорим об арифметике с плавающей запятой для графических приложений или о чем-то подобном, циклы ЦП обходятся дешевле, чем время разработчика.

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

  • Я осмелюсь любому, кто работает в .net, настроить ваш отладчик на прерывание всех исключений и отключение только моего кода и посмотреть, сколько исключений уже происходит, о которых вы даже не знаете.

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

person b_levitt    schedule 27.10.2017

Мне не нравятся коды возврата, потому что они вызывают появление в вашем коде следующей закономерности.

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

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

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

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

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

person Gishu    schedule 19.09.2008

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

person Smashery    schedule 19.09.2008
comment
Вы неправильно это истолковываете. Они имели в виду, что если ваша программа выдает исключения в своих обычных потоках, это неправильно. Другими словами, используйте исключения только в исключительных случаях. - person Paweł Hajdan; 19.09.2008

Некоторое время назад я написал сообщение в блоге об этом. .

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

person Thomas    schedule 19.09.2008
comment
Связанное сообщение в блоге больше о том, как посмотреть, прежде чем прыгать (проверки), а не о том, что проще просить прощения, чем разрешения (исключения или коды возврата). Я ответил там своими мыслями по этому поводу (подсказка: TOCTTOU). Но этот вопрос касается другого вопроса, а именно, при каких условиях использовать механизм исключений языка, а не возвращать значение со специальными свойствами. - person Damian Yerrick; 29.11.2016
comment
Я полностью согласен. Кажется, я кое-чему научился за последние девять лет;) - person Thomas; 30.11.2016

У меня простой набор правил:

1) Используйте коды возврата для вещей, на которые, как вы ожидаете, отреагирует ваш непосредственный абонент.

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

В Java я когда-либо использовал только непроверенные исключения, проверенные исключения в конечном итоге были просто еще одной формой кода возврата, и, по моему опыту, двойственность того, что могло быть «возвращено» вызовом метода, обычно была скорее препятствием, чем помощью.

person Kendall Helmstetter Gelner    schedule 19.09.2008

С любым достойным компилятором или средой выполнения исключения не влекут за собой значительных штрафов. Это более или менее похоже на оператор GOTO, который переходит к обработчику исключения. Кроме того, наличие исключений, перехваченных средой выполнения (например, JVM), помогает намного проще изолировать и исправить ошибку. Я приму исключение NullPointerException в Java вместо segfault в C в любой день.

person Kyle Cronin    schedule 19.09.2008
comment
Исключения очень дороги. Они должны пройтись по стеку, чтобы найти потенциальные обработчики исключений. Эта прогулка по стеку стоит недешево. Если строится трассировка стека, это еще дороже, потому что тогда весь стек должен быть проанализирован. - person Derek Park; 19.09.2008
comment
Я удивлен, что компиляторы не могут определить, где будет обнаружено исключение, по крайней мере, иногда. Кроме того, тот факт, что исключение изменяет поток кода, упрощает определение того, где именно возникает ошибка, на мой взгляд, компенсирует снижение производительности. - person Kyle Cronin; 19.09.2008
comment
Стек вызовов может стать чрезвычайно сложным во время выполнения, и компиляторы обычно не проводят такого анализа. Даже если бы они это сделали, вам все равно пришлось бы пройтись по стеку, чтобы найти след. Вам также все равно придется раскручивать стек, чтобы иметь дело с finally блоками и деструкторами для объектов, размещенных в стеке. - person Derek Park; 19.09.2008
comment
Я согласен с тем, что преимущества отладки исключений часто компенсируют затраты на производительность. - person Derek Park; 19.09.2008
comment
Дерак Парк, исключения дороги, когда они случаются. По этой причине не следует злоупотреблять ими. Но когда этого не происходит, они практически ничего не стоят. - person paercebal; 21.09.2008
comment
Сказать, что исключения - это дорого, все равно что сказать, что поездка на машине или поездка на машине обходятся дороже, чем ходьба. Да, это так, но вы часто будете отдавать предпочтение автомобилю, а не ходьбе пешком. - person FantomX1; 25.09.2020

Я использую исключения в python как в исключительных, так и в неисключительных обстоятельствах.

Часто приятно иметь возможность использовать Exception, чтобы указать, что «запрос не может быть выполнен», в отличие от возврата значения Error. Это означает, что вы / всегда / знаете, что возвращаемое значение имеет правильный тип, а не произвольно None или NotFoundSingleton или что-то в этом роде. Вот хороший пример того, где я предпочитаю использовать обработчик исключений вместо условия для возвращаемого значения.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

Побочный эффект заключается в том, что при запуске datastore.fetch (obj_id) вам никогда не нужно проверять, является ли его возвращаемое значение None, вы получите эту ошибку немедленно и бесплатно. Это противоречит аргументу, «ваша программа должна иметь возможность выполнять все свои основные функции без использования исключений».

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

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Один системный вызов вместо двух, состояния гонки нет. Это плохой пример, потому что, очевидно, это приведет к ошибке OSError в большем количестве обстоятельств, чем каталог не существует, но это «достаточно хорошее» решение для многих строго контролируемых ситуаций.

person Jerub    schedule 19.09.2008
comment
Второй пример вводит в заблуждение. Предположительно неправильный способ неверен, потому что код os.path.rmdir предназначен для создания исключения. Правильная реализация в потоке кода возврата: 'if rmdir (...) == FAILED: pass' - person MaR; 12.02.2013

Я считаю, что коды возврата усиливают кодовый шум. Например, мне всегда не нравился внешний вид кода COM / ATL из-за кодов возврата. Для каждой строчки кода требовалась проверка HRESULT. Я считаю, что код возврата ошибки - одно из плохих решений, принятых архитекторами COM. Это затрудняет логическую группировку кода, поэтому становится трудным проверка кода.

Я не уверен в сравнении производительности, когда есть явная проверка кода возврата в каждой строке.

person Community    schedule 19.09.2008
comment
COM wad предназначен для использования языками, не поддерживающими исключения. - person Kevin; 19.09.2008
comment
Это хороший момент. Имеет смысл разобраться с кодами ошибок для языков сценариев. По крайней мере, VB6 хорошо скрывает детали кода ошибки, инкапсулируя их в объект Err, что несколько помогает в более чистом коде. - person rpattabi; 20.09.2008
comment
Я не согласен: VB6 регистрирует только последнюю ошибку. В сочетании с печально известным возобновлением ошибки в следующий раз, вы вообще упустите источник своей проблемы к тому моменту, когда увидите его. Обратите внимание, что это основа обработки ошибок в Win32 APII (см. Функцию GetLastError) - person paercebal; 21.09.2008

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

Коды ошибок могут быть в порядке, но возвращение 404 или 200 из метода - это плохо, ИМО. Вместо этого используйте перечисления (.Net), что сделает код более читаемым и более простым в использовании для других разработчиков. Также вам не нужно вести таблицу с числами и описаниями.

Также; в моей книге шаблон «попробуй-поймай-наконец» - это анти-шаблон. Попробовать наконец-то может быть хорошо, пробовать-поймать тоже можно, но пробовать-наконец-то никогда не бывает. try-finally часто можно заменить оператором "using" (шаблон IDispose), что лучше IMO. И попытаться поймать, где вы действительно поймаете исключение, которое вы можете обработать, - это хорошо, или если вы сделаете это:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Итак, пока вы позволяете исключению продолжать пузыриться, все в порядке. Другой пример:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Здесь я действительно обрабатываю исключение; то, что я делаю за пределами try-catch, когда метод обновления терпит неудачу, - это совсем другая история, но я думаю, что моя точка зрения была высказана. :)

Почему же тогда попробуйте-поймать-наконец-то - это антипаттерн? Вот почему:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Что произойдет, если объект db уже закрыт? Выдается новое исключение, которое необходимо обработать! Это лучше:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Или, если объект db не реализует IDisposable, сделайте следующее:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

В любом случае это мои 2 цента! :)

person noocyte    schedule 19.09.2008
comment
Будет странно, если у объекта есть .Close (), но нет .Dispose () - person abatishchev; 17.08.2009
comment
Я использую Close () только в качестве примера. Не стесняйтесь думать об этом как о чем-то другом. Как я утверждаю; следует использовать шаблон using (нехорошо!), если он доступен. Это, конечно, означает, что класс реализует IDisposable, и поэтому вы можете вызвать Dispose. - person noocyte; 20.08.2009

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

person Trent    schedule 19.09.2008

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

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

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Лично я предпочитаю использовать исключения для ошибок, которые ДОЛЖНЫ или ДОЛЖНЫ быть обработаны вызывающим кодом, и использовать коды ошибок только для «ожидаемых сбоев», когда возврат чего-либо действительно действителен и возможен.

person Daniel Bruce    schedule 19.09.2008
comment
По крайней мере, в C / C ++ и gcc вы можете дать функции атрибут, который будет генерировать предупреждение, если его возвращаемое значение игнорируется. - person Paweł Hajdan; 19.09.2008
comment
phjr: Хотя я не согласен с шаблоном кода ошибки возврата, ваш комментарий, возможно, должен стать полным ответом. Я нахожу это достаточно интересным. По крайней мере, это дало мне полезную информацию. - person paercebal; 21.09.2008

Есть много причин предпочесть Исключения коду возврата:

  • Обычно для удобства чтения люди стараются минимизировать количество операторов возврата в методе. При этом исключения предотвращают выполнение дополнительной работы в некорректном состоянии и, таким образом, предотвращают потенциальное повреждение большего количества данных.
  • Исключения обычно более подробны и легче расширяются, чем возвращаемое значение. Предположим, что метод возвращает натуральное число и что вы используете отрицательные числа в качестве кода возврата при возникновении ошибки, если объем вашего метода изменится и теперь возвращает целые числа, вам придется изменить все вызовы методов, а не просто немного настроить исключение.
  • Исключения позволяют более легко отделить обработку ошибок от нормального поведения. Они позволяют гарантировать, что некоторые операции выполняются как атомарные операции.
person gizmo    schedule 19.09.2008

Я использую только исключения, без кодов возврата. Я говорю о Java здесь.

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

person SCdF    schedule 19.09.2008

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

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Или, что еще хуже, если бы я удалил то, чего не должен был, но меня бросили ловить до того, как я выполнил остальную очистку. Бросок давил на бедного пользователя ИМХО.

person Robert Gould    schedule 19.09.2008
comment
Это то, для чего нужен оператор ‹code› finally ‹/code›. Но, увы, этого нет в стандарте C ++ ... - person Thomas; 19.09.2008
comment
В C ++ вы должны придерживаться практического правила: приобретать ресурсы в конструкторе и освобождать их в деструкторе. В этом конкретном случае лучше всего подойдет auto_ptr. - person Serge; 19.09.2008
comment
Томас, ты ошибаешься. В C ++ нет, наконец, потому что он ему не нужен. Вместо него используется RAII. Решение Serge - это одно решение, использующее RAII. - person paercebal; 21.09.2008
comment
Роберт, используйте решение Сержа, и ваша проблема исчезнет. Теперь, если вы напишете больше try / catch, чем throw, тогда (судя по вашему комментарию), возможно, у вас есть проблема в вашем коде. Конечно, использование catch (...) без повторного броска обычно плохо, так как оно скрывает ошибку, чтобы лучше ее игнорировать. - person paercebal; 21.09.2008

Мое общее правило в аргументе исключения и кода возврата:

  • Используйте коды ошибок, когда вам нужна локализация / интернационализация - в .NET вы можете использовать эти коды ошибок для ссылки на файл ресурсов, который затем отобразит ошибку на соответствующем языке. В противном случае используйте исключения
  • Используйте исключения только для действительно исключительных ошибок. Если такое случается довольно часто, используйте логическое значение или код ошибки перечисления.
person Jon Limjap    schedule 19.09.2008
comment
Нет причин, по которым вы не могли бы использовать исключение при выполнении l10n / i18n. Исключения также могут содержать локализованную информацию. - person gizmo; 19.09.2008

Я не считаю, что коды возврата менее уродливы, чем исключения. За исключением, у вас есть try{} catch() {} finally {}, где, как и в случае с кодами возврата, у вас есть if(){}. Раньше я боялся исключений по причинам, указанным в посте; вы не знаете, нужно ли очистить указатель, что у вас есть. Но я думаю, что у вас те же проблемы, когда дело касается кодов возврата. Вы не знаете состояние параметров, если не знаете некоторых подробностей о рассматриваемой функции / методе.

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

Мне нравится идея возврата значения (перечисления?) Для результатов и исключения в исключительном случае.

person Jonathan Adelson    schedule 19.09.2008

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

Такой подход типичен для языка Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

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

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

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

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

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

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

person Nathan Long    schedule 13.08.2020

Для такого языка, как Java, я бы выбрал Exception, потому что компилятор выдает ошибку времени компиляции, если исключения не обрабатываются. Это заставляет вызывающую функцию обрабатывать / генерировать исключения.

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

person 2ank3th    schedule 20.02.2018