Соглашения об исключениях или кодах ошибок
Вчера у меня была горячая дискуссия с коллегой о том, какой метод сообщения об ошибках будет предпочтительным. В основном мы обсуждали использование исключений или кодов ошибок для сообщения об ошибках между прикладными уровнями или модулями.
Какие правила вы используете, чтобы решить, будете ли вы генерировать исключения или возвращать коды ошибок для сообщений об ошибках?
22 ответа
Я обычно предпочитаю исключения, потому что они имеют больше контекстной информации и могут передавать (при правильном использовании) ошибку программисту в более ясной форме.
С другой стороны, коды ошибок более легкие, чем исключения, но их сложнее поддерживать. Проверка ошибок может быть случайно исключена. Коды ошибок сложнее поддерживать, потому что вы должны вести каталог со всеми кодами ошибок, а затем включить результат, чтобы увидеть, какая ошибка была выдана. Здесь могут помочь диапазоны ошибок, потому что если единственное, что нас интересует, это наличие ошибки или нет, то ее проще проверить (например, код ошибки HRESULT, больший или равный 0, означает успех и меньше нуля это провал). Они могут случайно быть опущены, потому что нет никакого программного воздействия, которое разработчик будет проверять на наличие ошибок. С другой стороны, вы не можете игнорировать исключения.
Подводя итог, я предпочитаю исключения кодам ошибок почти во всех ситуациях.
В материалах высокого уровня, исключения; в низкоуровневые вещи, коды ошибок.
Поведение исключения по умолчанию состоит в том, чтобы разматывать стек и останавливать программу, если я пишу сценарий, и я обращаюсь к ключу, которого нет в словаре, это, вероятно, ошибка, и я хочу, чтобы программа остановилась и позволила мне знать все об этом.
Однако, если я пишу кусок кода, который должен знать поведение в каждой возможной ситуации, тогда я хочу коды ошибок. В противном случае я должен знать каждое исключение, которое может быть выдано каждой строкой в моей функции, чтобы знать, что он будет делать (Прочтите Исключение, которое основало авиакомпанию, чтобы понять, насколько это сложно). Написание кода, который соответствующим образом реагирует на каждую ситуацию (включая и несчастные), утомителен и труден, но это потому, что написание безошибочного кода утомительно и сложно, а не потому, что вы передаете коды ошибок.
И Раймонд Чен, и Джоэл приводили красноречивые аргументы против использования исключений для всего.
Коды ошибок могут игнорироваться (и часто таковыми являются!) Вызывающими вашими функциями. Исключения, по крайней мере, заставляют их как-то справляться с ошибкой. Даже если их версия имеет дело с этим - иметь пустой обработчик catch (вздох).
Я предпочитаю исключения, потому что
- они прерывают поток логики
- они получают выгоду от иерархии классов, которая дает больше возможностей / функциональности
- при правильном использовании может представлять широкий спектр ошибок (например, InvalidMethodCallException также является LogicException, поскольку оба они возникают, когда в вашем коде есть ошибка, которую следует обнаружить до выполнения), и
- они могут быть использованы для исправления ошибки (т. е. определение класса FileReadException может затем содержать код для проверки, существует ли файл или заблокирован и т. д.)
Исключения из кодов ошибок, без сомнения об этом. Вы получаете те же преимущества от исключений, что и с кодами ошибок, но и гораздо больше, без недостатков кодов ошибок. Единственный стук в исключениях состоит в том, что это немного больше накладных расходов; но в наше время эти накладные расходы следует считать незначительными почти для всех приложений.
Вот несколько статей, обсуждающих, сравнивающих и противопоставляющих две техники:
Есть несколько хороших ссылок в тех, которые могут дать вам дальнейшее чтение.
Я бы никогда не смешал две модели... слишком сложно конвертировать из одной в другую, когда вы переходите от одной части стека, использующей коды ошибок, к более высокой части, использующей исключения.
Исключения составляют "все, что останавливает или запрещает методу или подпрограмме выполнять то, что вы просили сделать" ... НЕ передавать сообщения о нарушениях, необычных обстоятельствах, состоянии системы и т. Д. Используйте возвращаемые значения или ref. (или нет) параметры для этого.
Исключения позволяют писать (и использовать) методы с семантикой, которая зависит от функции метода, то есть метод, который возвращает объект Employee или List of Employees, может быть набран именно для этого, и вы можете использовать его, вызывая.
Employee EmpOfMonth = GetEmployeeOfTheMonth();
При использовании кодов ошибок все методы возвращают код ошибки, поэтому для тех, кому необходимо возвращать что-то еще для использования вызывающим кодом, необходимо передать ссылочную переменную, которая будет заполнена этими данными, и проверить возвращаемое значение для код ошибки и обрабатывать его при каждом вызове функции или метода.
Employee EmpOfMonth;
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
// code to Handle the error here
Если вы кодируете так, что каждый метод выполняет одну-единственную простую вещь, вы должны генерировать исключение всякий раз, когда метод не может достичь желаемой цели метода. Исключения намного богаче и проще в использовании, чем коды ошибок. Ваш код намного чище - стандартный поток "нормального" пути кода может быть строго посвящен случаю, когда метод способен выполнить то, что вы хотели, а затем выполнить код для очистки или обработки "исключительные" обстоятельства, когда происходит что-то плохое, что мешает успешному завершению метода, могут быть отделены от обычного кода. Кроме того, если вы не можете обработать исключение в том месте, где оно произошло, и должны передать его по стеку в пользовательский интерфейс (или, что еще хуже, через провод от компонента среднего уровня к пользовательскому интерфейсу), то с моделью исключения вы не нужно кодировать каждый промежуточный метод в вашем стеке, чтобы распознать и передать исключение в стек... Модель исключений делает это для вас автоматически... С кодами ошибок эта часть головоломки может очень быстро обременительно,
Вы должны использовать оба. Дело в том, чтобы решить, когда использовать каждый из них.
Есть несколько сценариев, где исключения являются очевидным выбором:
В некоторых ситуациях вы ничего не можете сделать с кодом ошибки, и вам просто нужно обработать его на верхнем уровне в стеке вызовов, обычно просто регистрировать ошибку, отображать что-то для пользователя или закрывать программу. В этих случаях коды ошибок потребуют от вас всплывать коды ошибок вручную уровень за уровнем, что, очевидно, гораздо проще сделать с исключениями. Дело в том, что это для неожиданных и неуправляемых ситуаций.
Тем не менее, в ситуации 1 (когда происходит что-то неожиданное и необработанное, вы просто не хотите регистрировать это), исключения могут быть полезны, потому что вы можете добавить контекстную информацию. Например, если я получаю исключение SqlException в моих помощниках данных нижнего уровня, я захочу отловить эту ошибку на низкоуровневом уровне (где я знаю команду SQL, которая вызвала ошибку), чтобы я мог получить эту информацию и перезапустить ее с дополнительной информацией., Пожалуйста, обратите внимание на волшебное слово здесь: отбрасывать, а не глотать. Первое правило обработки исключений: не глотайте исключения. Кроме того, обратите внимание, что мой внутренний перехват не должен ничего регистрировать, потому что внешний перехват будет иметь трассировку всего стека и может записать его.
В некоторых ситуациях у вас есть последовательность команд, и если какая-либо из них не сработает, вы должны очистить / утилизировать ресурсы (*), независимо от того, является ли это неисправимой ситуацией (которая должна быть выброшена) или восстанавливаемой ситуацией (в этом случае вы можете обрабатывать локально или в коде вызывающей стороны, но вам не нужны исключения). Очевидно, что гораздо проще поместить все эти команды в одну попытку, вместо того, чтобы проверять коды ошибок после каждого метода, а также выполнять очистку / удаление в блоке finally. Обратите внимание, что если вы хотите, чтобы ошибка всплывала (что, вероятно, и нужно), вам даже не нужно ее ловить - вы просто используете finally для очистки / удаления - вы должны использовать только catch/retrow, если хотите добавить контекстную информацию (см. пункт 2).
Одним из примеров может быть последовательность операторов SQL внутри блока транзакции. Опять же, это также "неуправляемая" ситуация, даже если вы решили поймать это рано (лечите это локально, вместо того, чтобы подниматься до верха), это все еще фатальная ситуация, когда лучшим результатом является прерывание всего или, по крайней мере, прерывание большого часть процесса.
(*) Это какon error goto
что мы использовали в старой Visual BasicВ конструкторах вы можете генерировать только исключения.
Сказав это, во всех других ситуациях, когда вы возвращаете некоторую информацию, по которой вызывающая сторона МОЖЕТ / ДОЛЖНА предпринять какое-либо действие, использование кодов возврата, вероятно, является лучшей альтернативой. Это включает в себя все ожидаемые "ошибки", потому что, вероятно, они должны быть обработаны непосредственным вызывающим абонентом, и вряд ли нужно будет поднимать слишком много уровней в стеке.
Конечно, всегда можно трактовать ожидаемые ошибки как исключения и затем сразу перехватывать их на один уровень выше, а также можно охватить каждую строку кода в попытке перехвата и выполнить действия для каждой возможной ошибки. IMO, это плохой дизайн, не только потому, что он намного более многословен, но особенно потому, что возможные исключения, которые могут быть выброшены, не очевидны без чтения исходного кода - и исключения могут быть выброшены из любого глубокого метода, создающего невидимые gotos. Они нарушают структуру кода, создавая несколько невидимых точек выхода, которые затрудняют чтение и проверку кода. Другими словами, вы никогда не должны использовать исключения в качестве управления потоком, потому что это будет трудно понять и поддерживать другим. Может быть даже трудно понять все возможные потоки кода для тестирования.
Опять же: для правильной очистки / утилизации вы можете использовать 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. Вы действительно не можете сделать это с исключениями. Когда вы используете коды возврата, вы обычно заранее знаете все возможные ошибки и проверяете их. За исключением того, что вы обычно не знаете, что может случиться. Обертывание перечислений внутри исключений (вместо универсальных) является альтернативой (при условии, что ясно, какой тип исключений будет генерировать каждый метод), но IMO - это все еще плохой дизайн.
Может быть несколько ситуаций, когда использование исключений чистым, ясным и правильным способом является обременительным, но подавляющее большинство исключений по времени являются очевидным выбором. Самая большая выгода от обработки исключений по сравнению с кодами ошибок заключается в том, что она меняет поток выполнения, что важно по двум причинам.
Когда возникает исключение, приложение больше не следует своему "нормальному" пути выполнения. Первая причина, по которой это так важно, заключается в том, что если автор кода не сделает все возможное и действительно плохое, программа остановится и не будет продолжать делать непредсказуемые вещи. Если код ошибки не проверяется и соответствующие действия не предпринимаются в ответ на неверный код ошибки, программа продолжит делать то, что она делает, и кто знает, каков будет результат этого действия. Есть много ситуаций, когда выполнение программы "что угодно" может оказаться очень дорогим. Рассмотрим программу, которая получает информацию об эффективности различных финансовых инструментов, которые продает компания, и передает эту информацию брокерам / оптовым торговцам. Если что-то пойдет не так и программа продолжит работу, она может отправить ошибочные данные о производительности брокерам и оптовикам. Я не знаю ни о ком другом, но я не хочу, чтобы тот, кто сидит в офисе вице-президента, объяснял, почему мой кодекс заставил компанию получить 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
}
Есть и другие мелочи об исключениях, которые тоже хороши. Наличие множества условной логики для отслеживания того, возвращен ли какой-либо из методов, вызываемых в функции, код ошибки, и возврат этого кода ошибки выше, является большой проблемой. На самом деле, много котельной плиты может пойти не так. У меня гораздо больше веры в систему исключений большинства языков, чем у крыс, которые делают заявления "если-еще-если-еще", написанные Фредом "только что из колледжа", и у меня есть куда более полезные дела с моим временем, чем обзор кода сказал крысиное гнездо.
В прошлом я присоединился к лагерю кодов ошибок (слишком много занимался программированием на Си). Но теперь я увидел свет.
Да, исключения являются небольшим бременем для системы. Но они упрощают код, уменьшая количество ошибок (и WTF).
Так что используйте исключения, но используйте их мудро. И они будут твоим другом.
Как примечание стороны. Я научился документировать, какое исключение может быть сгенерировано каким способом. К сожалению, это не требуется большинством языков. Но это увеличивает вероятность обработки правильных исключений на правильном уровне.
Мой подход заключается в том, что мы можем использовать оба, то есть коды исключений и ошибок одновременно.
Я привык определять несколько типов исключений (например, 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;
}
Таким образом, я использую исключения, чтобы определить ошибки категории, и коды ошибок, чтобы определить более подробную информацию о проблеме.
Поскольку я работаю с C++ и имею RAII, чтобы сделать их безопасными для использования, я использую исключения почти исключительно. Это вытягивает обработку ошибок из нормального потока программы и делает намерение более ясным.
Я оставляю исключения для исключительных обстоятельств. Если я ожидаю, что определенная ошибка случится много, я проверю, что операция будет выполнена успешно, прежде чем выполнить ее, или вызову версию функции, которая вместо этого использует коды ошибок (например, TryParse()
)
Я могу сидеть здесь на заборе, но...
- Это зависит от языка.
- Какую бы модель вы ни выбрали, будьте последовательны в том, как вы ее используете.
В Python использование исключений является стандартной практикой, и я очень рад определить свои собственные исключения. В Си у вас нет исключений вообще.
В C++ (по крайней мере, в STL) исключения генерируются только для действительно исключительных ошибок (я сам их практически никогда не вижу). Я не вижу причин делать что-то другое в моем собственном коде. Да, легко игнорировать возвращаемые значения, но C++ также не заставляет вас перехватывать исключения. Я думаю, вы просто должны привыкнуть к этому.
Основой кода, над которой я работаю, в основном является C++, и мы используем коды ошибок почти везде, но есть один модуль, который вызывает исключения для любых ошибок, в том числе очень необычных, и весь код, который использует этот модуль, довольно ужасен. Но это может быть только потому, что мы смешали исключения и коды ошибок. Код, который последовательно использует коды ошибок, намного проще работать. Если бы в нашем коде последовательно использовались исключения, возможно, это было бы не так плохо. Смешение этих двух не очень хорошо работает.
Я рассуждаю так: если вы пишете низкоуровневый драйвер, который действительно нуждается в производительности, используйте коды ошибок. Но если вы используете этот код в приложении более высокого уровня, и он может справиться с небольшим объемом служебной информации, то оберните этот код интерфейсом, который проверяет эти коды ошибок и вызывает исключения.
Во всех других случаях, исключения, вероятно, путь.
Подписи метода должны сообщать вам, что делает метод. Что-то вроде long errorCode = getErrorCode(); может быть хорошо, но long errorCode = fetchRecord(); сбивает с толку.
Во-первых, я согласен с ответом Тома, что для высокоуровневых вещей используются исключения, а для низкоуровневых - коды ошибок, если это не сервис-ориентированная архитектура (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
};
}
Когда они последовательно используются в ваших ответах на сервисы, это создает очень хороший шаблон обработки успеха / сбоев в приложении. Это позволяет упростить обработку ошибок при асинхронных вызовах внутри служб, а также между службами.
Исключения составляют исключительные обстоятельства, т. Е. Когда они не являются частью нормального потока кода.
Вполне допустимо смешивать исключения и коды ошибок, где коды ошибок представляют состояние чего-либо, а не ошибку при выполнении кода как такового (например, проверка кода возврата от дочернего процесса).
Но когда возникает исключительное обстоятельство, я считаю, что исключения являются наиболее выразительной моделью.
В некоторых случаях вы можете предпочесть или иметь возможность использовать коды ошибок вместо исключений, и они уже были адекватно рассмотрены (кроме других очевидных ограничений, таких как поддержка компилятора).
Но если вы пойдете в другом направлении, использование исключений позволит вам создавать абстракции более высокого уровня для обработки ошибок, которые могут сделать ваш код еще более выразительным и естественным. Я очень рекомендую прочитать эту превосходную, но недооцененную статью эксперта по С ++ Андрея Александреску на тему того, что он называет "Принуждения": http://www.ddj.com/cpp/184403864. Хотя это статья на C++, принципы в целом применимы, и я довольно успешно перевел концепцию принудительного исполнения на C#.
Если вы работаете в большом проекте, вы не можете использовать только исключения или только коды ошибок. В разных случаях вы должны использовать разные подходы.
Например, вы решили использовать только исключения. Но как только вы решите использовать асинхронную обработку событий. Это плохая идея использовать исключения для обработки ошибок в этих ситуациях. Но использовать коды ошибок везде в приложении утомительно.
Поэтому мое мнение, что это нормально - использовать исключения и коды ошибок одновременно.
Для большинства приложений исключения лучше. Исключением является случай, когда программное обеспечение должно взаимодействовать с другими устройствами. Домен, в котором я работаю, является промышленным контролем. Здесь коды ошибок являются предпочтительными и ожидаемыми. Поэтому я отвечаю, что это зависит от ситуации.
Я бы предпочел исключения для всех случаев ошибок, за исключением случаев, когда сбой является ожидаемым безошибочным результатом функции, которая возвращает примитивный тип данных. Например, поиск индекса подстроки в большей строке обычно возвращает -1, если не найден, вместо вызова NotFoundException.
Возвращение недопустимых указателей, которые могут быть разыменованы (например, вызывает исключение NullPointerException в Java), не допускается.
Использование нескольких различных числовых кодов ошибок (-1, -2) в качестве возвращаемых значений для одной и той же функции обычно является плохим стилем, поскольку клиенты могут выполнить проверку "== -1" вместо "< 0".
Здесь нужно помнить об эволюции API со временем. Хороший API позволяет изменять и расширять поведение при сбоях несколькими способами, не нарушая работу клиентов. Например, если дескриптор ошибки клиента проверен на 4 случая ошибки, и вы добавляете пятое значение ошибки в свою функцию, обработчик клиента может не проверить это и не сломать. Если вы возбуждаете исключения, это обычно упрощает миграцию клиентов на более новую версию библиотеки.
Еще одна вещь, которую следует учитывать при работе в команде, где провести четкую линию для всех разработчиков, чтобы принять такое решение. Например, "Исключения для вещей высокого уровня, коды ошибок для вещей низкого уровня" очень субъективны.
В любом случае, когда возможно более одного тривиального типа ошибки, исходный код никогда не должен использовать числовой литерал для возврата кода ошибки или для его обработки (возврат -7, если x == -7 ...), но всегда именованная константа (верните NO_SUCH_FOO, если x == NO_SUCH_FOO) .
Мое общее правило:
- В функции может появиться только одна ошибка: используйте код ошибки (как параметр функции)
- Может появиться более одной конкретной ошибки: сгенерировать исключение
Я думаю, что это также зависит от того, действительно ли вам нужна информация, такая как трассировка стека от результата. Если да, вы обязательно выберете Исключение, которое предоставляет объект, полный информации о проблеме. Однако, если вас просто интересует результат, и вам все равно, зачем этот результат, перейдите к коду ошибки.
Например, когда вы обрабатываете файл и сталкиваетесь с IOException, клиент может быть заинтересован в том, чтобы узнать, откуда это было инициировано, в открытии файла или в синтаксическом анализе файла и т. д. Так что лучше возвращать IOException или его конкретный подкласс. Тем не менее, в сценарии, как у вас есть метод входа в систему, и вы хотите знать, был ли он успешным или нет, либо вы просто возвращаете логическое значение, либо для отображения правильного сообщения возвращаете код ошибки. Здесь Клиенту не интересно знать, какая часть логики вызвала этот код ошибки. Он просто знает, является ли его учетная запись недействительной или блокировка учетной записи и т. Д.
Еще один случай использования, о котором я могу подумать, - это когда данные перемещаются по сети. Ваш удаленный метод может вернуть только код ошибки вместо исключения, чтобы минимизировать передачу данных.
Коды ошибок также не работают, когда ваш метод возвращает что-либо кроме числового значения...