Який і чому ви віддаєте перевагу виняткам або кодам повернення?


92

Моє питання полягає в тому, що більшість розробників віддають перевагу обробці помилок, виняткам або кодам повернення помилок. Будь ласка, будьте конкретні щодо мови (або мовної сім’ї) та чому ви віддаєте перевагу одній над іншою.

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

оновлення: дякую за всі відповіді! Потрібно сказати, що, хоча мені не подобається непередбачуваність потоку коду за винятками. Відповідь про код повернення (та їх старший брат обробляє) додає багато коду до шуму.


1
Проблема курки чи яйця зі світу програмного забезпечення ... вічно дискусійна. :)
Гішу

Вибачте за це, але, сподіваюся, наявність різноманітних думок допоможе людям (включаючи мене) вибрати відповідний вибір.
Роберт Гулд,

Відповіді:


107

Для деяких мов (тобто C ++) витік ресурсів не повинен бути причиною

C ++ базується на RAII.

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

Коди повернення більш багатослівні

Вони багатослівні і, як правило, перетворюються на щось на зразок:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Врешті-решт, ваш код - це сукупність ідентифікованих інструкцій (я бачив такий код у виробничому коді).

Цей код цілком можна перекласти такою мовою:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Які чітко відокремлюють обробку коду та помилок, що може бути добре .

Коди повернення більш крихкі

Якщо не якесь неясне попередження від одного компілятора (див. Коментар "phjr"), їх можна легко проігнорувати.

З наведених вище прикладів припустимо, що хтось забув усунути можливу помилку (це трапляється ...). Помилка ігнорується, коли "повертається", і, можливо, вибухне пізніше (тобто покажчик NULL). Ця ж проблема не трапиться за винятком.

Помилка не буде проігнорована. Іноді ви хочете, щоб воно не вибухнуло, проте ... Тож ви повинні вибирати ретельно.

Коди повернення іноді потрібно перекладати

Скажімо, ми маємо такі функції:

  • doSomething, що може повернути int з назвою NOT_FOUND_ERROR
  • doSomethingElse, який може повернути значення bool "false" (для помилки)
  • doSomethingElseAgain, який може повернути об'єкт Помилки (як із змінними __LINE__, __FILE__, так і з половиною стека.
  • doTryToDoSomethingWithAllThisMess, що, ну ... Використовуйте наведені вище функції та повертайте код помилки типу ...

Який тип повернення doTryToDoSomethingWithAllThisMess, якщо одна з його викликаних функцій виходить з ладу?

Коди повернення не є універсальним рішенням

Оператори не можуть повернути код помилки. Конструктори С ++ теж не можуть.

Коди повернення означають, що ви не можете зв'язати вирази

Наслідок вищезазначеного пункту. Що робити, якщо я хочу написати:

CMyType o = add(a, multiply(b, c)) ;

Я не можу, тому що повернене значення вже використовується (а іноді його не можна змінити). Тож повернене значення стає першим параметром, що надсилається як посилання ... Або ні.

Винятки набираються

Ви можете надсилати різні класи для кожного виду виключення. Винятки з ресурсів (тобто нестача пам’яті) повинні бути легкими, але все інше може бути настільки важким, наскільки це потрібно (мені подобається виняток Java, який дає мені весь стек).

Кожен улов може бути спеціалізованим.

Ніколи не використовуйте улов (...) без перекидання

Зазвичай не слід приховувати помилку. Якщо ви не повторите викидання, принаймні, зафіксуйте помилку у файлі, відкрийте вікно повідомлення, що завгодно ...

Виняток становлять ... NUKE

Проблема за винятком полягає в тому, що надмірне їх використання створить код, повний спроб / ловів. Але проблема в іншому: хто намагається / зловити його / її код за допомогою контейнера STL? Проте ці контейнери можуть надсилати виняток.

Звичайно, в C ++ ніколи не дозволяйте винятку вийти з деструктора.

Виняток становлять ... синхронні

Обов’язково зловіть їх, перш ніж вони винесуть вашу нитку на коліна або розповсюдяться всередині циклу повідомлень Windows.

Рішенням може бути їх змішування?

Тож я думаю, що рішення - кинути, коли щось не повинно статися. А коли щось може статися, використовуйте код повернення або параметр, щоб користувач міг реагувати на це.

