1. Знакомство с блоком finally
Очистка и освобождение ресурсов
Представьте: вы занимаетесь экспериментами в химической лаборатории и, когда всё закончили (или даже если взорвали пару колбочек), вам всё равно нужно убрать рабочее место, смыть химикаты и выключить свет. Вот для этого и нужен блок finally — он срабатывает всегда, после try и catch, независимо от того, было ли исключение или нет.
try
{
// Здесь пишем "опасный" код, который может вызвать исключение
}
catch
{
// Здесь ловим и обрабатываем исключения
}
finally
{
// Этот код выполнится всегда, исключение было или не было
}
Для чего это нужно? В первую очередь, чтобы гарантированно освободить ресурсы: закрыть файл, соединение с базой данных, разблокировать дверь в серверную... Если бы не было finally, после ошибки некоторые ресурсы могли бы остаться «зависшими» — а это уже настоящая проблема. Например, файл будет заблокирован, и его не сможет открыть даже админ.
Почему нельзя всё писать в catch?
Могли бы мы просто освободить ресурс в catch? Теоретически, да. Но если всё прошло хорошо, catch не выполнится. Если мы хотим быть уверены, что освобождение ресурса произойдёт всегда, то нам нужен finally.
В реальной жизни часто встречаются такие задачи, где не важно, был успех или провал — уборка обязана быть. Вот для этого и был придуман finally.
2. Особенности блока finally
Когда finally НЕ срабатывает?
Вопрос с подвохом! finally выполняется всегда. Даже если в блоке try вызывается return (ранний выход из метода), или выбрасывается новое исключение, finally всё равно будет исполнен.
static void Test()
{
try
{
Console.WriteLine("До return");
return;
}
finally
{
Console.WriteLine("finally всё равно сработает!");
}
}
//Вызов Test()
Test();
Выведет:
// До return
// finally всё равно сработает!
Однако если внезапно у вашего приложения случится «жёсткое падение» (например, выключили компьютер, процесс был убит или умерло всё CLR), блок finally не выполнится. Только тут уж ничего не поделаешь.
Оператор finally и стек вызовов
Стек вызовов мы подробнее изучим на следующей лекции, а сейчас просто кратко опишем его: это как стопка вызванных методов, по которой программа «спускается», если не находит подходящий catch.
Ещё важный момент: если в try возникло исключение, а в catch оно не перехвачено (например, нет подходящего обработчика), то программа покидает текущий метод и движется дальше по стеку вызовов до тех пор, пока не найдёт подходящий catch. Но перед этим обязательно выполнится finally-блок на каждом уровне стека.
3. Оператор throw: как самим выбрасывать исключения
Что такое throw и зачем он нужен
Иногда простого перехвата исключения мало — бывает нужно создать свою ошибку и "выбросить" её наружу. Именно для этого и создан оператор throw.
throw new Exception("Это моя специальная ошибка!");
throw буквально говорит CLR: «Я тут заметил что-то очень нехорошее, бросаю свой Exception, дальше пусть разбирается тот, кто вызвал этот код».
throw без создания нового исключения
Можно использовать throw; внутри блока catch, чтобы повторно выбросить только что пойманное исключение — например, если вы обработали часть ошибки, но дальше ответственность за обработку хотите делегировать вышестоящему коду.
try
{
DangerousOperation();
}
catch (Exception ex)
{
LogError(ex);
throw; // повторно выбрасывает текущее исключение, стек вызовов сохранится
}
Если бы мы написали throw ex;, то информация о стеке вызовов была бы потеряна — это плохая практика.
4. Как finally работает вместе с throw?
finally срабатывает даже при выбрасывании исключения
Давайте проверим, что произойдет, если внутри try происходит throw, но у нас есть и finally:
try
{
Console.WriteLine("До ошибки...");
throw new Exception("Ошибка во время try!");
}
catch
{
Console.WriteLine("Catch ловит ошибку.");
}
finally
{
Console.WriteLine("Finally сработал.");
}
Результат:
До ошибки...
Catch ловит ошибку.
Finally сработал.
И если нет подходящего catch, finally всё равно отработает перед тем, как программа вылетит.
Нюансы: что произойдет, если в finally тоже вызывать throw?
Если в finally произойдет throw, то это исключение заменит предыдущее. То есть информация о том, что произошло в try/catch, будет потеряна. Вот почему не рекомендуется кидаться throw в finally, если внутри уже было исключение.
try
{
throw new Exception("Ошибка в try");
}
finally
{
throw new Exception("Ошибка в finally");
}
// Итог: наружу выйдет "Ошибка в finally"
5. Практические рекомендации и типичные ошибки
Что чаще забывают новички
- Не использовать блок finally при освобождении ресурсов, полагаясь только на catch.
- Помещать код, который может вызывать новые исключения, внутрь finally — это приводит к неожиданным ошибкам.
- Забывать, что return внутри try не «обходит» finally — он всегда выполнится.
Альтернатива finally: что выбрать?
С появлением конструкции using (которую мы подробно изучим чуть позже), освобождение ресурсов стало ещё удобнее, но по сути, using под капотом использует тот же finally. Для любых нестандартных ситуаций (например, разблокировка или отправка сообщения об ошибке) — всё равно приходится использовать finally.
За что любят finally на собеседованиях
Каждый рекрутер, который хоть раз писал сервер под высокой нагрузкой, любит задавать вопросы про finally. Обычно спрашивают: «Что произойдёт, если в try стоит return, а в finally — throw?» или «Гарантировано ли освобождение ресурсов при исключениях?». И теперь у вас есть не просто ответы, а понимание. Вы сможете не только рассказать, как работает finally, но и объяснить, зачем он нужен, когда его использовать, и почему без него ни один серьёзный код не обходится.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