Чому винятки слід використовувати консервативно?


80

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

Існує цілий ряд проблем, для вирішення яких може бути використаний виняток. Чому нерозумно використовувати їх для управління потоком? У чому полягає філософія винятково консервативного способу їх використання? Семантика? Продуктивність? Складність? Естетика? Конвенція?

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

Знову ж таки, я не обов'язково не погоджуюсь з тим, що їх слід зберігати за особливих обставин, але мені цікаво, що таке обґрунтування консенсусу (якщо таке існує).



32
Не дублікат. Пов’язаний приклад стосується того, чи корисна обробка винятків взагалі. Мова йде про різницю між тим, коли використовувати винятки та коли використовувати інший механізм звітування про помилки.
Адріан Маккарті

1
а як щодо цього одного stackoverflow.com/questions/1385172/… ?
stijn

1
Обґрунтування консенсусу не існує. Різні люди мають різні думки щодо "доцільності" кидання винятків, і на ці думки, як правило, впливає мова, якою вони розвиваються. Ви позначили це запитання С ++, але я підозрюю, що якщо ви позначите це тегом як Java, ви отримаєте різні думки.
Charles Salvia

5
Ні, це не повинна бути вікі. Відповіді тут вимагають досвіду і повинні бути винагороджені представниками.
bobobobo

Відповіді:


92

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

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

Відповідаючи безпосередньо на ваше запитання. Рідко слід використовувати винятки, оскільки виняткові ситуації трапляються рідко, а винятки дорогі.

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

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

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

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

Чому нерозумно використовувати їх для управління потоком?

Оскільки винятки порушують нормальний "потік управління". Ви викликаєте виняток, і звичайне виконання програми відмовляється від потенційно залишаючих об’єктів у несумісному стані, а деякі відкриті ресурси звільняються. Звичайно, у C # є оператор using, який гарантує, що об'єкт буде утилізований, навіть якщо виключення буде видалено з використовуючого тіла. Але на даний момент абстрагуймося від мови. Припустимо, що фреймворк не розпоряджається об’єктами для вас. Ви робите це вручну. У вас є певна система запитів та звільнення ресурсів та пам’яті. Ви маєте домовленості по всій системі, хто в яких ситуаціях відповідає за звільнення об’єктів та ресурсів. У вас є правила щодо роботи з зовнішніми бібліотеками. Це чудово працює, якщо програма слідує нормальному робочому потоку. Але раптом в середині страти ви кидаєте виняток. Половина ресурсів залишається вільною. Половина ще не отримала запит. Якщо операція повинна була бути транзакційною, то зараз вона порушена. Ваші правила поводження з ресурсами не працюватимуть, оскільки ті частини коду, що відповідають за звільнення ресурсів, просто не виконуються. Якщо хтось інший бажає використовувати ці ресурси, він може виявити їх у непослідовному стані та також зірватися, оскільки він не міг передбачити цю конкретну ситуацію.

Скажімо, ви хотіли, щоб метод M () викликав метод N (), щоб виконати якусь роботу і домовитись про якийсь ресурс, а потім повернути його назад у M (), який буде ним користуватися, а потім розпоряджатися ним. Прекрасно. Тепер у N () щось піде не так, і воно викидає виняток, якого ви не очікували в M (), тому виняток пузириться доверху, поки він, можливо, не потрапить у якийсь метод C (), який не матиме уявлення про те, що відбувалося глибоко внизу в N () та чи і як звільнити деякі ресурси.

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


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

9
@Catskul: не обов'язково. Повернення функції повертає управління безпосередньо безпосередньому абоненту. Викинутий виняток повертає керування першим обробником catch для цього типу винятків (або його базового класу або "...") у цілому поточному стеку викликів.
jon-hanson

5
Слід також зазначити, що налагоджувачі часто за замовчуванням ламаються на винятки. Налагоджувач сильно зламається, якщо ви використовуєте винятки для умов, які не є незвичними.
Qwertie

5
@jon: Це сталося б лише тоді, коли б його не спіймали і потрібно було б уникнути більшої кількості областей, щоб досягти обробки помилок. У цьому ж випадку, використовуючи повернені значення, (а також потребують передачі через кілька областей) відбудеться однакова кількість розмотування стека.
Catskul

3
-1 вибачте. Безглуздо говорити про те, для чого "призначені" виняткові об'єкти. Вони є лише механізмом, який можна використовувати для вирішення виняткових ситуацій, таких як закінчення пам'яті тощо - але це не означає, що їх не слід використовувати також для інших цілей. Я хочу побачити пояснення, чому їх не слід використовувати для інших цілей. Незважаючи на те, що ваша відповідь про це говорить трохи, вона змішується з великою кількістю розмов про те, для чого "призначені" винятки.
j_random_hacker

61

Хоча "кинути винятки у виняткових обставинах" є простою відповіддю, ви насправді можете визначити, якими є ці обставини: коли передумови виконуються, але пості умови не можуть бути виконані . Це дозволяє писати жорсткіші, жорсткіші та корисніші умови, не жертвуючи обробкою помилок; інакше, без винятків, вам доведеться змінити постумова, щоб врахувати всі можливі стани помилок.

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

Конструктори

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

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

Приклад зомбі

Прикладом цієї модифікації зомбі є std :: ifstream , і перед використанням ви завжди повинні перевіряти його стан. Оскільки std :: string , наприклад, ні, ви завжди гарантуєте, що зможете використовувати його відразу після побудови. Уявіть, якби вам довелося писати код, такий як цей приклад, і якщо ви забули перевірити стан зомбі, ви б або мовчки отримували неправильні результати, або пошкоджували інші частини вашої програми:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

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

