Я мог бы вызвать гнев 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 ++, который, скажем, блокирует мьютекс через объект мьютекса с областью действия с деструктором, не нужно вручную разблокировать его, поскольку он будет автоматически разблокирован, как только объект выйдет из области видимости, независимо от того, что происходит (даже если исключение встречается). Таким образом, действительно нет необходимости в хорошо написанном коде 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
чтобы места, где это наиболее целесообразно.