Отже, питання лише в тому, "що є того, що не повинно відбуватися?"

Це залежить від договору вашої функції. Якщо функція приймає вказівник, але вказує, що вказівник повинен бути не NULL, то нормально видавати виняток, коли користувач надсилає NULL-вказівник (питання в C ++, коли автор функції не використовував замість цього посилання покажчиків, але ...)

Іншим рішенням було б показати помилку

Іноді ваша проблема полягає в тому, що ви не хочете помилок. Використання винятків або кодів повернення помилок - це круто, але ... Ви хочете про це знати.

У моїй роботі ми використовуємо своєрідний «Асерт». Це, залежно від значень конфігураційного файлу, незалежно від параметрів налагодження / випуску компіляції:

  • реєструвати помилку
  • відкрити вікно повідомлення з повідомленням "Гей, у вас проблема"
  • відкрити вікно повідомлень із написом "Гей, у вас проблема, чи не потрібно налагодити"

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

Його легко додати до застарілого коду. Наприклад:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

веде своєрідний код, подібний до:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(У мене є подібні макроси, які активні лише під час налагодження).

Зверніть увагу, що у виробничій версії файл конфігурації не існує, тому клієнт ніколи не бачить результат цього макросу ... Але його легко активувати за потреби.

Висновок

Коли ви кодуєте за допомогою кодів повернення, ви готуєтесь до невдачі і сподіваєтесь, що ваша фортеця тестів достатньо безпечна.

Коли ви кодуєте, використовуючи виняток, ви знаєте, що ваш код може вийти з ладу, і, як правило, ставите контратаку на обрану стратегічну позицію у своєму коді. Але, як правило, ваш код більше стосується того, "що він повинен робити", а потім "того, що я боюся, що станеться".

Але коли ви кодуєте взагалі, ви повинні використовувати найкращий у вашому розпорядженні інструмент, а іноді це: "Ніколи не приховуйте помилку і показуйте її якомога швидше". Макрос, про який я говорив вище, дотримується цієї філософії.


3
Так, АЛЕ щодо вашого першого прикладу, який можна легко записати так: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo

Чому б і ні ... Але тоді, я все одно знаходжу doSomething(); doSomethingElse(); ...краще, тому що якщо мені потрібно додати if / while / тощо. оператори для цілей звичайного виконання, я не хочу, щоб їх змішували з if / while / і т.д. твердження, додані для виняткових цілей ... І оскільки справжнє правило щодо використання винятків - кидати , а не ловити , оператори try / catch зазвичай не є інвазивними.
paercebal

1
Ваш перший пункт показує, в чому проблема винятків. Ваш потік управління стає дивним і відокремлений від фактичної проблеми. Це заміна деяких рівнів ідентифікації каскадом уловів. Я б використовував обидва коди повернення (або повертав об’єкти з важкою інформацією) для можливих помилок та винятків для речей, які не очікуються .
Пітер

2
@Peter Weber: Це не дивно. Він відокремлений від фактичної проблеми, оскільки не є частиною звичайного потоку виконання. Це виняткове виконання. І тоді, знову ж таки, суть про виняток полягає у тому, щоб у випадку виняткової помилки часто кидати і ловити рідко , якщо взагалі. Тому блоки catch рідко навіть з’являються в коді.
paercebal

Приклади такого роду дискусій дуже спрощені. Зазвичай "doSomething ()" або "doSomethingElse ()" насправді виконують щось, наприклад, зміна стану певного об'єкта. Код винятку не гарантує повернення об'єкта в попередній стан і тим більше, коли catch дуже далеко від кидка ... Як приклад, уявіть, doSomething викликається двічі, і збільшуйте лічильник перед киданням. Як ви знаєте, коли ловите виняток, що вам слід зменшувати один-два рази? Загалом, написання безпечного коду винятків для будь-чого, що не є прикладом іграшок, дуже важко (неможливо?).
xryl669

36

Я фактично використовую обидва.

Я використовую коди повернення, якщо це відома, можлива помилка. Якщо це сценарій, який, як я знаю, може і відбудеться, тоді є код, який надсилається назад.

Винятки використовуються виключно для речей, яких я НЕ очікую.