Приклад перевірки введення

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

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

На жаль, це показує два приклади того, як масштабування C ++ вимагає зламу RAII / SBRM. Приклад у Python, який не має цієї проблеми і показує те, що я хотів би, щоб C ++ мав - try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Передумови

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

Зокрема, порівняйте гілки std :: logic_error та std :: runtime_error ієрархії винятків stdlib. Перший часто використовується для порушення попередніх умов, тоді як другий більше підходить для порушень, що передують умовам.


5
Пристойна відповідь, але ви в основному просто заявляєте правило, а не надаєте обґрунтування. Тоді ваш розум: умовність та стиль?
Catskul

6
+1. Ось так були розроблені винятки, і використання їх таким чином насправді покращує читабельність та повторне використання коду (IMHO).
Даніель Приден

15
Кацкуль: перше - це визначення виняткових обставин, а не обгрунтування та домовленість. Якщо ви не можете гарантувати дотримання умов, ви не можете повернутися . Без винятків, ви повинні зробити посткондуцію надзвичайно широкою, щоб включити всі стани помилок до такої міри, що вона практично марна. Схоже, я не відповів на ваше буквальне запитання "чому вони повинні бути рідкісними", бо я не дивлюся на них так. Вони є інструментом, який дозволяє мені корисно посилити посткондукції, одночасно допускаючи помилки (..за винятковими обставинами :)

9
+1. Умова до / після - найкраще пояснення, яке я коли-небудь чув.
KitsuneYMG

6
Багато людей говорять, "але це зводиться лише до умовності / семантики". Так, саме так. Але умовність та семантика мають значення, оскільки вони мають глибокі наслідки для складності, зручності та ремонтопридатності. Зрештою, хіба рішення про те, чи слід формально визначати передумови та післяумови, також не є лише питанням умовності та семантики? І все ж їх використання може значно спростити використання та обслуговування коду.
Дарріл,

40
  1. Дорогі
    дзвінки ядра (або інші системні виклики API) для управління інтерфейсами сигналу ядра (системи)
  2. Важко проаналізувати
    Багато проблемgotoтвердження стосуються винятків. Вони перескакують потенційно велику кількість коду, часто в декількох процедурах та вихідних файлах. Це не завжди видно з читання проміжного вихідного коду. (Це на Java.)
  3. Не завжди передбачений проміжним кодом
    Код, який перестрибується, може бути написаний чи не написаний з урахуванням можливості виходу з винятку. Якщо спочатку так було написано, можливо, це не підтримувалося з урахуванням цього. Подумайте: витоки пам'яті, витоки дескрипторів файлів, витоки сокетів, хто знає?
  4. Ускладнення технічного обслуговування Складніше
    підтримувати код, який стрибає навколо обробки винятків.

24
+1, загалом вагомі причини. Але зверніть увагу, що вартість розгортання стека та виклик деструкторів (принаймні) оплачується будь-яким функціонально еквівалентним механізмом обробки помилок, наприклад, тестуванням та поверненням кодів помилок, як у C.
j_random_hacker

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

14
Жульєн: коментар j_random зазначає, що немає порівняльної економії: int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... }Ви повинні заплатити, щоб розкрутити стек, незалежно від того, чи робите ви це вручну чи через винятки. Ви не можете порівняти з використанням винятків не обробки помилок взагалі .

14
1. Дорого: передчасна оптимізація - корінь усього зла. 2. Важко проаналізувати: порівняно з вкладеними шарами коду повернення помилки? Я з повагою не погоджуюсь. 3. Не завжди передбачений проміжним кодом: порівняно з вкладеними шарами, які не завжди обробляють помилки та перекладають їх розумно? Новачки зазнають невдачі в обох. 4. Ускладнення технічного обслуговування: як? Чи легше підтримувати залежності, опосередковані кодами помилок? ... але це, здається, одна з тих сфер, де розробники будь-якої школи важко сприймають аргументи один одного. Як і будь-що інше, дизайн обробки помилок - це компроміс.
Pontus Gagge

1
Я б погодився, що це складна справа, і що важко відповісти точно загальними словами. Але майте на увазі, що в оригінальному запитанні просто запитувалося, чому давалася порада проти винятку, а не для пояснення загальної найкращої практики.
DigitalRoss,

22

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


5
Як можна використовувати виняток для будь-яких цілей, крім контролю потоку? Навіть у винятковій ситуації вони все ще є механізмом контролю потоку, що має наслідки для ясності вашого коду (передбачається тут: незрозуміло). Я гадаю, ви могли б використовувати винятки, але заборонити catch, хоча в такому випадку ви майже могли б просто подзвонити std::terminate()собі. Загалом, мені здається, що цей аргумент говорить "ніколи не використовуй винятки", а не "використовуй виключення рідко".
Steve Jessop

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

2
Стів: Я розумію вашу думку, але це не те, що я мав на увазі. Винятки - це нормально для несподіваних ситуацій. Деякі люди зловживають ними як "раннє повернення", можливо, навіть як якесь твердження про перехід.
Еріх Кіцмюллер,

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

2
Плюс, звичайно, обгортки можуть перетворити функцію метання у функцію, що повертає помилку, або навпаки. Отже, якщо ваш абонент не погоджується з вашою ідеєю "несподіваного", це не обов'язково порушує зрозумілість їх коду. Це може пожертвувати деяким виступом, коли вони провокують безліч умов, які, на вашу думку, є "несподіваними", але вони вважають, що це нормально і підлягає відновленню, а отже, ловить і перетворює на помилку чи щось інше.
Steve Jessop

