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





Обычно я предпочитаю исключения, потому что они содержат больше контекстной информации и могут более четко передать (при правильном использовании) ошибку программисту.
С другой стороны, коды ошибок легче, чем исключения, но их сложнее поддерживать. Проверка ошибок может быть случайно пропущена. Коды ошибок труднее поддерживать, потому что вам нужно вести каталог со всеми кодами ошибок, а затем включать результат, чтобы увидеть, какая ошибка была выдана. Здесь могут помочь диапазоны ошибок, потому что, если единственное, что нас интересует, это наличие ошибки или ее отсутствие, это проще проверить (например, код ошибки HRESULT больше или равен 0 - это успех, а меньше нуля - отказ). Их можно непреднамеренно пропустить, потому что нет программного принуждения, которое разработчик будет проверять на наличие кодов ошибок. С другой стороны, нельзя игнорировать исключения.
Подводя итог, я предпочитаю исключения кодам ошибок почти во всех ситуациях.
@MooingDuck, невозможно, если только те тесты не являются несправедливыми / предвзятыми. Вы фактически утверждаете, что существует среда, которая манипулирует объектами и строками более эффективно, чем числами. Определенно возможный, но вряд ли заурядный.
@smink, Хорошие моменты, но как решить проблему накладных расходов, связанных с исключениями? Коды ошибок не просто легковесны, это в основном невесомый; исключения не просто средние по весу, это объекты с тяжелый-весом, содержащие информацию о стеке и разный материал, который мы в любом случае не используем.
@Pacerier: вы рассматриваете только тесты, в которых выбрасываются исключения. Вы на 100% правы в том, что генерирование исключения C++ значительно медленнее, чем возврат кода ошибки. Руки вниз, без споров. Чем отличается делать, так это остальными 99,999% кода. За исключениями, нам не нужно проверять возврат кода ошибки между каждым оператором, что делает код который примерно на 1-50% быстрее (или нет, в зависимости от вашего компилятора). Это означает, что полный код может работать быстрее или медленнее, полностью в зависимости от того, как написан код и как часто возникают исключения.
@Pacerier: В этом искусственном тесте, который я только что написал, код на основе исключений работает так же быстро, как коды ошибок в MSVC и Clang, но не в GCC: coliru.stacked-crooked.com/a/e81694e5c508945b (время внизу). Похоже, что используемые мной флаги GCC генерировали необычно медленные исключения по сравнению с другими компиляторами. Я предвзят, поэтому, пожалуйста, критикуйте мой тест и не стесняйтесь пробовать другой вариант.
@Mooing Duck, если вы одновременно тестировали и коды ошибок, и исключения, значит, у вас должны быть включены исключения. Если это так, ваши результаты предполагают, что использование кодов ошибок с накладными расходами на обработку исключений не медленнее, чем использование только исключений. Чтобы получить значимые результаты, тесты кода ошибки должны выполняться с отключенными исключениями.
@MikaHaarahiltunen: Хорошо подмечено, мне нужен новый искусственный тест coliru.stacked-crooked.com/a/3564a61ef4c9bcfb. Однажды я нашел отличный, но сейчас не могу вспомнить. Я думаю, это было сродни наивному Фибоначчи.
«Они могут быть непреднамеренно опущены, потому что нет программного принуждения, которое разработчик будет проверять на наличие кодов ошибок»: это больше не соответствует действительности, посмотрите, как работает обработка ошибок ржавчины, на некоторых языках компилятор проверяет, обработали ли вы ошибку
Я предпочитаю исключения, потому что
Ваш четвертый пункт не совсем правильный: статус ошибки при преобразовании в объект также может содержать код для проверки того, существует ли файл, заблокирован и т. д. Это просто вариант stackoverflow.com/a/3157182/632951
«они прерывают поток логики». Исключения имеют более или менее эффект goto.
@peterchaula Нет никаких дурных функций, только злоупотребление функциями.
В прошлом я присоединился к лагерю кодов ошибок (слишком много программировал на C). Но теперь я увидел свет.
Да, исключения - это небольшая нагрузка для системы. Но они упрощают код, уменьшая количество ошибок (и WTF).
Так что используйте исключения, но используйте их с умом. И они будут твоими друзьями.
В качестве примечания. Я научился документировать, какое исключение может быть вызвано каким методом. К сожалению, для большинства языков этого не требуется. Но это увеличивает шанс обработки правильных исключений на нужном уровне.
yap C действительно оставляет у всех нас несколько привычек;)
Сигнатуры методов должны сообщать вам, что делает метод. Что-то типа длинный errorCode = getErrorCode (); может быть хорошо, но длинный errorCode = fetchRecord (); сбивает с толку.
Коды ошибок могут игнорироваться (и часто!) Вызывающими вашими функциями. Исключения, по крайней мере, заставляют их каким-то образом устранять ошибку. Даже если их версия борьбы с этим - пустой обработчик catch (вздох).
Это верно для одних языков и неверно для других. Например, Java заставляет вас либо перехватывать исключения, либо генерировать их, но есть и другие языки, такие как JavaScript или Python, где обработка исключений не обеспечивается языком.
Коды ошибок также не работают, когда ваш метод возвращает что-либо, кроме числового значения ...
Гм, нет. См. Парадигму Win32 GetLastError (). Я не защищаю это, просто указываю на то, что вы неправы.
На самом деле есть много других способов сделать это. Другой способ - вернуть объект, содержащий код ошибки и реальное возвращаемое значение. Еще один способ - передать ссылку.
Для большинства приложений лучше использовать исключения. Исключение составляют случаи, когда программное обеспечение должно взаимодействовать с другими устройствами. Область, в которой я работаю, - это промышленные системы управления. Здесь коды ошибок предпочтительны и ожидаемы. Так что я отвечу, что это зависит от ситуации.
Несомненно, исключения по кодам ошибок. Вы получаете много тех же преимуществ от исключений, что и от кодов ошибок, но также и гораздо больше, без недостатков кодов ошибок. Единственный удар по исключениям состоит в том, что это немного больше накладных расходов; но в наши дни эти накладные расходы следует считать незначительными почти для всех приложений.
Вот несколько статей, в которых обсуждаются, сравниваются и противопоставляются эти две техники:
В них есть несколько хороших ссылок, по которым можно почитать дальше.
В материалах высокого уровня исключения; в низкоуровневых материалах - коды ошибок.
Поведение исключения по умолчанию - раскрутить стек и остановить программу, если я пишу сценарий и выбираю ключ, которого нет в словаре, это, вероятно, ошибка, и я хочу, чтобы программа остановилась и позволила мне знать все об этом.
Если, однако, я пишу фрагмент кода, поведение которого я должен знать во всех возможных ситуациях, то мне нужны коды ошибок. В противном случае мне нужно знать каждое исключение, которое может быть сгенерировано каждой строкой в моей функции, чтобы знать, что она будет делать (прочтите Исключение, которое послужило основанием для авиакомпании, чтобы понять, насколько это сложно). Утомительно и сложно писать код, который должным образом реагирует на каждую ситуацию (включая несчастливые), но это потому, что писать безошибочный код утомительно и сложно, а не потому, что вы передаете коды ошибок.
Оба Раймонд ЧениДжоэл выдвинули несколько красноречивых аргументов против использования исключений для всего.
Я нашел два из этих постов очень интересными (я не читал «и»), но особенно с постом Раймонда было трудно сказать, какая его часть действительно не зависит от языка. Думаю, вы могли бы написать столь же красноречивый аргумент в пользу использования исключений, а не кодов ошибок.
+1 за указание на то, что контекст имеет какое-то отношение к стратегии обработки ошибок.
@Tom, Хорошие моменты, но исключения обязательно вылавливаются. Как мы гарантируем, что коды ошибок равны пойманный и не игнорируются автоматически из-за ошибок?
Итак, ваш единственный аргумент против исключений состоит в том, что может случиться что-то плохое, если вы забудете перехватить исключение, но вы не рассматриваете столь же вероятную ситуацию, когда вы забудете проверить возвращаемое значение на наличие ошибок? Не говоря уже о том, что вы получаете трассировку стека, когда забываете перехватить исключение, но ничего не получаете, когда забываете проверить код ошибки.
@Esailija: нет, основные аргументы против заключаются в том, что сложнее понять ход программы, написать безопасный код и распознать плохой код. Тем не менее, я думаю, что преимуществ намного больше.
Я не полностью согласен с использованием кодов ошибок, когда вы должны знать поведение в каждой возможной ситуации. Даже с кодами ошибок легко забыть обработать несколько. Java в любом случае поощряет использование исключений в таких ситуациях. Одна из причин, по которой Java делает обработку возможных исключений обязательной. Я согласен с низким уровнем и высоким уровнем, но только потому, что выброс и перехват исключений - относительно дорогостоящая операция с точки зрения циклов ЦП. По другой причине использовать исключения - это чистый код. Ваш код становится намного чище, если вы не смешиваете логику с обработкой ошибок.
C++ 17 представляет атрибут nodiscard, который выдает предупреждение компилятора, если возвращаемое значение функции не сохраняется. Немного помогает отловить забытый код ошибки. Пример: godbolt.org/g/6i6E0B
Комментарий @ Esailija здесь как раз по теме. Аргумент против исключений здесь берет гипотетический API на основе кода ошибки, где все коды ошибок задокументированы, и гипотетический программист, который читает документацию, правильно определяет все случаи ошибок, которые логически возможны в ее приложении, и пишет код для обработки каждой из них. , затем сравнивает этот сценарий с гипотетическим API на основе исключений и программистом, где по какой-то причине один из этих шагов идет не так ... даже несмотря на то, что одинаково легко (возможно, Полегче) выполнить все эти шаги правильно в API на основе исключений.
Рабочая ссылка (на момент написания этой статьи, разумеется) на статью Раймонда Чена: devblogs.microsoft.com/oldnewthing/20050114-00/?p=36693 (заголовок сообщения: «Чище, элегантнее и труднее распознать»
Я считаю, что если вы пишете низкоуровневый драйвер, которому действительно нужна производительность, используйте коды ошибок. Но если вы используете этот код в приложении более высокого уровня, и он может обрабатывать небольшие накладные расходы, тогда оберните этот код интерфейсом, который проверяет эти коды ошибок и вызывает исключения.
Во всех остальных случаях, вероятно, лучше использовать исключения.
Мой подход заключается в том, что мы можем использовать оба кода, то есть коды исключений и ошибок одновременно.
Я привык определять несколько типов исключений (например, DataValidationException или ProcessInterruptExcepion) и внутри каждого исключения определять более подробное описание каждой проблемы.
Простой пример на Java:
public class DataValidationException extends Exception {
private DataValidation error;
/**
*
*/
DataValidationException(DataValidation dataValidation) {
super();
this.error = dataValidation;
}
}
enum DataValidation{
TOO_SMALL(1,"The input is too small"),
TOO_LARGE(2,"The input is too large");
private DataValidation(int code, String input) {
this.input = input;
this.code = code;
}
private String input;
private int code;
}
Таким образом, я использую исключения для определения ошибок категорий и коды ошибок для определения более подробной информации о проблеме.
эээ ... throw new DataValidationException("The input is too small")? Одно из преимуществ исключений - предоставление подробной информации.
Я могу сидеть здесь на заборе, но ...
В Python использование исключений является стандартной практикой, и я вполне счастлив определить свои собственные исключения. В C у вас вообще нет исключений.
В C++ (по крайней мере, в STL) исключения обычно генерируются только для действительно исключительных ошибок (я практически никогда их не вижу). Я не вижу причин делать что-то другое в собственном коде. Да, легко игнорировать возвращаемые значения, но C++ также не заставляет вас перехватывать исключения. Я думаю, тебе просто нужно выработать привычку это делать.
Кодовая база, над которой я работаю, - это в основном C++, и мы используем коды ошибок почти везде, но есть один модуль, который вызывает исключения для любых ошибок, в том числе очень обычных, и весь код, который использует этот модуль, довольно ужасен. Но это может быть только потому, что мы смешали исключения и коды ошибок. С кодом, который постоянно использует коды ошибок, намного проще работать. Если бы наш код постоянно использовал исключения, возможно, это было бы не так плохо. Смешивание этих двух вещей, кажется, не так хорошо работает.
Исключения составляют обстоятельства исключительный, т. Е. Когда они не являются частью нормального потока кода.
Вполне законно смешивать исключения и коды ошибок, где коды ошибок представляют статус чего-либо, а не ошибку в выполнении кода как таковую (например, проверка кода возврата из дочернего процесса).
Но когда случаются исключительные обстоятельства, я считаю, что исключения являются наиболее выразительной моделью.
Есть случаи, когда вы могли бы предпочесть или использовать коды ошибок вместо исключений, и они уже были адекватно рассмотрены (кроме других очевидных ограничений, таких как поддержка компилятора).
Но если пойти в другом направлении, то использование исключений позволяет создавать абстракции еще более высокого уровня для обработки ошибок, что может сделать ваш код еще более выразительным и естественным. Я настоятельно рекомендую прочитать эту превосходную, но недооцененную статью эксперта по C++ Андрея Александреску на тему того, что он называет «Enforcements»: http://www.ddj.com/cpp/184403864. Хотя это статья на C++, принципы в целом применимы, и я довольно успешно перевел концепцию принудительного исполнения на C#.
Я бы никогда не смешал две модели ... слишком сложно преобразовать одну в другую, когда вы переходите от одной части стека, которая использует коды ошибок, к более высокой части, которая использует исключения.
Исключения составляют «все, что останавливает или запрещает методу или подпрограмме выполнять то, что вы просили сделать» ... НЕ передавать обратно сообщения о нарушениях или необычных обстоятельствах, состоянии системы и т. д. Используйте возвращаемые значения или ссылку (или вне) параметры для этого.
Исключения позволяют писать (и использовать) методы с семантикой, зависящей от функции метода, то есть метод, который возвращает объект Employee или список сотрудников, может быть набран для этого, и вы можете использовать его, вызвав.
Employee EmpOfMonth = GetEmployeeOfTheMonth();
С кодами ошибок все методы возвращают код ошибки, поэтому для тех, кому нужно вернуть что-то еще для использования вызывающим кодом, вы должны передать ссылочную переменную, которая будет заполнена этими данными, и протестировать возвращаемое значение для код ошибки и обрабатывать его при каждом вызове функции или метода.
Employee EmpOfMonth;
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
// code to Handle the error here
Если вы кодируете так, чтобы каждый метод выполнял одну и только одну простую вещь, вы должны генерировать исключение всякий раз, когда метод не может выполнить желаемую цель метода. Таким образом исключения намного богаче и проще в использовании, чем коды ошибок. Ваш код намного чище - стандартный поток "нормального" пути кода может быть посвящен строго тому случаю, когда метод МОЖЕТ выполнить то, что вы хотели, чтобы он делал ... А затем код для очистки или обработки «исключительные» обстоятельства, когда происходит что-то плохое, препятствующее успешному завершению метода, могут быть отделены от обычного кода. Кроме того, если вы не можете обработать исключение там, где оно произошло, и должны передать его вверх по стеку в пользовательский интерфейс (или, что еще хуже, по сети от компонента среднего уровня к пользовательскому интерфейсу), то с моделью исключения вы не нужно кодировать каждый промежуточный метод в вашем стеке, чтобы распознать и передать исключение в стек ... Модель исключения делает это автоматически ... С кодами ошибок эта часть головоломки может очень быстро стать обременительной. .
Отличный ответ! Готовое решение как раз вовремя!
Поскольку я работаю с C++ и у меня есть RAII, чтобы сделать их безопасными в использовании, я почти всегда использую исключения. Он выводит обработку ошибок из обычного потока программы и делает ее более понятной.
Однако я оставляю исключения для исключительных обстоятельств. Если я ожидаю, что определенная ошибка будет происходить часто, я проверю, будет ли операция успешной, прежде чем выполнять ее, или вызову версию функции, которая вместо этого использует коды ошибок (например, TryParse())
Я бы предпочел исключения для всех случаев ошибок, кроме тех случаев, когда отказ является ожидаемым результатом без ошибок функции, возвращающей примитивный тип данных. Например. поиск индекса подстроки в более крупной строке обычно возвращает -1, если не найден, вместо того, чтобы вызывать NotFoundException.
Возврат недействительных указателей, которые могут быть разыменованы (например, вызывая исключение NullPointerException в Java), недопустим.
Использование нескольких различных числовых кодов ошибок (-1, -2) в качестве возвращаемых значений для одной и той же функции обычно является плохим стилем, поскольку клиенты могут выполнять проверку «== -1» вместо «<0».
Здесь следует помнить об эволюции API с течением времени. Хороший API позволяет изменять и расширять поведение при сбоях несколькими способами, не нарушая работу клиентов. Например. если дескриптор ошибки клиента проверен на наличие 4 случаев ошибок, и вы добавляете пятое значение ошибки в свою функцию, обработчик клиента может не проверить это и сломаться. Если вы вызываете исключения, это обычно облегчает клиентам переход на более новую версию библиотеки.
Еще одна вещь, которую следует учитывать при работе в команде, - где провести четкую границу, чтобы все разработчики приняли такое решение. Например. «Исключения для высокоуровневых вещей, коды ошибок для низкоуровневых вещей» очень субъективно.
В любом случае, когда возможно более одного тривиального типа ошибки, исходный код никогда не должен использовать числовой литерал для возврата кода ошибки или для его обработки (возврат -7, если x == -7 ...), но всегда именованная константа (возвращает NO_SUCH_FOO, если x == NO_SUCH_FOO).
Если вы работаете над большим проектом, вы не можете использовать только исключения или только коды ошибок. В разных случаях следует использовать разные подходы.
Например, вы решили использовать только исключения. Но как только вы решите использовать асинхронную обработку событий. В таких ситуациях использовать исключения для обработки ошибок - плохая идея. Но использовать коды ошибок везде в приложении утомительно.
Поэтому я считаю, что одновременное использование исключений и кодов ошибок - это нормально.
Может быть несколько ситуаций, когда использование исключений чистым, ясным и правильным способом является обременительным, но подавляющее большинство временных исключений - очевидный выбор. Самым большим преимуществом обработки исключений над кодами ошибок является то, что она изменяет поток выполнения, что важно по двум причинам.
Когда возникает исключение, приложение больше не следует своему «нормальному» пути выполнения. Первая причина, почему это так важно, заключается в том, что, если автор кода не пойдет хорошо и действительно не будет плохим, программа остановится и не продолжит делать непредсказуемые вещи. Если код ошибки не проверяется и соответствующие действия не предпринимаются в ответ на неправильный код ошибки, программа продолжит делать то, что делает, и кто знает, каков будет результат этого действия. Существует множество ситуаций, когда выполнение программы «что угодно» может оказаться очень дорогостоящим. Рассмотрим программу, которая извлекает информацию о производительности для различных финансовых инструментов, которые продает компания, и доставляет эту информацию брокерам / оптовикам. Если что-то пойдет не так, а программа продолжит работу, она может отправить ошибочные данные о производительности брокерам и оптовикам. Я не знаю ни о ком другом, но я не хочу сидеть в офисе вице-президента и объяснять, почему из-за моего кодекса компания получила 7-значные штрафы. Отправка сообщения об ошибке клиентам, как правило, предпочтительнее доставки неправильных данных, которые могут выглядеть «реальными», и в последней ситуации гораздо легче столкнуться с гораздо менее агрессивным подходом, например с кодами ошибок.
Вторая причина, по которой мне нравятся исключения и их нарушение нормального выполнения, заключается в том, что они значительно упрощают сохранение логики «происходят нормальные вещи» отдельно от логики «что-то пошло не так». Для меня это:
try {
// Normal things are happening logic
catch (// A problem) {
// Something went wrong logic
}
... предпочтительнее этого:
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
Есть и другие приятные мелочи об исключениях. Наличие связки условной логики для отслеживания того, был ли какой-либо из методов, вызываемых в функции, возвращенным кодом ошибки, и возвращение этого кода ошибки выше - это много шаблонов. На самом деле, может выйти из строя множество котлов. Я гораздо больше верю в систему исключений большинства языков, чем в крысиное гнездо утверждений if-else-if-else, написанных Фредом из «Новенького колледжа», и у меня есть много дел поинтереснее. с моим временем, чем просмотр кода, сказал крысиное гнездо.
Я думаю, это также зависит от того, действительно ли вам нужна информация, такая как трассировка стека, из результата. Если да, вы определенно выбираете Exception, который предоставляет объект с большим количеством информации о проблеме. Однако, если вас просто интересует результат и вас не волнует, почему этот результат, введите код ошибки.
например Когда вы обрабатываете файл и сталкиваетесь с IOException, клиенту может быть интересно узнать, откуда это было инициировано, в открытии файла или при анализе файла и т. д. Так что лучше вам вернуть IOException или его конкретный подкласс. Однако в сценарии, например, у вас есть метод входа в систему, и вы хотите знать, что он был успешным или нет, вы либо просто возвращаете логическое значение, либо показываете правильное сообщение, возвращаете код ошибки. Здесь Клиента не интересует, какая часть логики вызвала этот код ошибки. Он просто знает, недействительны ли его учетные данные или заблокирована ли учетная запись и т. д.
Еще один вариант использования, который я могу придумать, - это передача данных по сети. Ваш удаленный метод может возвращать только код ошибки вместо исключения, чтобы свести к минимуму передачу данных.
Во-первых, я согласен с Томом отвечать в том, что для высокоуровневых материалов используются исключения, а для низкоуровневых - коды ошибок, если это не сервис-ориентированная архитектура (SOA).
В SOA, где методы могут вызываться на разных машинах, исключения не могут передаваться по сети, вместо этого мы используем ответы об успехе / неудаче со структурой, подобной приведенной ниже (C#):
public class ServiceResponse
{
public bool IsSuccess => string.IsNullOrEmpty(this.ErrorMessage);
public string ErrorMessage { get; set; }
}
public class ServiceResponse<TResult> : ServiceResponse
{
public TResult Result { get; set; }
}
И используйте вот так:
public async Task<ServiceResponse<string>> GetUserName(Guid userId)
{
var response = await this.GetUser(userId);
if (!response.IsSuccess) return new ServiceResponse<string>
{
ErrorMessage = $"Failed to get user."
};
return new ServiceResponse<string>
{
Result = user.Name
};
}
Когда они последовательно используются в ваших ответах службы, это создает очень хороший шаблон обработки успехов / сбоев в приложении. Это позволяет упростить обработку ошибок в асинхронных вызовах внутри служб, а также между службами.
Мое общее правило:
«Коды ошибок легче, чем исключения», зависит от того, что вы измеряете и как вы измеряете. Довольно легко придумать тесты, которые показывают, что API на основе исключений могут быть намного быстрее.