Зачем использовать try… наконец без предложения catch?

79

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

В Python следующее кажется законным и может иметь смысл:

try:
  #do work
finally:
  #do something unconditional

Тем не менее, код ничего не сделал catch. Точно так же можно подумать, что в Java это будет выглядеть следующим образом:

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

Это выглядит хорошо, и вдруг мне не нужно беспокоиться о типах исключений и т. Д. Если это хорошая практика, то когда это хорошая практика? В качестве альтернативы, по каким причинам это не является хорошей практикой или не является законным? (Я не скомпилировал исходный код. Я спрашиваю об этом, поскольку это может быть синтаксической ошибкой для Java. Я проверил, что Python обязательно компилируется.)

С этой проблемой я столкнулся: я продолжаю писать функцию / метод, в конце которой она должна что-то вернуть. Тем не менее, это может быть в месте, которое не должно быть достигнуто и должно быть точкой возврата. Таким образом, даже если я обработаю исключения выше, я все еще возвращаю NULLили пустую строку в некоторой точке кода, которая не должна быть достигнута, часто в конце метода / функции. Мне всегда удавалось реструктурировать код так, чтобы в этом не было необходимости return NULL, так как это, похоже, выглядит менее чем хорошей практикой.

Никлас Розенкранц
источник
4
В Java, почему бы не поместить оператор return в конец блока try?
Кевин Клайн
2
Мне грустно, что try..finally и try..catch оба используют ключевое слово try, за исключением того, что оба, начиная с try, представляют собой 2 совершенно разные конструкции.
Питер Б
3
try/catchэто не «классический способ программирования». Это классический способ программирования на C ++ , потому что в C ++ отсутствует правильная конструкция try / finally, что означает, что вы должны реализовывать гарантированные обратимые изменения состояния, используя некрасивые хаки с использованием RAII. Но приличные языки OO не имеют этой проблемы, потому что они обеспечивают try / finally. Он используется для совсем другой цели, чем попытка / отлов.
Мейсон Уилер
3
Я вижу это много с внешними ресурсами связи. Вы хотите исключение, но должны убедиться, что вы не оставляете открытое соединение и т. Д. Если вы поймали его, вы в любом случае просто перекинули бы его на следующий уровень.
Рог
4
@MasonWheeler "Уродливые хаки", пожалуйста, объясните, что плохого в том, что объект обрабатывает самостоятельно?
Балдрикк

Ответы:

146

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

Если вы можете обрабатывать исключения локально, вы должны это делать, и лучше обрабатывать ошибку как можно ближе к месту ее возникновения.

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

Тем не менее, вам все равно понадобится обработчик исключений где-то в вашем коде - если, конечно, вы не хотите, чтобы ваше приложение полностью аварийно завершало работу. Это зависит от архитектуры вашего приложения, где именно находится этот обработчик.

ChrisF
источник
8
«и лучше обрабатывать ошибку как можно ближе к месту ее возникновения». эх, это зависит Если вы можете восстановить и все же завершить миссию (так сказать), да, конечно. Если вы не можете, лучше оставить исключение до самого верха, где (вероятно) потребуется вмешательство пользователя, чтобы справиться с тем, что произошло.
Будет
13
@Will - вот почему я использовал фразу «как можно».
ChrisF
8
Хороший ответ, но я бы добавил пример: открытие потока и передача этого потока внутреннему методу, который нужно загрузить, является отличным примером того, когда вам нужно try { } finally { }, воспользоваться предложением finally, чтобы гарантировать, что поток в конечном итоге будет закрыт независимо от успеха / отказ.
Нил
потому что иногда весь путь сверху настолько близок, насколько это возможно
Newtopian
«просто иметь блок try / finally вполне разумно», - искал именно этот ответ. спасибо @ChrisF
Нил Айз
36

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

Код в finallyблоке запускается после завершения tryблока и, если возникло исключение, после завершения соответствующего catchблока. Он всегда запускается, даже если в блоке tryor произошло необработанное исключение catch.

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

В finallyблоке следует позаботиться о том, чтобы он сам не выдал исключение. Например, не забудьте проверить все переменные nullи т. Д.

yfeldblum
источник
8
+1: это идиоматично для "должно быть убрано". Большинство случаев использования try-finallyможет быть заменено withутверждением.
С.Лотт
12
Различные языки имеют чрезвычайно полезные специфичные для языка улучшения try/finallyконструкции. usingwith
Есть
5
@yfeldblum - существует небольшая разница между usingи try-finally, так как Disposeметод не будет вызываться usingблоком, если IDisposableв конструкторе объекта происходит исключение . try-finallyпозволяет выполнять код, даже если конструктор объекта выдает исключение.
Скотт Уитлок
5
@ ScottWhitlock: это хорошо ? Что вы пытаетесь сделать, вызвать метод для неструктурированного объекта? Это миллиард плохих.
DeadMG
1
@DeadMG: См. Также stackoverflow.com/q/14830534/18192
Брайан,
17

Примером использования try ... finally без оператора catch (и более того, идиоматического ) в Java является использование Lock в пакете одновременных утилит блокировки.

  • Вот как это объясняется и обосновывается в документации API (жирный шрифт в кавычках мой):

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

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

    Когда блокировка и разблокировка происходят в разных областях, необходимо позаботиться о том, чтобы весь код, который выполняется во время удержания блокировки, был защищен с помощью try-finally или try-catch, чтобы гарантировать, что блокировка снимается при необходимости .