16

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

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

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

Java має більш широке визначення поняття "справді винятковий", ніж C ++. Програмісти на C ++, швидше за все, хочуть подивитися на значення повернення функції, ніж програмісти Java, тому в Java "дійсно винятковий" може означати "Я не можу повернути ненульовий об'єкт як результат цієї функції". У C ++ це, швидше за все, означає "Я дуже сумніваюся, що мій абонент може продовжити". Тож потік Java кидає, якщо не може прочитати файл, тоді як потік C ++ (за замовчуванням) повертає значення, що вказує на помилку. Однак у всіх випадках справа в тому, який код ви готові змусити свого абонента писати. Отже, це справді питання стилю кодування: ви повинні досягти консенсусу, як повинен виглядати ваш код, і скільки коду "перевірки помилок" ви хочете написати проти того, наскільки "безпека винятків"

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

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


2
"Наприклад, у C ++, вам потрібно додатково продумати, щоб переконатися, що ваші функції надійно захищені від винятків, ніж це було б потрібно робити, якби їм не потрібно було." - враховуючи, що основні конструкції C ++ (такі як new) та стандартна бібліотека викидають винятки, я не бачу, як невикористання винятків у вашому коді позбавляє вас від написання безпечного для винятків коду незалежно.
Павло Мінаєв

1
Вам доведеться запитати Google. Але так, якщо взяти до уваги, якщо вашу функцію не заборонено, то вашим абонентам доведеться виконати певну роботу, а додавання додаткових умов, які спричиняють винятки, не робить її "більш винятковою". Але відсутність пам’яті в будь-якому випадку є особливим випадком. Поширені програми, які не справляються з цим, і замість цього просто вимикаються. Ви можете мати стандарт кодування, який говорить, "не використовуйте винятки, не дайте гарантій винятків, якщо нові викидає, ми припиняємо роботу, і ми будемо використовувати компілятори, які не розмотують стек". Не високий концепт, але він би летів.
Steve Jessop

Як тільки ви використовуєте компілятор, який не розмотує стек (або іншим чином відключає в ньому винятки), технічно кажучи, це вже не C ++ :)
Павло Мінаєв

Я маю на увазі не розмотувати стек на невпійманому винятку.
Steve Jessop

Звідки він знає, що його не спіймали, якщо він не розмотує стек, щоб знайти блок блокування?
jmucchiello

11

Насправді немає консенсусу. Весь випуск є дещо суб’єктивним, оскільки „доцільність” подання винятку часто пропонується існуючими практиками стандартної бібліотеки самої мови. Стандартна бібліотека C ++ видає винятки набагато рідше, ніж, скажімо, стандартна бібліотека Java, яка майже завжди надає перевагу виняткам, навіть для очікуваних помилок, таких як недійсне введення користувачем (наприклад Scanner.nextInt). Я вважаю, це суттєво впливає на думки розробників щодо того, коли доцільно застосовувати виняток.

Як програміст на C ++, я особисто вважаю за краще зарезервувати винятки для дуже "виняткових" обставин, наприклад, з-за нестачі пам'яті, місця на диску, апокаліпсису тощо. Але я не наполягаю на тому, що це абсолютно правильний спосіб зробити речі.


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

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

2
+1. Немає жорстких правил, лише домовленості - але корисно дотримуватися домовленостей інших, хто використовує вашу мову, оскільки це полегшує програмістам розуміння коду один одного.
j_random_hacker

1
"стався апокаліпсис" - неважливо винятки, якщо щось виправдовує UB, це робить ;-)
Стів Джессоп,

3
Кацкул: Майже все програмування - це умовність. Технічно нам навіть не потрібні винятки або взагалі насправді багато. Якщо це не стосується NP-повноти, big-O / theta / little-o або універсальних машин Тьюрінга, це, мабуть, умовно. :-)
Кен

7

Я не думаю, що винятки слід використовувати рідко. Але.

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

Якщо ви збираєтеся широко використовувати винятки, тоді:

  • будьте готові навчити своїх людей про те, що таке виняток безпека
  • ви не повинні використовувати необроблене управління пам'яттю
  • широко використовувати RAII

З іншого боку, використання винятків у нових проектах із потужною командою може зробити код чистішим, простішим у обслуговуванні та навіть швидшим:

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

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

1
+1 для "коли вас змушують писати безпечний для винятків код, він стає більш структурованим". Код на основі винятків змушений мати більшу структуру, і я вважаю, що насправді набагато легше міркувати про об'єкти та інваріанти, коли неможливо їх ігнорувати. Насправді, я вважаю, що сильна безпека винятків полягає в написанні майже реверсивного коду, що дозволяє дуже легко уникнути невизначеного стану.
Том,

7

РЕДАКТУВАТИ 20.11.2009 :

Я просто читав цю статтю MSDN про покращення продуктивності керованого коду, і ця частина нагадала мені це питання:

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

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

РЕДАГУВАТИ :

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

  1. Плакат вимагає об'єктивного обґрунтування загальноприйнятої думки, що винятки слід використовувати економно. Ми можемо обговорювати читабельність та правильний дизайн все, що хочемо; але це суб’єктивні питання з людьми, готовими сперечатися з будь-якої сторони. Я думаю, що плакат про це знає. Справа в тому, що використання винятків для управління потоком програм часто є неефективним способом здійснення дій. Ні, не завжди , але часто . Ось чому розумна порада - вживати винятки економно, як і хороша порада - їсти червоне м’ясо або економно пити вино.

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

