1. Введение
Когда программа становится чуть сложнее, чем Hello, world!, вы быстро сталкиваетесь с задачей: обрабатывать ошибки разных типов по-разному. Представьте: вы работаете с файлами, сетью, БД — и для разных ошибочных сценариев реакция должна быть разной. Например, при отсутствии файла можно предложить пользователю выбрать другой, а при ошибке сети — повторить операцию или показать ободряющее сообщение "Проверьте кабель, вдруг кошка его опять перегрызла".
В C# для этого используют несколько блоков catch подряд. Каждый такой блок специализируется на своём типе исключения (и его наследниках).
Структура множественного catch
try
{
// Здесь — код, который может выбросить разные исключения
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Файл не найден: {ex.Message}");
}
catch (IOException ex) // ловит все ошибки ввода/вывода, если это не FileNotFoundException
{
Console.WriteLine($"Ошибка ввода-вывода: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Что-то пошло не так: {ex.Message}");
}
Важно! Компилятор идёт по блокам сверху вниз и выбирает первый, который подходит по типу. Если поймался FileNotFoundException, дальше по цепочке он уже не пойдёт.
Иллюстрация "цепочки"
| Тип исключения | Поймает какой блок? |
|---|---|
| FileNotFoundException | Первый catch |
| IOException (другой) | Второй catch |
| ArgumentException | Третий (общий) catch |
Почему нельзя смешивать?
Самую "широкую" ловушку — например, с catch (Exception) — всегда ставьте последней, иначе она "поглотит" все исключения раньше времени, и до специализированных catch-ов исполнение не дойдёт.
Это как выключать все пожарные датчики в здании, потому что один из них сработал на подгоревший тостер: в таком случае последующие пожары система не заметит.
Рабочий пример
using System;
using System.IO;
class Program
{
static void Main()
{
try
{
double result = CalculateAverageAgeFromFile("users.txt");
Console.WriteLine($"Средний возраст — {result}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Ошибка: файл не найден. Проверьте путь.");
}
catch (FormatException ex)
{
Console.WriteLine("Ошибка данных: не удаётся прочитать возраст.");
}
catch (Exception ex)
{
Console.WriteLine($"Другая ошибка: {ex.Message}");
}
}
static double CalculateAverageAgeFromFile(string filePath)
{
// (Реализация: читает файл, парсит возраста, вычисляет среднее)
// ...
throw new NotImplementedException();
}
}
Здесь мы явно разделяем, что делать, если файла нет (FileNotFoundException), а что — если в файле какие-то "кривые" данные (FormatException). Все остальные случаи возьмёт "запасной" третий блок.
2. Фильтры catch: ловим тонкие нюансы
Множественные блоки catch удобны, но иногда этого мало. Бывает, что внутри одного типа исключения хочется реагировать по-разному, в зависимости от обстоятельств.
Например, если сеть не работает потому что закончился интернет — мы можем попробовать переподключить. А если сервер просто не отвечает — возможно, стоит показать другое сообщение.
Вот тут и вступают в бой фильтры catch — настоящая суперфича C#, которая позволяет отлавливать не только по типу, но и по дополнительному условию.
Синтаксис фильтра when
catch (IOException ex) when (ex.Message.Contains("диска нет"))
{
Console.WriteLine("Упс! Похоже, вы выдернули флешку.");
}
catch (IOException ex)
{
Console.WriteLine("Другая ошибка ввода/вывода: " + ex.Message);
}
Тут первый блок ловит только те IOException, чьё сообщение содержит фразу "диска нет" — всё остальное уходит во второй блок.
Применение в реальной жизни
Фильтры особенно полезны в работе с сетевыми ошибками, когда по внутренним свойствам исключения нужно принимать решение: повторять ли попытку, или просто сообщить об ошибке.
Ещё пример: представьте, у нас в методе, который парсит файл, мы хотим не просто "ругающийся" FormatException, а отдельно — если речь идёт о возврате возраста (например, если возраст записан как "abc" вместо числа).
catch (FormatException ex) when (ex.Message.Contains("возраст"))
{
Console.WriteLine("Ошибка: не удалось прочитать возраст. Проверьте данные.");
}
catch (FormatException ex)
{
Console.WriteLine("Ошибка формата данных: " + ex.Message);
}
Фильтры и производительность
Фильтры — штука удобная, но помните, что условие в фильтре вычисляется до входа в блок catch, то есть если оно не выполняется — тело блока вовсе не трогается.
Кстати, если фильтр выбрасывает исключение сам по себе (например, в вашем выражении в when случается деление на нуль) — то такое исключение этот блок catch никогда не поймает. С ним нужно быть аккуратнее.
3. Совмещаем множество catch и фильтры
Представьте, что в вашем проекте надо обработать не только тип исключения, но и его внутреннее состояние. Например, при работе с IOException мы хотим разное поведение, если ошибка в файловой системе связана с доступом или с отсутствием места на диске.
try
{
File.AppendAllText("log.txt", "Новая запись\n");
}
catch (IOException ex) when (ex.Message.Contains("Нет места"))
{
Console.WriteLine("Ошибка: На диске закончилось место!");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Ошибка: нет прав для записи файла. Запустите от имени администратора.");
}
catch (IOException ex)
{
Console.WriteLine("Другая ошибка диска: " + ex.Message);
}
Здесь мы используем и фильтр, и разделение по типам ошибок. Такая гибкость особенно ценна при разработке сложного приложения, где важно информативно реагировать на частые сбои.
4. Особенности и "подводные камни" фильтров и множественных catch
- Фильтр catch может использовать переменную исключения (ex), но не может менять её.
- Если вы бросите исключение внутри when, оно никак не "примется" этим же catch — оно пойдёт дальше вверх по стеку, как будто фильтра и не было.
- Логика фильтра становится частью контракта: ваш коллега по проекту должен знать, что не всякий IOException будет пойман — только если условие выполнилось.
- Если вы используете всего один catch на весь метод, но внутри него фильтр, то есть шанс что "лишние" исключения вы не перехватите вовсе. Поэтому, если есть сомнения — используйте явные, отдельные блоки.
- В больших приложениях можно применять фильтры для централизованной логики, например, логирования только критических ошибок.
5. Сценарии для собеседований и реальной работы
Знание фильтров и множественных catch — не только для красоты или высокого балла по clean code. Это реальный навык, который от вас могут потребовать на собеседованиях, особенно если позиция связана с поддержкой большого, сложного, распределённого приложения.
Примеры вопросов:
- Как в C# обработать разные сценарии ошибок чтения файла, чтобы для разных ситуаций (нет файла, диск занят, данные испорчены) выводить разное сообщение?
- Как не "заглушить" важную ошибку, если поставил общий блок catch (Exception)?
- Для чего нужен фильтр catch? Может ли в нем быть throw?
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