комар
источник
Могу ли я положить l.lock()внутрь попробовать? try{ l.lock(); }finally{l.unlock();}
RMachnik
1
технически, вы можете. Я не поместил это там, потому что семантически, это имеет меньше смысла. tryв этом фрагменте кода предполагается обернуть доступ к ресурсам, зачем загрязнять его чем-то, не связанным с этим
комнат
4
Можно, но если l.lock() не finallyполучится, блок все равно будет работать, если l.lock()находится внутри tryблока. Если вы сделаете это так, как предлагает gnat, finallyблок будет работать только тогда, когда мы узнаем, что блокировка была получена.
Чт
12

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

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

Так что оба связаны с проблемами (исключениями), но это почти все, что у них общего.

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

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

Йоахим Зауэр
источник
2
Nitpick: «... блок finally должен быть в том же методе, в котором были созданы ресурсы ...» . Конечно, это хорошая идея , потому что легче увидеть, что нет утечки ресурсов. Однако это не является необходимым предварительным условием; то есть вам не нужно делать это таким образом. Вы можете освободить ресурс в finally(статически или динамически) вмещающем операторе try ... и при этом быть на 100% герметичным.
Стивен С.
6

У @yfeldblum правильный ответ: try-finally без оператора catch обычно следует заменить соответствующей языковой конструкцией.

В C ++ используются RAII и конструкторы / деструкторы; в Python этоwith утверждение; и в C # это usingутверждение.

Они почти всегда более элегантны, потому что код инициализации и финализации находится в одном месте (абстрагированный объект), а не в двух местах.

Нил Г
источник
2
А в Java это блоки ARM
MarkJ
2
Предположим, у вас есть плохо спроектированный объект (например, тот, который должным образом не реализует IDisposable в C #), который не всегда является жизнеспособным вариантом.
mootinator
1
@mootinator: вы не можете унаследовать от плохо спроектированного объекта и исправить его?
Нил Г
2
Или инкапсуляция? Ба. Я имею в виду да, конечно.
mootinator
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 ++, который, скажем, блокирует мьютекс через объект мьютекса с областью действия с деструктором, не нужно вручную разблокировать его, поскольку он будет автоматически разблокирован, как только объект выйдет из области видимости, независимо от того, что происходит (даже если исключение встречается). Таким образом, действительно нет необходимости в хорошо написанном коде 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

Настоятельно рекомендуется отлавливать ошибки / исключения и обрабатывать их аккуратно, даже если это не обязательно.

Причина, по которой я это говорю, заключается в том, что я считаю, что каждый разработчик должен знать и учитывать поведение своего приложения, иначе он не выполнил свою работу должным образом. Не существует ситуации, в которой блок try-finally заменяет блок try-catch-finally.

Я приведу простой пример: предположим, что вы написали код для загрузки файлов на сервер без перехвата исключений. Теперь, если по какой-либо причине загрузка не удалась, клиент никогда не узнает, что пошло не так. Но, если вы поймали исключение, вы можете отобразить аккуратное сообщение об ошибке, объясняющее, что пошло не так и как пользователь может исправить это.

Золотое правило: всегда ловите исключение, потому что угадывание требует времени

Панкадж Упадхяй
источник
22
-1: В Java может потребоваться предложение finally для освобождения ресурсов (например, закрытие файла или освобождение соединения с БД). Это не зависит от способности обрабатывать исключение.
Кевин Клайн
@kevincline, он не спрашивает, использовать ли, наконец, или нет ... Все, что он спрашивает, это то, требуется ли ловить исключение или нет .... Он знает, что попробовать, поймать и, наконец, делает ..... Наконец, это самая важная часть, мы все знаем, что и почему это используется ....
Pankaj Upadhyay
63
@Pankaj: ваш ответ предполагает, что catchпункт должен присутствовать всегда, когда есть try. Более опытные участники, включая меня, считают, что это плохой совет. Ваши рассуждения ошибочны. Метод, содержащий tryне единственное возможное место, где может быть поймано исключение. Зачастую проще и лучше разрешать ловить и сообщать об исключениях с самого верхнего уровня, чем дублировать предложения catch во всем коде.
Кевин Клайн
@kevincline, я считаю, что восприятие вопроса с другой стороны немного отличается. Вопрос был именно в том, поймать ли мы исключение или нет ... И я ответил на этот счет. Обработка исключений может быть осуществлена ​​множеством способов, а не просто попытаться окончательно. Но это не было проблемой ОП. Если повышение для вас и других ребят достаточно хорошо, тогда я должен сказать удачи.
Панкадж Упадхяй
За исключением случаев, вы хотите, чтобы нормальное выполнение операторов было прервано (и без проверки вручную на каждом этапе). Это особенно верно для глубоко вложенных вызовов методов - слои метода 4 внутри некоторой библиотеки не могут просто «проглотить» исключение; это должно быть отброшено через все слои. Конечно, каждый слой может обернуть исключение и добавить дополнительную информацию; это часто не делается по разным причинам, дополнительное время для разработки и ненужное многословие являются двумя самыми большими.
Даниэль Б