Для ілюстрації моєї думки розглянемо наступні приклади коду C #.

Приклад 1. Виявлення недійсного вводу користувача

Це приклад того, що я б назвав зловживанням виключеннями .

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}

Для мене цей код смішний. Звичайно, це працює . З цим ніхто не сперечається. Але це має бути приблизно так:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

Приклад 2: Перевірка наявності файлу

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

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}

Це здається досить розумним, так? Ми намагаємось відкрити файл; якщо його немає, ми ловимо цей виняток і намагаємось відкрити інший файл ... Що з цим поганого?

Нічого, насправді. Але розгляньте цю альтернативу, яка не створює жодних винятків:

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}

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

Використання try/ catchблокування: 25.455 секунд
Використання int.TryParse: 1.637 мілісекунд

Для другого прикладу я в основному зробив те саме: склав список з 10000 випадкових рядків, жоден з яких не був дійсним шляхом, а потім додав дійсний шлях до самого кінця. Ось такі результати:

Використання try/ catchблокування: 29.989 секунд
Використання File.Exists: 22.820 мілісекунд

Багато людей відповіли б на це, сказавши: "Так, ну, кидати і ловити 10 000 винятків вкрай нереально; це перебільшує результати". Звичайно. Різниця між створенням одного винятку та власною обробкою неправильних даних не буде помітною для користувача. Фактом залишається той факт, що використання винятків у цих двох випадках у 1000–10 000 разів повільніше, ніж альтернативні підходи, які настільки ж читаються - якщо не більше.

Ось чому я включив приклад GetNine()методу нижче. Справа не в тому, що це нестерпно повільно чи неприпустимо повільно; це те, що це повільніше, ніж повинно бути ... без поважних причин .

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


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

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

Ви можете також запитати: чому цей код поганий?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}

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

Ось що люди мають на увазі, коли говорять про винятки "зловживання".


2
"Обробка винятків має жахливі результати." - це деталь реалізації, і вона не відповідає дійсності для всіх мов, і навіть для C ++, зокрема, для всіх реалізацій.
Павло Мінаєв

"це деталь реалізації" Я не пам'ятаю, як часто я чув цей аргумент - і він просто смердить. Пам’ятайте: усі комп’ютерні матеріали - це сукупність деталей реалізації. А також: Нерозумно використовувати викрутку як заміну молотка - це те, що роблять люди, які багато говорять про "просто деталі реалізації".
Юрген

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

1
@j_random_hacker: Ви маєте рацію, GetNineвідмінно виконує свою роботу. Моя думка полягає в тому, що немає підстав використовувати його. Це не схоже на те, що це "швидкий і легкий" спосіб зробити щось, тоді як більш "правильний" підхід зажадає набагато більше зусиль. З мого досвіду, часто трапляється так, що для "правильного" підходу насправді знадобиться менше зусиль, або приблизно те саме. У будь-якому випадку, для мене продуктивність єдина об’єктивна причина, чому не рекомендується використовувати винятки ліворуч та праворуч. Що стосується того, чи "втрата продуктивності" сильно завищена ", це насправді суб'єктивно.
Дан Тао,

1
@j_random_hacker: Ваш бік дуже цікавий, і справді веде додому думку Павла про влучення продуктивності в залежності від реалізації. Я б дуже сумнівався в тому, що хтось міг би знайти сценарій, коли використання винятків насправді перевершує альтернативу (де альтернатива існує, принаймні, як у моїх прикладах).
Dan Tao

6

Усі основні правила щодо винятків зводяться до суб'єктивних термінів. Ви не повинні розраховувати на отримання жорстких і швидких визначень, коли їх використовувати, а коли - ні. "Тільки у виняткових обставинах". Приємне кругове визначення: винятки стосуються виключних обставин.

Коли використовувати винятки, це потрапляє в той самий сегмент, що й "як дізнатися, чи є цей код одним класом чи двома?" Це частково стилістичне питання, частково перевагу. Винятки - це інструмент. Їх можна використовувати та зловживати ними, а пошук межі між ними є частиною мистецтва та навички програмування.

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


6

Не те, щоб винятки рідко використовувались. Просто кидати їх слід лише у виняткових обставинах. Наприклад, якщо користувач вводить неправильний пароль, це не є винятком.

Причина проста: винятки різко виходять із функції і поширюються вгору по стеку до catchблоку. Цей процес є дуже обчислювально обчислювальним: C ++ створює свою систему винятків, щоб мати невеликі накладні витрати на "звичайні" виклики функцій, тому, коли виникає виняток, йому потрібно зробити багато роботи, щоб знайти, куди йти. Більше того, оскільки кожен рядок коду може викликати виняток. Якщо у нас є якась функція, fяка часто породжує винятки, нам тепер потрібно подбати про використання наших try/ catchблоків навколо кожного виклику f. Це досить погана взаємодія інтерфейсу / реалізації.


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

4
Що означає "виняткові обставини"? На практиці моя програма повинна обробляти фактичні винятки IOE частіше, ніж погані паролі користувачів. Це означає, що ви думаєте, що я повинен зробити BadUserPassword винятком або що хлопці stdlib не повинні робити IOException винятком? "Дуже обчислювально обчислювально" не може бути справжньою причиною, тому що я ніколи не бачив програми (і, звичайно, не моєї), для якої обробка неправильного пароля за допомогою будь-якого механізму управління була вузьким місцем у роботі.
Кен

5

Я згадав про це питання у статті про винятки на C ++ .

Відповідна частина:

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