+1 за дуже простий спосіб. Atleast Його досить короткий, щоб я міг прочитати його дуже швидко. :-)
Ashish Gupta

1
" Винятки використовуються виключно для речей, яких я НЕ очікую. " Якщо ви їх не очікуєте, то чому це робити, і як ви можете їх використовувати?
анар халілов

5
Те, що я не очікував, що це станеться, не означає, що я не бачу, як це могло. Я очікую, що мій SQL Server буде ввімкнено та відповідатиме. Але я все ще кодую свої очікування, щоб я міг граціозно вийти з ладу, якщо трапляються несподівані простої.
Стівен Райгтон,

Хіба б невідповідний SQL Server легко класифікувати за категорією "відома, можлива помилка"?
tkburbidge

21

Згідно з главою 7 під назвою "Винятки" у "Рекомендаціях щодо проектування фреймворків: конвенції, ідіоми та шаблони для багаторазових .NET-бібліотек" , наводиться безліч аргументів, чому використання винятків із повернутих значень необхідно для таких фреймворків, як C #.

Можливо, це найбільш вагома причина (стор. 179):

"Винятки добре інтегруються з об'єктно-орієнтованими мовами. Об'єктно-орієнтовані мови, як правило, накладають обмеження на підписи членів, які не нав'язуються функціями в мовах, що не належать до ОО. Наприклад, у випадку конструкторів, перевантажень операторів та властивостей, розробник не має вибору у поверненому значенні. З цієї причини неможливо стандартизувати звітування про помилки на основі поверненого значення для об’єктно-орієнтованих фреймворків. Метод звітування про помилки, такий як винятки, який виходить за межі підпису методу це єдиний варіант ".


10

Моя перевага (у C ++ та Python) полягає у використанні винятків. Забезпечені мовою засоби роблять чітко визначеним процес як збирання, ловлення, так і (за необхідності) повторного викидання винятків, що робить модель легкою для перегляду та використання. Концептуально це чистіше, ніж коди повернення, оскільки конкретні винятки можуть визначатися їх іменами та мати додаткову інформацію, що супроводжує їх. З кодом повернення ви обмежуєтесь лише значенням помилки (якщо ви не хочете визначити об'єкт ReturnStatus або щось інше).

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


2
Пам'ятайте, що використання винятків ускладнює аналіз програм.
Павел Гайдан

7

Винятки слід повертати лише тоді, коли трапляється щось, чого ви не очікували.

Інший момент винятків, що склався історично, полягає в тому, що коди повернення за своєю суттю є власністю, іноді 0 може бути повернуто з функції C, щоб вказати на успіх, іноді -1, або будь-який з них у випадку відмови з 1 для успіху. Навіть коли вони перераховані, перелічення можуть бути неоднозначними.

Винятки також можуть надати набагато більше інформації, а конкретніше чітко прописувати: "Щось пішло не так, ось що, трасування стека та деяка допоміжна інформація для контексту"

З огляду на це, добре перерахований код повернення може бути корисним для відомого набору результатів, простого "ось n результатів функції, і він просто пройшов таким чином"


6

У Java я використовую (у наступному порядку):

  1. Розробка за контрактом (забезпечення виконання передумов перед тим, як спробувати щось, що може зазнати невдачі). Це вловлює більшість речей, і я повертаю код помилки для цього.

  2. Повернення кодів помилок під час обробки (та виконання відкоту, якщо потрібно).

  3. Винятки, але вони використовуються лише для несподіваних речей.


1
Чи не було б трохи правильніше використовувати твердження для контрактів? Якщо контракт порушений, вас нічого не врятує.
Павел Гайдан

@ PawełHajdan, твердження відключені за замовчуванням, я вважаю. Це має таку ж проблему, як і assertречі C, оскільки воно не буде виявляти проблем у виробничому коді, якщо ви не будете постійно працювати з твердженнями. Я схильний розглядати твердження як спосіб виявлення проблем під час розробки, але лише для матеріалів, які будуть стверджуватись чи не затверджуватись послідовно (наприклад, речі з константами, а не речі зі змінними чи щось інше, що може змінитися під час виконання).
paxdiablo

І дванадцять років, щоб відповісти на ваше запитання. Мені слід запустити довідкову службу :-)
paxdiablo

