Навіщо використовувати спробувати… нарешті, без застереження?


79

Класичний спосіб програмування - с try ... catch. Коли доцільно використовувати tryбез catch?

У Python наступне виглядає законним і може мати сенс:

try:
  #do work
finally:
  #do something unconditional

Однак код нічого не зробив catch. Аналогічно можна подумати і на Яві:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

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

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


4
Чому б у Java не ставити оператор return в кінці блоку спробу?
Кевін Клайн

2
Мені сумно, що спробуйте .. остаточно і спробуйте .. обидва використовують ключове слово спробу, крім обох, починаючи з спробувати, вони - 2 абсолютно різні конструкції.
Пітер Б

3
try/catchне є "класичним способом програмування". Це класичний C ++ спосіб програмування, оскільки C ++ не має належної спроби / нарешті побудувати, а це означає, що вам доведеться впроваджувати гарантовані зворотні зміни стану, використовуючи некрасиві хаки, що включають RAII. Але гідні мови OO не мають такої проблеми, оскільки вони пропонують спробувати. Він використовується для зовсім інших цілей, ніж спроба / ловити.
Мейсон Вілер

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

4
@MasonWheeler "Потворні хаки", будь ласка, поясніть, що поганого в тому, щоб обробляти об'єкт - це власне очищення?
Балдрік

Відповіді:


146

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

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

Якщо ви не можете впоратися з ними локально, то просто мати try / finallyблок цілком розумно - якщо припустити, що якийсь код потрібно виконати незалежно від того, вдався метод чи ні. Наприклад (з коментаря Ніла ) відкриття потоку та передання цього потоку до внутрішнього методу для завантаження є прекрасним прикладом того, коли вам знадобиться try { } finally { }, використовуючи остаточне застереження, щоб забезпечити закриття потоку незалежно від успіху чи провал прочитаного.

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


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

13
@will - тому я використав фразу "як можна".
ChrisF

8
Хороша відповідь, але я додам приклад: відкриття потоку та передача цього потоку до внутрішнього методу для завантаження - це чудовий приклад того, коли вам знадобиться try { } finally { }, скориставшись, нарешті, пропозицією про те, щоб потік було закрито в кінцевому рахунку незалежно від успіху / невдача.
Ніл

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

"просто мати спробу / остаточно заблокувати цілком розумно" шукав саме цю відповідь. дякую @ChrisF
neal aise

36

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

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

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

У finallyблоці слід бути обережним, щоб він сам по собі не став винятком. Наприклад, переконайтеся, що перевіряєте всі змінні nullтощо.


8
+1: Ідіоматичне для "треба прибирати". Більшість застосувань try-finallyможе бути замінено withвисловлюванням.
С.Лотт

12
Різні мови мають надзвичайно корисні особливості, пов'язані з мовою try/finally. C # has using, Python has withetc.
yfeldblum

5
@yfeldblum - є тонка різниця між, usingі try-finally, оскільки Disposeметод не буде викликаний usingблоком, якщо IDisposableв конструкторі об'єкта трапиться виняток . try-finallyдозволяє виконувати код, навіть якщо конструктор об'єкта кидає виняток.
Скотт Вітлок

5
@ScottWhitlock: Це гарна річ? Що ви намагаєтесь зробити, викликати метод на неконструйованому об’єкті? Це мільярд видів поганого.
DeadMG

1
@DeadMG: Дивіться також stackoverflow.com/q/14830534/18192
Brian

17

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

  • Ось як це пояснено та виправдано в документації API (жирний шрифт у цитаті - мій):

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

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

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


Чи можу я поставити l.lock()всередину спробувати? try{ l.lock(); }finally{l.unlock();}
RMachnik

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

4
Можна, але якщо l.lock()не вдасться, finallyблок все одно буде працювати, якщо він l.lock()знаходиться всередині tryблоку. Якщо ви робите це так, як пропонує gnat, finallyблок запуститься лише тоді, коли ми дізнаємось, що замок був придбаний.
Wtrmute

12

