Вы должны использовать оба. Дело в том, чтобы решить, когда использовать каждый из них .
Есть несколько сценариев, где исключения являются очевидным выбором :
В некоторых ситуациях вы ничего не можете сделать с кодом ошибки , и вам просто нужно обработать его на верхнем уровне в стеке вызовов , обычно просто зарегистрируйте ошибку, покажите что-нибудь пользователю или закройте программу. В этих случаях коды ошибок требуют, чтобы вы кодировали коды ошибок вручную по уровням, что, очевидно, намного проще делать с исключениями. Дело в том, что это для неожиданных и неуправляемых ситуаций.
Тем не менее, в ситуации 1 (когда происходит что-то неожиданное и необработанное, вы просто не хотите регистрировать это), исключения могут быть полезны, потому что вы можете добавить контекстную информацию . Например, если я получу исключение SqlException в моих помощниках данных нижнего уровня, я захочу отловить эту ошибку на низкоуровневом уровне (где я знаю команду SQL, которая вызвала ошибку), чтобы я мог получить эту информацию и перебросить с дополнительной информацией. Обратите внимание на волшебное слово здесь: отбрасывать, а не глотать .
Первое правило обработки исключений: не глотать исключения . Также обратите внимание, что мой внутренний перехват не должен ничего регистрировать, потому что внешний перехват будет иметь всю трассировку стека и может записать его.
В некоторых ситуациях у вас есть последовательность команд, и если какая-либо из них не срабатывает вам следует очистить / утилизировать ресурсы (*), независимо от того, является ли это неисправимая ситуация (которая должна быть выброшена) или восстанавливаемая ситуация (в этом случае вы можете обрабатывать локально или в коде вызывающей стороны, но вам не нужны исключения). Очевидно, что гораздо проще поместить все эти команды в одну попытку, вместо того, чтобы проверять коды ошибок после каждого метода, а также очищать / удалять в блоке finally. Обратите внимание, что , если вы хотите, чтобы ошибка всплывала (что, вероятно, то, что вам нужно), вам даже не нужно ее ловить - вы просто используете finally для очистки / удаления - вам следует использовать только catch / retrow, если вы хотите добавить контекстную информацию (см. пункт 2).
Одним из примеров может быть последовательность операторов SQL внутри блока транзакции. Опять же, это также «неуправляемая» ситуация, даже если вы решите поймать ее раньше (обрабатывайте ее локально, а не поднимайтесь до верха), это все равно фатальная ситуация , из которой лучший результат - отменить все или хотя бы прервать большую часть процесса.
(*) Это похоже на on error goto
, который мы использовали в старой Visual Basic
В конструкторах вы можете генерировать только исключения.
Сказав, что во всех других ситуациях, когда вы возвращаете некоторую информацию, по которой вызывающий абонент МОЖЕТ / ДОЛЖЕН предпринять какое-то действие , использование кодов возврата, вероятно, является лучшей альтернативой. Это включает в себя все ожидаемые «ошибки» , потому что, вероятно, они должны быть обработаны непосредственным вызывающим абонентом, и вряд ли нужно будет поднимать слишком много уровней в стеке.
Конечно, всегда можно обрабатывать ожидаемые ошибки как исключения и затем сразу перехватывать их на один уровень выше, а также можно охватить каждую строку кода в попытке перехвата и выполнить действия для каждой возможной ошибки. 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. Вы действительно не можете сделать это с исключениями. Когда вы используете коды возврата, вы, как правило, заранее знаете все возможные ошибки и проверяете их. За исключением того, что вы обычно не знаете, что может случиться. Обертывание перечислений внутри исключений (вместо универсальных) является альтернативой (если ясно, какой тип исключений будет выдавать каждый метод), но, по-моему, это все еще плохой дизайн.