6

Коди повернення проходять для мене тест " Яма успіху " майже щоразу.

  • Надто легко забути перевірити код повернення, а потім помилку червоного оселедця.
  • Коди повернення не містять жодної великої інформації щодо налагодження, як стек викликів, внутрішні винятки.
  • Коди повернення не поширюються, що разом із пунктом вище, як правило, веде надмірне та переплетене діагностичне ведення журналу замість того, щоб вести журнал в одному централізованому місці (обробники винятків рівня програм та потоків).
  • Коди повернення, як правило, створюють безладний код у вигляді вкладених блоків if
  • Час розробника, витрачений на налагодження невідомої проблеми, яка в іншому випадку була б очевидним винятком (яма успіху), коштує дорого.
  • Якби команда, яка стоїть за C #, не мала на меті виключення для управління потоком керування, винятки не вводились би, не було б фільтра "коли" для операторів catch, і не було б потреби в безпараметричному операторі 'throw' .

Щодо продуктивності:

  • Винятками можуть бути обчислювально дорогі РОДИЧНІ, щоб взагалі не кидати, але вони називаються ВИНЯТКАМИ з причини. Швидкість порівняння завжди вдається прийняти 100% виняток, що ніколи не повинно бути. Навіть якщо виняток повільніший у 100 разів, наскільки це насправді має значення, якщо це відбувається лише 1% випадків?
  • Якщо ми не говоримо про арифметику з плаваючою комою для графічних додатків чи щось подібне, цикли процесора дешеві порівняно з часом розробника.
  • Вартість із часової точки зору має той самий аргумент. Порівняно із запитами до бази даних або дзвінками веб-служб або завантаженням файлів, звичайний час програми буде карликовим. Винятки становили майже суб-MICROsecond у 2006 році
    • Я наважуюсь будь-кому, хто працює в .net, налаштувати ваш налагоджувач на всі винятки та вимкнути лише мій код і подивитися, скільки вже відбувається винятків, про які ви навіть не знаєте.

4

Чудова порада, яку я отримав від The Pragmatic Programmer, полягав у тому, що "ваша програма повинна мати можливість виконувати всі основні функції без використання виключень".


4
Ви неправильно трактуєте це. Вони мали на увазі: "якщо ваша програма видає винятки у звичайних потоках, це неправильно". Іншими словами "використовуйте виключення лише для виняткових речей".
Павел Гайдан,

4

Я писав про це в блозі деякий час тому.

Накладні витрати на створення виключення не повинні грати жодної ролі у вашому рішенні. Якщо ви робите це правильно, зрештою, виняток є винятковим .


Пов’язаний допис у блозі більше стосується погляду перед стрибком (перевірки) та простішого прохання пробачення, ніж дозволу (винятки або коди повернення). Я відповів там своїми думками з цього приводу (підказка: TOCTTOU). Але це питання стосується іншого питання, а саме, за яких умов використовувати механізм виключення мови, а не повертати значення зі спеціальними властивостями.
Damian Yerrick

Я цілком згоден. Здається, я чогось навчився за останні дев'ять років;)
Томас

4

Мені не подобаються коди повернення, оскільки вони спричиняють наступний зразок у вашому коді

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Незабаром виклик методу, що складається з 4 викликів функцій, роздувається з 12 рядками обробки помилок .. Деякі з них ніколи не відбудуться. Якщо і переключити випадків достатньо.

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

Якщо у вас є кілька станів після виклику методу, які слід обробляти по-різному (і не є винятковими випадками), використовуйте коди помилок або параметри. Хоча особисто я виявив, що це буває рідко ..

Я трохи полював на контраргумент "покарання за продуктивність" .. більше у світі C ++ / COM, але в нових мовах, я думаю, різниця не така вже й велика. У будь-якому випадку, коли щось підривається, проблеми з продуктивністю відходять на задній план :)


3

З будь-яким гідним компілятором чи середовищем виконання винятки не несуть суттєвого штрафу. Це більш-менш схоже на оператор GOTO, який переходить до обробника винятків. Крім того, наявність винятків, спричинених середовищем виконання (наприклад, JVM), набагато легше ізолювати та виправити помилку. Я візьму NullPointerException у Java за сефайл у C будь-якого дня.