На базовому рівні catchі finallyвирішити дві пов'язані, але різні проблеми:

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

Тож обидва пов'язані якось із проблемами (винятками), але це майже все, що їм спільне.

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

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


2
Нітпік: "... нарешті блок повинен бути в тому ж методі, де створені ресурси ..." . Безумовно, це гарна ідея зробити це саме так, адже простіше помітити, що немає витоку ресурсів. Однак це не є необхідною передумовою; тобто вам не потрібно робити це так. Ви можете випустити ресурс у finally(статично чи динамічно), що додає спробу оператора ... і все-таки бути 100% герметичним.
Стівен C

6

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

У C ++ використовується RAII та конструктори / деструктори; у Python це withтвердження; а в C # - це usingтвердження.

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


2
А в Java це ARM блоки
MarkJ

2
Припустимо, у вас погано розроблений об’єкт (наприклад, той, який не реалізує належним чином IDisposable в C #), який не завжди є життєздатним варіантом.
мотиватор

1
@mootinator: чи не можете ви успадкувати погано сконструйований об'єкт і виправити його?
Ніл Г

2
Або інкапсуляція? Ба. Я маю на увазі, так, звичайно.
мотиватор

4

У багатьох мовах finallyзаява також працює після оператора return. Це означає, що ви можете зробити щось на кшталт:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

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

Незалежно від того, чи це добре, чи погано, це обговорення, але try {} finally {}не завжди обмежується обробкою винятків.


0

Я можу викликати гнів Pythonistas (не знаю, як я мало використовую Python) або програмістів з інших мов з цією відповіддю, але, на мою думку, більшість функцій не повинні мати catchблок, в ідеалі. Щоб показати чому, дозвольте мені протиставити це ручному розповсюдженню коду помилок такого типу, який я мав робити під час роботи з Turbo C наприкінці 80-х та на початку 90-х.

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

введіть тут опис зображення

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

Точка відмови та відновлення

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

Ці функції завжди були тривіальними для правильного запису до того, як було доступно обробку винятків, оскільки функція, яка може зіткнутися із зовнішнім збоєм, як-от невміння виділити пам'ять, може просто повернути NULLабо 0або -1або встановити глобальний код помилки чи щось для цього. І відновлення помилок / звітування про помилки було завжди простим, оскільки, як тільки ви відпрацювали свій шлях до стека викликів, до того моменту, коли було сенс відновити та повідомити про помилки, ви просто приймаєте код помилки та / або повідомлення та повідомляєте про це користувачеві. І, природно, функція в аркуші цієї ієрархії, яка ніколи не може зникнути, незалежно від того, як це буде змінено в майбутньому ( Convert Pixel), є мертвим простим правильним написанням (принаймні, стосовно обробки помилок).

Поширення помилок

Однак виснажливими функціями, схильними до людських помилок, були розповсюджувачі помилок - ті, які не стикалися безпосередньо з помилками , а називали функції, які могли виходити з ладу десь глибше в ієрархії. У цей момент, Allocate Scanlineможливо, доведеться попрацювати з помилкою, mallocа потім повернути помилку вниз Convert Scanlines, потім Convert Scanlinesдоведеться перевірити цю помилку і передати її до Decompress Image, потім Decompress Image->Parse Image, і Parse Image->Load Image, і Load Imageдо команди для завершення користувача, де остаточно повідомляється про помилку .

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

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

Зменшення людської помилки: глобальні коди помилок

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

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

Зменшення людської помилки: поводження з винятками

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

Але значення поводження з винятками тут полягає в тому, щоб звільнити потребу в роботі з аспектом потоку управління в ручному розповсюдженні помилок. Це означає, що його значення пов'язане з можливістю уникнути необхідності запису блоку catchблоків у всій вашій базі коду. На наведеній діаграмі єдине місце, яке повинно мати catchблок, - це місце, Load Image User Commandде повідомляється про помилку. Нічого іншого в ідеалі не повинно бути catchні до чого, тому що в іншому випадку воно починає ставати настільки виснажливим і нахилом до помилок, як обробка коду помилок.

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

Очищення ресурсів

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

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

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

Зворотні зовнішні побічні ефекти

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

Ось finally, мабуть, одне з найелегантніших рішень проблеми в мовах, що обертаються навколо змінності та побічних ефектів, оскільки часто цей тип логіки дуже специфічний для певної функції і не так добре відповідає поняттю "очищення ресурсів" ". І я рекомендую finallyв цих випадках вільно використовувати, щоб переконатися, що ваша функція повертає побічні ефекти на мовах, які її підтримують, незалежно від того, потрібен вам catchблок чи ні (і знову ж таки, якщо ви запитаєте мене, добре написаний код повинен містити мінімальну кількість catchблоки, і всі catchблоки повинні знаходитись там, де це має найбільш сенс, як на схемі, наведеній вище в Load Image User Command).

Мова снів

Однак ІМО finallyблизький до ідеалу для усунення побічних ефектів, але не зовсім. Нам потрібно ввести одну booleanзмінну, щоб ефективно відмовити побічні ефекти у разі передчасного виходу (із викинутого винятку чи іншим чином), наприклад:

bool finished = false;
try
{
    // Cause external side effects.
    ...

    // Indicate that all the external side effects were
    // made successfully.
    finished = true; 
}
finally
{
    // If the function prematurely exited before finishing
    // causing all of its side effects, whether as a result of
    // an early 'return' statement or an exception, undo the
    // side effects.
    if (!finished)
    {
        // Undo side effects.
        ...
    }
}

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

transaction
{
    // Cause external side effects.
    ...
}
rollback
{
    // This block is only executed if the above 'transaction'
    // block didn't reach its end, either as a result of a premature
    // 'return' or an exception.

    // Undo side effects.
    ...
}

... з деструкторами для автоматизації очищення локальних ресурсів, роблячи це таким чином, що нам тільки потрібно transaction, rollbackі catch(хоча я все ще хотів би додати finally, скажімо, роботу з ресурсами C, які не очищають себе). Однак finallyіз booleanзмінною - це найближче до того, щоб зробити це просто, що я вважав, що поки не вистачає моєї мрії. Другим найпростішим рішенням, який я знайшов для цього, є захист області мов, таких як C ++ і D, але я завжди вважав, що захисники області є дещо незграбними концептуально, оскільки це розмиває ідею "очищення ресурсів" та "зміни побічних ефектів". На мою думку, це дуже чіткі ідеї, які слід вирішувати по-іншому.

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

Висновок

Так що, як би там не було, я вважаю, що ваш try/finallyкод для закриття сокета чудовий і чудовий, враховуючи, що Python не має C ++-еквівалента деструкторів, і я особисто вважаю, що ви повинні використовувати це вільно для місць, які потребують зворотних побічних ефектів і мінімізуйте кількість місць, куди вам доводиться, catchдо місць, де це має найбільше сенс.


-66

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

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

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

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


22
-1: У Java може знадобитися остаточний пункт для звільнення ресурсів (наприклад, закриття файлу або випуску з'єднання з БД). Це не залежить від можливості обробляти виняток.
кевін клайн

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

63
@Pankaj: Ваша відповідь говорить про те, що catchпункт повинен бути завжди присутній, коли є try. Більш досвідчені працівники, включаючи мене, вважають, що це погана порада. Ваші міркування хибні. Метод, що містить tryне єдине можливе місце, за яким може бути вилучений виняток. Часто найпростіше і найкраще дозволити винятки з лову та звітування з найвищого рівня, а не дублювати застереження про вилов у коді.
кевін клайн

@kevincline, я вважаю, що сприйняття питання з боку інших дещо інше. Питання полягало саме в тому, чи слід ми вилучити виняток чи ні .... І я відповів у цьому відношенні. Поводження з ексцепсією можна здійснити кількома способами, а не просто спробувати. Але це не стосувалося ОП. Якщо збірка винятків є достатньою для вас та інших хлопців, тоді я мушу сказати, що удача.
Pankaj Upadhyay

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