Я можу викликати гнів 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
до місць, де це має найбільше сенс.