1
Розумієте, я прийшов до C ++ із C, і я думаю, що "сценарії обробки помилок" - це нормально. Вони можуть виконуватися не часто, але я витрачаю більше часу на роздуми над ними, ніж на шляхи коду без помилок. Тож знову ж таки, я загалом погоджуюся з настроями, але це не наближує нас до визначення поняття "нормальне" і того, як ми можемо зробити висновок із визначення "нормальний", що використання винятків є поганим варіантом.
Steve Jessop

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

2
Правильно, але я не загадковим чином сприймаю міркування щодо коду помилки. Отже, якщо "невидимі шляхи коду" дуже важко читати, розуміти та поширювати на "звичайний" код, то я взагалі не бачу, чому застосування слова "помилка" робить їх "безперечно прийнятними". З мого досвіду, ситуації з помилками трапляються постійно, і це робить їх "нормальними". Якщо ви можете виявити винятки в ситуаціях помилок, то ви можете розібратися в ситуаціях, що не стосуються помилок. Якщо ви не можете, ви не можете, і все, що ви зробили, робить ваш код помилки непроникним, але ігнорує цей факт, оскільки це "лише помилки".
Steve Jessop

1
Приклад: у вас є функція, де щось потрібно зробити, але ви спробуєте цілий ряд різних підходів, і ви не знаєте, який з них вдасться, і кожен з них може спробувати кілька додаткових підпідходів, і так далі, поки щось не спрацює. Тоді я б стверджував, що невдача є "нормальною", а успіх "винятковою", хоча явно не є помилкою. Накидати виняток на успіх не важче зрозуміти, ніж кинути виняток на жахливі помилки в більшості програм - ви гарантуєте, що лише один біт коду повинен знати, що робити далі, і ви стрибаєте прямо туди.
Steve Jessop

1
@onebyone: Ваш другий коментар насправді робить хороший момент, якого я ще не бачив. Я думаю, що відповідь на це полягає в тому, що (як ви фактично сказали у власній відповіді) використання винятків для умов помилок було вписано в "стандартну практику" багатьох мов, що робить його корисним керівним принципом, якого слід дотримуватися, навіть якщо оригінальні причини робити це було помилково.
j_random_hacker

5

Мій підхід до обробки помилок полягає в тому, що існує три основні типи помилок:

  • Дивна ситуація, з якою можна впоратися на сайті помилок. Це може бути, якщо користувач вводить невірний ввід у рядку командного рядка. Правильна поведінка - це просто скаржитись користувачеві та цикл у цьому випадку. Іншою ситуацією може бути ділення на нуль. Ці ситуації насправді не є ситуаціями помилок, і вони, як правило, спричинені неправильним введенням.
  • Ситуація, подібна до попередньої, але така, яку неможливо обробити на сайті помилок. Наприклад, якщо у вас є функція, яка приймає ім’я файлу та аналізує файл із цим іменем, можливо, він не зможе відкрити файл. У цьому випадку він не може впоратися з помилкою. Це коли винятки сяють. Замість того, щоб використовувати підхід C (повернути недійсне значення як прапор і встановити глобальну змінну помилки для позначення проблеми), код може замість цього створити виняток. Тоді телефонний код зможе мати справу з винятком - наприклад, запитувати користувача про інше ім'я файлу.
  • Ситуація, яка не повинна статися. Це коли порушується інваріант класу, або функція отримує недійсний параметр або подібне. Це вказує на логічний збій у коді. Залежно від рівня відмови може бути доречним виняток або примусове негайне припинення може бути кращим (як assertце робиться). Як правило, ці ситуації вказують на те, що десь у коді щось зламалось, і ви фактично не можете довіряти, щоб щось інше було правильним - може статися пошкодження пам'яті. Ваш корабель тоне, сходите.

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


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

5