2
Винятки надзвичайно дорогі. Вони повинні пройти стек, щоб знайти потенційних обробників винятків. Ця прогулянка стеком коштує недешево. Якщо побудовано трасування стека, це ще дорожче, оскільки тоді весь стек повинен бути проаналізований.
Дерек Парк

Я здивований, що компілятори не можуть визначити, де виняток буде схоплений принаймні частину часу. Крім того, той факт, що виняток змінює потік коду, полегшує ідентифікацію, де саме виникає помилка, на мій погляд, компенсує покарання за продуктивність.
Kyle Cronin

Стеки викликів можуть бути надзвичайно складними під час виконання, і компілятори, як правило, не роблять такого аналізу. Навіть якщо вони це зробили, вам все одно доведеться пройти стеком, щоб отримати слід. Вам також доведеться розкрутити стек, щоб мати справу з finallyблоками та деструкторами для виділених стеком об'єктів.
Дерек Парк

Я згоден з тим, що переваги налагодження винятків часто компенсують витрати на продуктивність.
Дерек Парк

1
Парк Дерак, виняток, дорогий, коли він трапляється. Ось чому не слід ними зловживати. Але коли цього не відбувається, вони практично нічого не коштують.
paercebal

3

У мене є простий набір правил:

1) Використовуйте коди повернення для речей, на які, як ви очікуєте, зреагує ваш безпосередній абонент.

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

У Java я коли-небудь використовував неперевірені винятки, перевірені винятки в кінцевому підсумку є просто іншою формою коду повернення, і, на мій досвід, подвійність того, що може бути "повернуто" за допомогою виклику методу, як правило, була більше перешкодою, ніж допомогою.


3

Я використовую винятки в python як у виняткових, так і в невиняткових обставинах.

Часто приємно мати можливість використовувати виняток, щоб вказати, що "запит неможливо виконати", на відміну від повернення значення помилки. Це означає, що ви / завжди / знаєте, що повернене значення є правильним типом, а не довільно None або NotFoundSingleton чи щось інше. Ось хороший приклад того, де я вважаю за краще використовувати обробник винятків замість умовного значення, яке повертається.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

Побічним ефектом є те, що при запуску datastore.fetch (obj_id) вам ніколи не потрібно перевіряти, чи повертається його значення None, ви отримуєте цю помилку відразу безкоштовно. Це протилежне аргументу: "Ваша програма повинна мати змогу виконувати всі основні функції без використання виключень".

Ось ще один приклад того, де винятки є "виключно" корисними для написання коду для роботи з файловою системою, яка не підпадає під умови перегонів.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Один системний дзвінок замість двох, відсутність перегонових умов. Це поганий приклад, оскільки, очевидно, це не вдасться з помилкою OSE за більшої кількості обставин, ніж каталог не існує, але це досить хороше рішення для багатьох жорстко контрольованих ситуацій.


Другий приклад - оманливий. Нібито неправильний спосіб є неправильним, оскільки код os.path.rmdir призначений для викиду. Правильна реалізація в потоці поверненого коду буде "if rmdir (...) == FAILED: pass"
MaR

3

Я вважаю, що коди повернення додають шуму коду. Наприклад, я завжди ненавидів вигляд коду COM / ATL через коди повернення. Для кожного рядка коду повинна бути перевірка HRESULT. Я вважаю, що код повернення помилки є одним з неправильних рішень, прийнятих архітекторами COM. Це ускладнює логічне групування коду, тому перегляд коду стає важким.

Я не впевнений у порівнянні продуктивності, коли є явна перевірка коду повернення кожного рядка.


1
COM вата розроблена для використання мовами, які не підтримують винятки.
Кевін

Це добре. Має сенс мати справу з кодами помилок для мов сценаріїв. Принаймні VB6 добре приховує деталі коду помилки, інкапсулюючи їх в об'єкт Err, що дещо допомагає в чистішому коді.
rpattabi

Я не згоден: VB6 реєструє лише останню помилку. У поєднанні з сумнозвісним "при наступному відновленні помилки", ви побачите джерело своєї проблеми до того моменту, коли побачите її. Зверніть увагу, що це основа обробки помилок у Win32 APII (див. Функцію GetLastError)
paercebal

