Соглашения об исключениях или кодах ошибок - PullRequest
106 голосов
/ 31 октября 2008

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

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

Ответы [ 22 ]

77 голосов
/ 31 октября 2008

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

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

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

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

56 голосов
/ 31 октября 2008

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

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

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

23 голосов
/ 31 октября 2008

Я предпочитаю исключения, потому что

  • они прерывают поток логики
  • они получают выгоду от иерархии классов, которая дает больше возможностей / функциональности
  • при правильном использовании может представлять широкий спектр ошибок (например, InvalidMethodCallException также является LogicException, поскольку оба возникают, когда в вашем коде есть ошибка, которую следует обнаружить до выполнения), и
  • они могут использоваться для исправления ошибки (то есть определение класса FileReadException может затем содержать код для проверки, существует ли файл или заблокирован и т. Д.)
18 голосов
/ 31 октября 2008

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

16 голосов
/ 31 октября 2008

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

Вот некоторые статьи, обсуждающие, сравнивающие и противопоставляющие две техники:

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

14 голосов
/ 31 октября 2008

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

Исключения составляют «все, что останавливает или запрещает методу или подпрограмме выполнять то, что вы просили его» ... НЕ передавать сообщения о нарушениях или необычных обстоятельствах, о состоянии системы и т. Д. Используйте return значения или ref (или out) параметры для этого.

Исключения позволяют писать (и использовать) методы с семантикой, зависящей от функции метода, то есть метод, который возвращает объект Employee или список сотрудников, может быть набран именно для этого, и вы можете использовать его, вызывая ,

Employee EmpOfMonth = GetEmployeeOfTheMonth();

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

Employee EmpOfMonth; 
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
    // code to Handle the error here

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

11 голосов
/ 31 октября 2008

В прошлом я присоединился к лагерю кодов ошибок (слишком много программировал на Си) Но теперь я увидел свет.

Да, исключения являются небольшим бременем для системы. Но они упрощают код, уменьшая количество ошибок (и WTF).

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

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

10 голосов
/ 21 февраля 2015

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

Когда возникает исключение, приложение больше не следует своему «нормальному» пути выполнения. Первая причина, по которой это так важно, заключается в том, что если автор кода не сделает все возможное и действительно плохое, программа остановится и не будет продолжать делать непредсказуемые вещи. Если код ошибки не проверяется и соответствующие действия не предпринимаются в ответ на неверный код ошибки, программа продолжит делать то, что она делает, и кто знает, каков будет результат этого действия. Есть много ситуаций, когда выполнение программы «что угодно» может оказаться очень дорогим. Рассмотрим программу, которая извлекает информацию об эффективности различных финансовых инструментов, которые продает компания, и передает эту информацию брокерам / оптовикам. Если что-то пойдет не так и программа продолжит работу, она может отправить ошибочные данные о производительности брокерам и оптовикам. Я не знаю ни о ком другом, но я не хочу, чтобы тот, кто сидит в офисе вице-президента, объяснял, почему мой кодекс заставил компанию получить 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
}

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

5 голосов
/ 19 октября 2017

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

Есть несколько сценариев, где исключения являются очевидным выбором :

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

  2. Тем не менее, в ситуации 1 (когда происходит что-то неожиданное и необработанное, вы просто не хотите регистрировать это), исключения могут быть полезны, потому что вы можете добавить контекстную информацию . Например, если я получу исключение SqlException в моих помощниках данных нижнего уровня, я захочу отловить эту ошибку на низкоуровневом уровне (где я знаю команду SQL, которая вызвала ошибку), чтобы я мог получить эту информацию и перебросить с дополнительной информацией. Обратите внимание на волшебное слово здесь: отбрасывать, а не глотать . Первое правило обработки исключений: не глотать исключения . Также обратите внимание, что мой внутренний перехват не должен ничего регистрировать, потому что внешний перехват будет иметь всю трассировку стека и может записать его.

  3. В некоторых ситуациях у вас есть последовательность команд, и если какая-либо из них не срабатывает вам следует очистить / утилизировать ресурсы (*), независимо от того, является ли это неисправимая ситуация (которая должна быть выброшена) или восстанавливаемая ситуация (в этом случае вы можете обрабатывать локально или в коде вызывающей стороны, но вам не нужны исключения). Очевидно, что гораздо проще поместить все эти команды в одну попытку, вместо того, чтобы проверять коды ошибок после каждого метода, а также очищать / удалять в блоке finally. Обратите внимание, что , если вы хотите, чтобы ошибка всплывала (что, вероятно, то, что вам нужно), вам даже не нужно ее ловить - вы просто используете finally для очистки / удаления - вам следует использовать только catch / retrow, если вы хотите добавить контекстную информацию (см. пункт 2).

    Одним из примеров может быть последовательность операторов SQL внутри блока транзакции. Опять же, это также «неуправляемая» ситуация, даже если вы решите поймать ее раньше (обрабатывайте ее локально, а не поднимайтесь до верха), это все равно фатальная ситуация , из которой лучший результат - отменить все или хотя бы прервать большую часть процесса.
    (*) Это похоже на on error goto, который мы использовали в старой Visual Basic

  4. В конструкторах вы можете генерировать только исключения.

Сказав, что во всех других ситуациях, когда вы возвращаете некоторую информацию, по которой вызывающий абонент МОЖЕТ / ДОЛЖЕН предпринять какое-то действие , использование кодов возврата, вероятно, является лучшей альтернативой. Это включает в себя все ожидаемые «ошибки» , потому что, вероятно, они должны быть обработаны непосредственным вызывающим абонентом, и вряд ли нужно будет поднимать слишком много уровней в стеке.

Конечно, всегда можно обрабатывать ожидаемые ошибки как исключения и затем сразу перехватывать их на один уровень выше, а также можно охватить каждую строку кода в попытке перехвата и выполнить действия для каждой возможной ошибки. IMO, это плохой дизайн, не только потому, что он намного более многословен, но и особенно потому, что возможные исключения, которые могут быть выброшены, не очевидны без чтения исходного кода - и исключения могут быть выброшены из любого глубокого метода, создавая невидимые gotos . Они нарушают структуру кода, создавая несколько невидимых точек выхода, которые затрудняют чтение и проверку кода. Другими словами, вы никогда не должны использовать исключений как flow-control , потому что другим будет трудно понять и поддерживать. Может быть даже трудно понять все возможные потоки кода для тестирования.
Еще раз: для правильной очистки / утилизации вы можете использовать try-finally, не ловя ничего .

Самая популярная критика в отношении кодов возврата заключается в том, что «кто-то может игнорировать коды ошибок, но в том же смысле кто-то может также проглотить исключения. Плохая обработка исключений проста в обоих методах. Но написание хорошей программы на основе кода ошибки все еще намного проще, чем написание программы на основе исключения . И если кто-либо по какой-либо причине решит игнорировать все ошибки (старые on error resume next), вы можете легко сделать это с помощью кодов возврата, и вы не сможете сделать это без большого количества шаблонов try-catchs.

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

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

Подводя итог:

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

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

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

Тем не менее, для GO он также позволяет многократные возвращаемые значения , что очень помогает при использовании кодов возврата, поскольку вы можете одновременно возвращать ошибку и что-то еще. На C # / Java мы можем добиться этого без параметров, Tuples или (моего любимого) Generics, которые в сочетании с перечислениями могут предоставить вызывающей стороне четкие коды ошибок:

public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
    ....
    return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");

    ...
    return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}

var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
    // do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
    order = result.Entity; // etc...

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

4 голосов
/ 31 октября 2008

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

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

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