Ви повинні використовувати обидва. Річ у тому, щоб вирішити, коли використовувати кожен .
Є декілька сценаріїв, коли винятки є очевидним вибором :
У деяких ситуаціях ви нічого не можете зробити з кодом помилки , і вам просто потрібно обробити його на верхньому рівні в стеку викликів , як правило, просто записуйте помилку, показуючи щось користувачеві або закриваючи програму. У цих випадках коди помилок вимагають від вас підняти коди помилок вручну за рівнем, що, очевидно, набагато простіше зробити за винятками. Справа в тому, що це для несподіваних та незмінних ситуацій.
Але в ситуації 1 (коли трапляється щось несподіване і незмінне, ви просто не хочете це ввійти), винятки можуть бути корисними, оскільки ви можете додати контекстну інформацію . Наприклад, якщо я отримаю SqlException у своїх помічників даних нижчого рівня, я захочу виявити цю помилку на низькому рівні (де я знаю команду SQL, яка викликала помилку), щоб я міг захопити цю інформацію та повторно скинути додаткову інформацію . Зверніть увагу на чарівне слово тут: відкиньте, а не ковтайте .
Перше правило поводження з винятками: не ковтайте винятки . Також зауважте, що в моєму внутрішньому улові нічого не потрібно реєструвати, оскільки зовнішній вилов матиме весь слід стека і може записувати його.
У деяких ситуаціях у вас є послідовність команд, і якщо будь-яка з них виходить з ладу, вам слід очистити / видалити ресурси (*), незалежно від того, це ситуація, яку не можна відновити (яку слід кинути), або ситуацію, що підлягає відновленню (у такому випадку ви можете обробляти локально або в коді абонента, але винятки не потрібні). Очевидно, що набагато простіше поставити всі ці команди в одну спробу, замість тестування кодів помилок після кожного методу та очищення / розпорядження в остаточному блоці. Зауважте, що якщо ви хочете, щоб помилка зростала (що, мабуть, те, що ви хочете), вам навіть не потрібно її вловлювати - ви просто використовуєте нарешті для очищення / знешкодження - вам слід використовувати лише ловлю / повторне використання, якщо ви хочете додати контекстну інформацію (див. пул.2).
Одним із прикладів може бути послідовність операторів SQL всередині блоку транзакцій. Знову ж таки, це також "непридатна" ситуація, навіть якщо ви вирішите зловити її рано (лікуйте її місцево, а не киплячи до верху), це все ще фатальна ситуація, коли найкращим результатом є переривання всього або принаймні переривання великого частина процесу.
(*) Це як те, on error goto
що ми використовували в старому Visual Basic
У конструкторах можна кидати лише винятки.
Сказавши це, у всіх інших ситуаціях, коли ви повертаєте деяку інформацію, за якою абонент МОЖЕ / ВІДКЛАДИТИ вжити певних дій , використовуючи коди повернення, мабуть, краща альтернатива. Сюди входять усі очікувані "помилки" , тому що, ймовірно, з ними повинен працювати негайний абонент, і навряд чи потрібно буде переповнювати занадто багато рівнів в стеку.
Звичайно, завжди можна трактувати очікувані помилки як винятки, а потім вловлювати відразу на один рівень вище, а також можна охопити кожен рядок коду у спробі лову та вжити заходів для кожної можливої помилки. IMO, це поганий дизайн не тільки тому, що він набагато більш багатослівний, але спеціально тому, що можливі винятки, які можуть бути кинуті, не очевидні без читання вихідного коду - і винятки можуть бути викинуті з будь-якого глибокого методу, створюючи невидимі готи . Вони порушують структуру коду, створюючи кілька невидимих точок виходу, які ускладнюють читання та перевірку коду. Іншими словами, ви ніколи не повинні використовувати винятки як контроль потоку, тому що іншим було б важко зрозуміти та підтримати. Це може отримати навіть важко зрозуміти всі можливі потоки коду для тестування.
Знову ж таки: для правильної очистки / утилізації ви можете використовувати спробу, остаточно не вловлюючи нічого .
Найпопулярніша критика щодо повернення кодів полягає в тому, що "хтось міг ігнорувати коди помилок, але в тому ж сенсі хтось також може проковтнути винятки. Погане поводження з винятками легко в обох методах. Але написання хорошої програми на основі коду помилок все ще набагато простіше ніж написання програми, що базується на винятках . І якщо хтось із будь-яких причин вирішить ігнорувати всі помилки (старі on error resume next
), ви можете легко зробити це з кодами повернення, і ви не можете цього зробити без великої кількості спроб-котів.
Друга найпопулярніша критика щодо кодів повернення полягає в тому, що "важко пузирити" - але це тому, що люди не розуміють, що винятки є для ситуацій, що не підлягають відновленню, тоді як коди помилок - ні.
Рішення між винятками та кодами помилок - це сіра область. Можливо навіть, що вам потрібно отримати код помилки з якогось бізнес-методу багаторазового використання, а потім ви вирішите перетворити його на виняток (можливо, додавши інформацію) і дозвольте йому перетворюватися на пульс. Але помилкою в дизайні вважати, що ВСІ помилки слід викидати як винятки.
Підсумовуючи це:
Мені подобається використовувати винятки, коли у мене виникає несподівана ситуація, в якій мало що робити, і зазвичай ми хочемо відмінити великий блок коду або навіть всю операцію чи програму. Це як старий "на помилку goto".
Мені подобається використовувати коди повернення, коли я очікував ситуацій, коли код абонента може / повинен виконати певні дії. Сюди входить більшість бізнес-методів, 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...
Якщо я додаю нове можливе повернення у своєму методі, я навіть можу перевірити всіх абонентів, чи вони покривають це нове значення, наприклад, у операторі перемикача. Ви справді не можете цього зробити за винятком. Використовуючи коди повернення, зазвичай ви заздалегідь знаєте всі можливі помилки та перевіряєте їх. За винятком, ви зазвичай не знаєте, що може статися. Обертання перерахунків всередині винятків (замість Generics) - це альтернатива (до тих пір, поки буде зрозумілий тип винятків, який кожен метод буде викидати), але IMO все одно поганий дизайн.