2

Я вважаю за краще використовувати винятки для обробки помилок і повернення значень (або параметрів) як звичайний результат роботи функції. Це дає просту та послідовну схему обробки помилок, і якщо її правильно зробити, це робить набагато чистіший вигляд коду.


2

Однією з великих відмінностей є те, що винятки змушують вас обробляти помилку, тоді як коди повернення помилок можуть не перевірятися.

Коди повернення помилок при інтенсивному використанні також можуть спричинити дуже потворний код із великою кількістю тестів if, подібних до цієї форми:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Особисто я вважаю за краще використовувати винятки для помилок, на які ПОТРІБНО або ПОВИННО діяти викликовий код, і використовувати коди помилок лише для "очікуваних збоїв", коли повернення чогось дійсно є дійсним і можливим.


Принаймні в C / C ++ та gcc ви можете надати функції атрибут, який генеруватиме попередження, якщо її повертане значення ігнорується.
Павел Гайдан,

phjr: Хоча я не згоден із шаблоном "повернути код помилки", ваш коментар, можливо, повинен стати повноцінною відповіддю. Я вважаю це досить цікавим. Принаймні, це дало мені корисну інформацію.
paercebal

2

Є багато причин віддавати перевагу виняткам перед кодом повернення:

  • Зазвичай для читання люди намагаються мінімізувати кількість операторів повернення в методі. Роблячи це, винятки заважають виконувати додаткову роботу, перебуваючи в неналежному стані, і тим самим запобігають потенційному пошкодженню більшої кількості даних.
  • Винятком є, як правило, більш багатослівний арн, який легше розширюється, ніж повертається значення. Припустимо, що метод повертає натуральне число і що ви використовуєте від’ємні числа як код повернення, коли виникає помилка; якщо область дії вашого методу змінюється і тепер повертає цілі числа, вам доведеться змінити всі виклики методу, а не просто трохи налаштовувати виняток.
  • Винятки дозволяють легше відокремити обробку помилок при нормальній поведінці. Вони дозволяють гарантувати, що деякі операції виконуються якось як атомна операція.

2

Винятки не стосуються обробки помилок, IMO. Винятки становлять саме це; виняткові події, яких ви не очікували. Я використовую з обережністю.

Коди помилок можуть бути нормальними, але повернення 404 або 200 із методу - це погано, IMO. Замість цього використовуйте перелічення (.Net), що робить код більш читабельним та простішим у використанні для інших розробників. Також вам не потрібно вести таблицю з числами та описами.

Також; візерунок try-catch-нарешті - це анти-шаблон у моїй книзі. Спробувати-нарешті може бути добре, спробувати-зловити теж може бути добре, але спробувати-зловити-нарешті ніколи не буває добре. try-нарешті часто можна замінити оператором "using" (шаблон IDispose), що краще IMO. І спробуй-лови, де ти насправді ловиш виняток, з яким ти можеш обробляти, це добре, або якщо ти робиш це:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Отже, поки ви дозволяєте винятку продовжувати пускатись, це нормально. Інший приклад:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Тут я фактично займаюся винятком; те, що я роблю поза спробою ловити, коли метод оновлення не вдається, - це вже інша історія, але я думаю, що моя думка була зроблена. :)

Чому тоді спробувати-зловити-нарешті анти-шаблон? Ось чому:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Що станеться, якщо об’єкт db уже закрито? Видано новий виняток, і він повинен бути оброблений! Це краще:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Або, якщо об’єкт db не реалізує IDisposable, зробіть це:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Це мої 2 центи в будь-якому випадку! :)


Буде дивно, якщо об’єкт має .Close (), але не має .Dispose ()
abatishchev

Я використовую лише Close () як приклад. Не соромтеся думати про це як про щось інше. Як я заявляю; слід використовувати шаблон використання (doh!), якщо такий є. Звичайно, це означає, що клас реалізує IDisposable, і як такий ви можете викликати Dispose.
нооцит

1

Я використовую лише винятки, коди повернення відсутні. Я тут кажу про Java.

Загальне правило, якого я дотримуюсь, полягає в тому, що якщо у мене є метод, що викликається, doFoo()то з цього випливає, що якщо він не "робить foo", так би мовити, тоді сталося щось виняткове, і слід створити виняток.