Я прочитав тут деякі відповіді. Я все ще вражений тим, у чому вся ця плутанина. Я категорично не погоджуюся з усіма цими винятками == spagetty code. З розгубленістю я маю на увазі, що є люди, які не цінують обробку винятків на C ++. Я не впевнений, як я дізнався про обробку винятків на C ++ - але я зрозумів наслідки за лічені хвилини. Це було приблизно в 1996 році, і я використовував компілятор borland C ++ для OS / 2. У мене ніколи не було проблеми вирішити, коли використовувати винятки. Зазвичай я обертаю помилкові дії відміни в класи C ++. Такі дії скасування дії включають:

  • створення / знищення системного дескриптора (для файлів, карт пам'яті, графічних інтерфейсів WIN32, сокетів тощо)
  • налаштування / зняття обробників
  • виділення / вивільнення пам'яті
  • заявляючи / звільняючи мьютекс
  • збільшення / зменшення кількості посилань
  • показ / приховування вікна

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

Потім є вилучення / повторне вилучення, щоб додати більше інформації про помилку.

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

Можна класифікувати такі класи в складні класи. Після того, як конструктор якогось об'єкта-члена / бази виконується, можна покладатися на те, що всі інші конструктори того самого об'єкта (виконувались раніше) виконувались успішно.


3

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

Але існують певні типи помилок, які неможливо легко усунути за допомогою звичайних конструкцій. Катастрофічні збої (наприклад, збій у розподілі ресурсів) можна виявити на низькому рівні, але, мабуть, неможливо впоратись з ними, тому просте твердження if є неадекватним. Зазвичай такі типи несправностей потрібно обробляти на значно вищому рівні (наприклад, зберегти файл, записати помилку, вийти). Спроба повідомити про таку помилку за допомогою традиційних методів (наприклад, повернених значень) є нудною та схильною до помилок. Крім того, він вводить накладні витрати в шари API середнього рівня, щоб впоратися з цією химерною, незвичною помилкою. Накладні витрати відволікають клієнта цих API і вимагають від них занепокоєння проблемами, які не залежать від них. Винятки забезпечують спосіб нелокальної обробки великих помилок, які '

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

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

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


1
І все ж, в Java, на parseIntсамому ділі це виняток. Отже, я б сказав, що думки щодо доцільності використання винятків дуже залежать від існуючих практик у стандартних бібліотеках обраної ними мови розробки.
Чарльз Сальвія

Час виконання Java у будь-якому випадку повинен відстежувати інформацію, що полегшує генерування винятків ... Код c ++, як правило, не має такої інформації в руках, тому для її генерування передбачено покарання за продуктивність. Не тримаючи його під рукою, він, як правило, стає швидшим / меншим / зручнішим для кешування тощо. Крім того, код Java має динамічні перевірки таких речей, як масиви тощо, функція, не властива c ++, тому Java додатковий клас винятків, яким є розробник вже дбаю про багато цих речей за допомогою блоків try / catch, то чому б не використовувати експетони для ВСЕГО?
Ape-inago

1
@Charles - Питання позначене як C ++, тому я відповідав з цієї точки зору. Я ніколи не чув, щоб програміст на Java (або C #) говорив, що винятки стосуються лише виняткових умов. Програмісти на C ++ говорять це регулярно, і питання полягало в тому, чому.
Адріан Маккарті,

1
Власне, про це говорять і винятки на C #, і причина в тому, що винятки дуже дорого вкидати в .NET (більше, ніж, наприклад, на Java).
Павло Мінаєв

1
@Adrian: правда, питання позначене як C ++, хоча філософія обробки винятків є дещо міжмовною діалоговою, оскільки мови, безумовно, впливають одна на одну. У будь-якому випадку, Boost.lexical_cast видає виняток. Але чи насправді недійсний рядок є таким "винятковим"? Це, мабуть, ні, але розробники Boost вважали, що мати такий крутий синтаксис варто використовувати винятки. Це просто свідчить про те, наскільки все це суб’єктивно.
Чарльз Сальвія

3

У C ++ є кілька причин.

По-перше, часто важко зрозуміти, звідки беруться винятки (оскільки їх можна викинути майже з чого завгодно), тому блок catch - це щось на кшталт оператора COME FROM. Це гірше, ніж GO TO, оскільки в GO TO ви знаєте, звідки ви робитесь (твердження, а не якийсь випадковий виклик функції) і куди ви йдете (мітка). Вони в основному є потенційно безпечною версією C setjmp () і longjmp (), і ніхто не хоче їх використовувати.

По-друге, в C ++ немає вбудованого збору сміття, тому класи C ++, які мають власні ресурси, позбавляються їх у своїх деструкторах. Отже, в C ++ обробка винятків система повинна запускати всі деструктори в області дії. У мовах з GC і відсутністю реальних конструкторів, таких як Java, викиди є набагато менш обтяжливими.

По-третє, спільнота С ++, включаючи Б'ярн Страуструп та Комітет стандартів та різних авторів компіляторів, припускає, що винятки мають бути винятковими. Загалом, не варто йти проти мовної культури. Реалізації засновані на припущенні, що винятки будуть рідкісними. Кращі книги трактують винятки як виняткові. Хороший вихідний код використовує кілька винятків. Хороші розробники C ++ розглядають винятки як виняткові. Щоб піти проти цього, вам потрібна вагома причина, і всі причини, які я бачу, на стороні того, щоб тримати їх винятковими.


1
"C ++ не має вбудованого збору сміття, тому класи C ++, які мають власні ресурси, позбавляються їх у своїх деструкторах. Тому в обробці винятків C ++ система повинна запускати всі деструктори в області дії. На мовах з GC і без реальних конструкторів , як Java, кидати винятки набагато менш обтяжливим ". - деструктори повинні запускатися, коли зазвичай залишають область дії, а finallyблоки Java нічим не відрізняються від деструкторів C ++ з точки зору накладних витрат на реалізацію.
Павло Мінаєв

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

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

@Pavel: Будь ласка, проігноруйте мій неправильний коментар вище (тепер видалений), в якому сказано, що finallyблоки Java не гарантовано запускаються - звичайно, вони є. Я плутався з finalize()методом Java , який не гарантовано запускатиметься.
j_random_hacker

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

2

Це поганий приклад використання винятків як потоку управління:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

Що дуже погано, але ви повинні писати:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

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

Винятки також пов’язані з певними покараннями щодо продуктивності, але проблеми з продуктивністю повинні дотримуватися правила: "передчасна оптимізація - корінь усього зла"


Це виглядає як випадок, коли більше поліморфізму було б добре, а не перевіряти incomeType.
Sarah Vessels

@Sarah так, я знаю, це було б непогано. Це тут лише для ілюстрації.
Едісон Густаво Муенц,

3
Чому це погано? Чому ви повинні писати це другим способом? Запитувач хоче знати причини правил, а не правила. -1.
j_random_hacker

2
  1. Ремонтопридатність: Як згадували люди вище, кидання винятків у краплю капелюха схоже на використання гото.
  2. Сумісність: Ви не можете взаємодіяти з бібліотеками C ++ з модулями C / Python (принаймні непросто), якщо використовуєте винятки.
  3. Погіршення продуктивності: RTTI використовується, щоб фактично знайти тип винятку, який накладає додаткові накладні витрати. Таким чином, винятки не підходять для обробки випадків використання, що часто трапляються (користувач вводить int замість рядка тощо).

2
1. Але в деяких мовах винятки наводяться на краплю капелюха. 3. Іноді продуктивність не має значення. (80% випадків?)
UncleBens

2
2. Програми на C ++ майже завжди використовують винятки, оскільки їх використовує значна частина стандартної бібліотеки. Класи контейнерів кидають. Клас рядків кидає. Класи потоку кидають.
Девід Торнлі,

Які контейнерні та рядкові операції викидають, крім at()? Тим не менш, будь-хто newможе кинути, так що ...
Павло Мінаєв

RTTI не є суворо необхідним для Try / Throw / Catch, наскільки я розумію. Погіршення продуктивності завжди згадувалось, і я в це вірю, але ніхто ніколи не згадує шкалу чи посилання на будь-які посилання.
Catskul

UncleBens: (1) для ремонтопридатності, а не продуктивності. Надмірне використання спроби / лову / кидка зменшує читабельність коду. Емпіричне правило (і за це я можу потрапити під удар, але це лише моя думка), чим менше точок входу та виходу, тим легше читати код. Девід Торнлі: Не тоді, коли вам потрібна сумісність. -fno-exceptions прапор у gcc відключає винятки під час компіляції бібліотеки, що викликається із коду C.
Шрідхар Айєр,

2

Я б сказав, що винятки - це механізм, який дозволяє безпечно вивести вас із поточного контексту (з поточного кадру стека в найпростішому розумінні, але це більше того). Це найближче, до чого структуроване програмування дійшло до goto. Щоб використовувати винятки у тому вигляді, в якому вони передбачалися використовувати, у вас повинна виникнути ситуація, коли ви не можете продовжувати те, що зараз робите, і не можете впоратися з цим на тому місці, де ви зараз перебуваєте. Так, наприклад, коли пароль користувача помилковий, ви можете продовжити, повернувши false. Але якщо підсистема користувацького інтерфейсу повідомляє, що не може навіть запропонувати користувачеві, просто повернення "не вдалося ввійти" було б неправильним. Поточний рівень коду просто не знає, що робити. Тому він використовує механізм винятків, щоб делегувати відповідальність комусь вище, хто може знати, що робити.


2

Одна дуже практична причина полягає в тому, що під час налагодження програми я часто переключаю винятки First Chance (Налагодження -> Винятки) для налагодження програми. Якщо трапляється багато винятків, дуже важко знайти, де щось пішло "не так".

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


2

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

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

Політика кодування Google говорить, що ніколи не використовувати винятки , особливо в C ++. Ваша програма або не готова до обробки винятків, або вона є. Якщо це не так, то виняток, ймовірно, поширюватиметься доти, доки ваша програма не помре.

Ніколи не цікаво дізнатися, в якій бібліотеці ви використовували виключення, і ви не були готові їх обробляти.


1

Легітимний випадок виключення:

  • Ви намагаєтесь відкрити файл, його там немає, викидається FileNotFoundException;

Незаконна справа:

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

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

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

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

Це було б краще:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

КОМЕНТАР ДОДАТИ ДО ВІДПОВІДІ:

Можливо, мій приклад був не дуже вдалим - ri.next () не повинен викидати виняток у другому прикладі, а якщо так, то є щось справді виняткове, і слід вжити якихось інших дій. Коли приклад 1 активно використовується, розробники схоплять загальний виняток замість конкретного і припускають, що виняток пов’язаний з помилкою, яку вони очікують, але це може бути пов’язано з чимось іншим. Зрештою, це призводить до того, що реальні винятки ігноруються, оскільки винятки стали частиною потоку програм, а не винятком із нього.

Коментарі до цього можуть додати більше, ніж сама моя відповідь.


1
Чому ні? Чому б не використовувати винятки для другого випадку, про який ви згадали? Запитувач хоче знати причини правил, а не правила.
j_random_hacker

Дякуємо за докладнішу розробку, але IMHO ваші 2 фрагменти коду мають майже однакову складність - обидва використовують сильно локалізовану логіку управління. Там, де складність винятків найбільш чітко перевищує її / тоді / інакше, коли у вас є купа тверджень всередині блоку try, будь-який з яких міг би кинути - чи погодитесь ви?
j_random_hacker

Можливо, мій приклад був не дуже вдалим - ri.next () не повинен викидати виняток у другому прикладі, а якщо так, то є щось справді виняткове, і слід вжити якихось інших дій. Коли приклад 1 активно використовується, розробники схоплять загальний виняток замість конкретного і припускають, що виняток пов’язаний з помилкою, яку вони очікують, але це може бути пов’язано з чимось іншим. Зрештою, це призводить до того, що реальні винятки ігноруються, оскільки винятки стали частиною потоку програм, а не винятком із нього.
Раві Валлау

Отже, ви кажете: З часом інші твердження можуть зростати всередині tryблоку, і тоді ви вже не впевнені, що catchблок справді ловить те, про що ви думали, що ловить - це правда? Хоча важче зловживати підходом if / then / else таким же чином, оскільки ви можете протестувати лише 1 річ за раз, а не набір речей одночасно, тому підхід з виключенням може призвести до більш крихкого коду. Якщо так, будь ласка, обговоріть це у своїй відповіді, і я із задоволенням поставитиму +1, оскільки, на мою думку, крихкість коду є добросовісною причиною.
j_random_hacker

0

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

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


0

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

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

Я зупинюсь на іншому моменті:

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

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

Таким чином, ви в кінцевому підсумку елегантно спалите свій процесор, не знаючи про це.

Отже: використовуйте винятки лише у виняткових випадках - Значення: Коли траплялись реальні помилки!


1
"Причина в тому, що винятки - це не лише дуже потужні goto-оператори, вони також повинні розмотувати стек для всіх кадрів, які вони залишають. Таким чином, безумовно, також слід деконструювати об'єкти в стеку тощо". - якщо у вас є кілька фреймів стека на стеку викликів, тоді всі ці фрейми стека (і об'єкти на них) у будь-якому випадку доведеться знищити - не має значення, чи це відбувається тому, що ви повертаєтеся нормально, або тому, що ви кидаєте виняток. Зрештою, незважаючи на дзвінки std::terminate(), вам все одно доведеться знищити.
Павло Мінаєв

Ви, звичайно, праві. Але все ж, будь ласка, пам’ятайте: кожного разу, коли ви використовуєте винятки, система повинна забезпечувати інфраструктуру, щоб робити все це чарівно. Я просто хотів трохи пояснити, що це не тільки goto. Також при розмотуванні система повинна знайти правильне місце лову, на що буде витрачений додатковий час, код та використання RTTI. Тож це набагато більше, ніж стрибок - більшість людей просто не знають, що це розуміють.
Юрген

Поки ви дотримуєтесь стандартного стандарту C ++, RTTI не уникнути. В іншому випадку, звичайно, накладні витрати є. Просто я часто бачу, що це суттєво перебільшено.
Павло Мінаєв

1
-1. "Винятки мають право на обробку помилок і виключно на обробку помилок" - говорить хто? Чому? Ефективність не може бути єдиною причиною. (А) Більшість кодів у світі не перебувають у внутрішньому циклі механізму рендерингу гри або функції множення матриць, тому використання винятків не матиме помітної різниці в продуктивності. (B) Як хтось десь зазначав у коментарі, все розгортання стека в кінцевому підсумку повинно відбутися в будь-якому випадку, навіть якщо старий C-стиль перевірки на помилку-повернення-значень-і-передачу-назад-якщо-потрібна помилка- застосовувався підхід до обробки.
j_random_hacker

Скажіть I. У цій темі там, де наводиться безліч причин. Просто прочитайте їх. Я хотів описати лише одну. Якщо це не той, який вам потрібен, не звинувачуйте мене.
Юрген

0

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

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

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


0

Я використовую винятки, якщо:

  • сталася помилка, яку неможливо відновити локально І
  • якщо помилка не відновлена ​​з програми, слід припинити.

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

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

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


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

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

ІМХО, ви не пояснили, чому необхідний консерватизм. Чому вони іноді лише "відповідні"? Чому не постійно? (До речі, я вважаю, що запропонований вами підхід чудовий, це більш-менш те, що я роблю сам, я просто не думаю, що це стосується більшості причин, чому питання.)
j_random_hacker

ОП задав сім різних питань. Я вирішив відповісти лише на одну. Мені шкода, що Ви вважаєте, що це варто проти голосувати.
Білл

0

Хто сказав, що їх слід використовувати консервативно? Просто ніколи не використовуйте винятки для регулювання потоку і все. А кого хвилює вартість винятку, коли його вже кинули?


0

Мої два центи:

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

Чудовим прикладом для мене є або обробка файлів, або обробка базами даних. Припустимо, що все нормально, і закрийте файл наприкінці, або якщо трапиться якесь виняток. Або відкатіть транзакцію, коли сталося виняток.

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

Для ParseInt, як згадувалося раніше, мені подобається ідея винятків. Просто поверніть значення. Якщо параметр не підлягав синтаксичному аналізу, викиньте виняток. Це робить ваш код чистішим, з одного боку. На рівні дзвінків вам потрібно зробити щось подібне

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

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

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

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

  • винятки все ще потрібно десь обробляти. Додаткова проблема: c ++ не має синтаксису, який дозволяє вказати, які винятки функція може створювати (як у Java). Отже, рівень виклику не знає, з якими винятками, можливо, доведеться обробляти.
  • іноді код може бути дуже багатослівним, якщо кожну функцію потрібно обернути блоком try / catch. Але іноді все-таки це має сенс.

Тому іноді важко знайти хороший баланс.


-1

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

Приблизно кожне правило програмування - це справді керівний принцип, який говорить: "Не робіть цього, якщо у вас немає дійсно вагомих причин": "Ніколи не використовуй goto", "Уникай глобальних змінних", "Регулярні вирази попередньо збільшують кількість проблем на одну "і т. д. Винятки не є винятком ....


... і запитувач хотів би знати, чому це правило великого пальця, а не почути (ще раз), що це правило великого пальця. -1.
j_random_hacker

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

Можливо, немає чіткого "чому", але є часткові "чому", про які згадують інші, наприклад "тому, що це роблять усі інші" (IMHO - сумна, але реальна причина) або "продуктивність" (IMHO ця причина зазвичай завищена , але це все одно причина). Те саме стосується інших емпіричних правил, таких як уникання goto (зазвичай це ускладнює аналіз потоку управління більше, ніж еквівалентний цикл) та уникнення глобальних змінних (вони потенційно можуть внести багато взаємозв’язків, ускладнюючи подальші зміни коду, і зазвичай однакові цілі можуть бути досягнуті з меншим сполученням інших способів).
j_random_hacker

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

Я згоден з вами, оскільки всі емпіричні правила мають довгі списки застережень. Я не погоджуюсь з тим, що, на мою думку, варто спробувати визначити початкові причини цього правила, а також конкретні застереження. (Я маю на увазі в ідеальному світі, де у нас є нескінченний час на обдумування цих речей і, звичайно, немає термінів;)) Я думаю, що про це просив ОП.
j_random_hacker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.