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