1

Одне я боюся щодо винятків - це те, що викидання винятку призведе до псування коду. Наприклад, якщо ви це робите

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Або ще гірше, що якби я видалив щось, чого не повинен був, але мене кинули зловити, перш ніж зробити решту прибирань. Кидання великої ваги на бідного користувача IMHO.


Для цього призначений вираз <code> нарешті </code>. Але, на жаль, це не в стандарті C ++ ...
Томас

У C ++ ви повинні дотримуватися емпіричного правила "Отримати ресурси у конструкторі та випустити їх у деструкторі. У цьому конкретному випадку auto_ptr буде робити найкраще.
Серж,

Томас, ти помиляєшся. C ++ ще не остаточно, тому що йому це не потрібно. Натомість він має RAII. Рішення Сержа - це одне рішення з використанням RAII.
paercebal

Роберте, скористайся рішенням Сержа, і ти знайдеш, що твоя проблема зникає. Тепер, якщо ви пишете більше спроб / ловів, ніж кидків, то (оцінюючи ваш коментар), можливо, у вас є проблема у коді. Звичайно, використовувати catch (...) без повторного кидка, як правило, погано, оскільки приховує помилку, щоб краще її ігнорувати.
paercebal

1

Моє загальне правило в аргументі виняток проти коду повернення:

  • Використовуйте коди помилок, коли вам потрібна локалізація / інтернаціоналізація - у .NET ви можете використовувати ці коди помилок для посилання на файл ресурсу, який потім відобразить помилку відповідною мовою. В іншому випадку використовуйте винятки
  • Використовуйте винятки лише для помилок, які насправді є винятковими. Якщо це трапляється досить часто, використовуйте логічний код або код помилки переліку.

Немає жодної причини, поки ви не можете використовувати виняток, коли робите l10n / i18n. Винятки можуть містити також локалізовану інформацію.
gizmo

1

Я не вважаю коди повернення менш потворними, ніж винятки. За винятком, у вас є try{} catch() {} finally {}де, як і у вас є коди повернення if(){}. Раніше я боявся винятків з причин, зазначених у дописі; ви не знаєте, чи потрібно очищати вказівник, що у вас. Але я думаю, що у вас є ті самі проблеми, що стосується кодів повернення. Ви не знаєте стану параметрів, якщо не знаєте деяких деталей про функцію / метод, про який йде мова.

Незважаючи на це, ви повинні усунути помилку, якщо це можливо. Ви можете так само легко дозволити винятковій ситуації розповсюдитись на найвищий рівень, як ігнорувати код повернення та дозволити програмі segfault.

Мені подобається ідея повернення значення (перерахування?) Для результатів та виняток у виняткових випадках.


0

Для такої мови, як Java, я б вибрав Exception, оскільки компілятор видає помилку часу компіляції, якщо винятки не обробляються. Це змушує функцію виклику обробляти / кидати виключення.

Щодо Python, я більш суперечливий. Немає компілятора, тому можливо, що абонент не обробляє виняток, викликаний функцією, що призводить до винятків виконання. Якщо ви використовуєте коди повернення, у вас може виникнути несподівана поведінка, якщо неправильно обробляти їх, і якщо ви використовуєте винятки, ви можете отримати винятки під час виконання.


0

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

Цей підхід характерний для мови еліксир.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Люди згадували, що коди повернення можуть призвести до того, що у вас є багато вкладених ifоператорів, але це можна обробити з кращим синтаксисом. В еліксирі,with вираз дозволяє нам легко відокремити ряд повернутих значень щасливого шляху від будь-яких помилок.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

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

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Якщо я, як абонент, знаю, що хочу викликати помилку, якщо запис не вдається, я можу вибрати зателефонувати File.write!замістьFile.write . Або я можу зателефонувати File.writeта по-різному розглянути кожну з можливих причин невдачі.

Звичайно, завжди rescueє виняток, якщо ми цього хочемо. Але порівняно з обробкою інформативного значення повернення, мені здається незручним. Якщо я знаю, що виклик функції може вийти з ладу або навіть повинен провалитися, його збій не є винятковим випадком.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.