Какие и почему вы предпочитаете исключения или коды возврата? - PullRequest
72 голосов
/ 19 сентября 2008

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

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

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

Ответы [ 23 ]

97 голосов
/ 21 сентября 2008

Для некоторых языков (например, 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, который может возвращать bool "false" (для сбойного)
  • doSomethingElseSagain, которая может возвращать объект Error (как с __LINE__, __FILE__, так и с половиной переменных стека.
  • doTryToDoSomethingWithAllThisMess, который, ну ... Используйте вышеупомянутые функции и возвращают код ошибки типа ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Это зависит от контракта вашей функции. Если функция принимает указатель, но указывает, что указатель должен быть ненулевым, то можно генерировать исключение, когда пользователь отправляет указатель 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.
}

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

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

Заключение

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

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

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

30 голосов
/ 19 сентября 2008

Я использую оба на самом деле.

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

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

19 голосов
/ 19 сентября 2008

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

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

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

10 голосов
/ 19 сентября 2008

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

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

7 голосов
/ 19 сентября 2008

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

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

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

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

4 голосов
/ 19 сентября 2008

Я недавно написал в блоге 1002 * об этом.

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

4 голосов
/ 19 сентября 2008

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

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

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

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

4 голосов
/ 19 сентября 2008

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

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

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

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

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

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

3 голосов
/ 19 сентября 2008

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

3 голосов
/ 19 сентября 2008

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

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

Также; образец try-catch-finally - это анти-шаблон в моей книге. Try-finally может быть хорошим, try-catch также может быть хорошим, но try-catch-finally никогда не бывает хорошим. 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-catch-finally является анти-паттерном? И вот почему:

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

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

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 цента! :)

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