Обработка исключений в C#
Ошибка vs исключение в энциклопедии; в .NET исключения — управляемые отклонения, системные OutOfMemory — отдельный класс критичных сбоев.
Иерархия типов — Иерархия классов исключений в C#.
Обработка исключений в C#
Исключения
Исключение (exception) — это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения и требует специальной обработки. Название "исключение" (exception) означает отклонение от нормального сценария.
Ошибка (error) — это, как правило, системный сбой, который невозможно обработать (например, нехватка памяти). Исключение (exception) — это управляемое отклонение, которое можно предвидеть, перехватить и обработать.
Пример: Пользователь ввёл текст вместо числа. Это исключительная ситуация, которую программа может и должна обработать — например, попросить ввести число ещё раз. Таким образом, исключения — это часть логики приложения.
Интерактивное демо — часть сценариев на Python (
try/except); в C# синтаксис свой, но стек вызовов и раскрутка те же. Подробнее: ошибки и исключения.
Play ITЗагрузка интерактивного демо…
В .NET исключение — объект System.Exception, поднимающийся по стеку до catch; finally и using гарантируют освобождение ресурсов (аналог блока с гарантированным завершением). CLR может собрать отчёт об ошибке при необработанном сбое. Различайте исключение (перехватываемый сбой в логике или I/O) и фатальные состояния среды, близкие к Error в Java.
Общая теория — ошибки и исключения; дерево типов — Иерархия классов исключений в C#.
System.Exception
Все исключения в C# основаны на классе System.Exception. Это — базовый класс для всех исключений. .NET framework предоставляет класс System.Exception для обработки различных типов исключений, которые имеют место. Класс исключений является базовым классом среди других классов исключений.
System.Exception
├── SystemException (базовый класс для исключений среды .NET)
├── ArgumentException
│ ├── ArgumentNullException
│ └── ArgumentOutOfRangeException
├── InvalidOperationException
├── NullReferenceException
├── IOException
├── FormatException
└── DivideByZeroException
Разбор:
- Диаграмма показывает дерево наследования исключений в .NET.
System.Exception— корневой базовый класс для всех исключений.- Более специфичные типы (
ArgumentNullException,FormatException) помогают точнее диагностировать проблему. - В обработке лучше сначала ловить узкие типы, а
Exceptionоставлять последним.
Все исключения — это объекты. Это значит, что у них есть свойства, методы и можно их расширять.
try, catch, finally
Основные блоки – try, catch, finally.
try {
// Блок кода, в котором может произойти ошибка
}
catch (ExceptionType ex) {
// Обработка исключения
}
finally {
// Код, который выполнится всегда (например, освобождение ресурсов)
}
Разбор:
tryоборачивает код, где может возникнуть ошибка.catchперехватывает исключение указанного типа и даёт доступ к объекту ошибки (ex).finallyвыполняется независимо от того, была ошибка или нет.- Такая конструкция отделяет "обычную" логику от аварийной.
try — блок с "опасным" кодом. Содержит код, который может выбросить исключение.
try
{
int number = int.Parse(input); // Может выбросить FormatException
Console.WriteLine(100 / number); // Может выбросить DivideByZeroException
}
Разбор:
int.Parse(input)может завершитьсяFormatException, если строка не число.- Деление
100 / numberможет выброситьDivideByZeroException, еслиnumber == 0. - Оба потенциально опасных шага сгруппированы в одном
try. - Это упрощает централизованную обработку ошибок ниже в
catch.
catch — обработка исключения. Выполняется, если в try возникло исключение указанного типа.
catch (FormatException ex)
{
Console.WriteLine("Неверный формат числа.");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Деление на ноль!");
}
Разбор:
- Первый
catchобрабатывает ошибки формата входных данных. - Второй
catchотдельно ловит деление на ноль. - Разделение по типам позволяет выдавать точные сообщения пользователю.
- Порядок
catchважен: более конкретные типы должны идти раньше общих.
Можно ловить общее исключение:
catch (Exception ex)
{
Console.WriteLine($"Произошла ошибка: {ex.Message}");
}
Разбор:
catch (Exception ex)перехватывает почти любые необработанные ошибки.ex.Messageсодержит текстовое описание причины исключения.- Интерполяция строки
$"..."подставляет сообщение в вывод. - Такой блок полезен как fallback, но лучше дополнять его специализированными
catch.
Ловить Exception — последнее средство. Лучше указывать конкретные типы.
Можно использовать несколько catch-блоков, чтобы обработать разные типы исключений по-разному:
try {
// ...
} catch (FileNotFoundException ex) {
Console.WriteLine("Файл не найден: " + ex.Message);
} catch (UnauthorizedAccessException ex) {
Console.WriteLine("Нет доступа: " + ex.Message);
} catch (Exception ex) {
Console.WriteLine("Другая ошибка: " + ex.Message);
}
Разбор:
- Пример показывает каскад обработчиков от узких к широким типам.
FileNotFoundExceptionиUnauthorizedAccessExceptionобрабатываются отдельно.- Последний
catch (Exception)ловит всё, что не подошло под предыдущие ветки. - Такой подход делает обработку ошибок предсказуемой и прозрачной.
При наличии нескольких catch-блоков важен порядок. Более специфичные исключения должны идти раньше, чем общие.
finally — блок, который выполняется всегда. Выполняется в любом случае — было ли исключение, был ли return, break или goto.
finally
{
// Освобождение ресурсов
file?.Close();
connection?.Dispose();
}
Разбор:
finallyиспользуется для обязательной очистки ресурсов.file?.Close()вызывает метод только еслиfileнеnull.connection?.Dispose()корректно освобождает соединение.- Это защищает от утечек даже при исключениях и раннем выходе.
Он используется для закрытия файлов, освобождения соединений, очистки ресурсов. Начиная с C# 8, для таких задач лучше использовать using, но finally всё ещё актуален.
Когда finally может не выполниться? На практике — при аварийном завершении процесса: Environment.FailFast, необработанное исключение в финализаторе, принудительное убийство процесса ОС (kill -9), сбой питания. В нормальном управляемом коде при выходе из try/catch блок finally выполняется даже при return или повторном throw.
throw — генерация исключения вручную
if (age < 0)
{
throw new ArgumentException("Возраст не может быть отрицательным.", nameof(age));
}
Разбор:
- Это guard-проверка входных данных перед основной логикой.
- При
age < 0вручную выбрасываетсяArgumentException. nameof(age)безопасно передаёт имя параметра без "магической строки".- Такой стиль делает причину ошибки явной и облегчает диагностику.
Можно выбрасывать:
- Новые исключения: throw new ArgumentException(...);
- Перебрасывать текущее: throw; (сохраняет стек-трейс);
- Перебрасывать с изменением: throw ex; (теряет оригинальный стек — плохо!).
Типы исключений
Типы исключений:
| Исключение | Когда возникает? |
|---|---|
ArgumentException | Передан недопустимый аргумент |
ArgumentNullException | Аргумент равен null |
ArgumentOutOfRangeException | Аргумент выходит за пределы допустимого диапазона |
InvalidOperationException | Недопустимая операция в текущем состоянии объекта |
IOException | Ошибки ввода/вывода (файлы, сеть) |
NullReferenceException | Попытка вызвать метод у null-объекта |
IndexOutOfRangeException | Выход за границы массива |
FormatException | Неверный формат данных |
DivideByZeroException | Деление на ноль (для целых чисел) |
Совет: не ловите NullReferenceException — лучше проверяйте на null заранее.
Исключения, которые нельзя "остановить" в catch
Некоторые ситуации не предназначены для обычной обработки в catch:
| Ситуация | Поведение |
|---|---|
StackOverflowException | Переполнение стека — процесс обычно завершается |
OutOfMemoryException при исчерпании кучи | Может быть неперехватываемым в критическом состоянии |
AccessViolationException / повреждение нативной памяти | Часто фатально для процесса |
ThreadAbortException | Устаревший сценарий (.NET Framework); в .NET Core+ — не используйте Thread.Abort |
В ASP.NET Core middleware перехватывает необработанные исключения запроса и возвращает ответ клиенту, но не спасает процесс от фатальных сбоев CLR.
Как читать стек-трейс (stack trace)?
Когда исключение не перехвачено, C# выводит стек-трейс — цепочку вызовов, по которой "всплывало" исключение. Для обычного пользователя это выглядит как "ошибка" с "крокозябрами", но нам нужно уметь их читать.
Пример:
System.FormatException: Input string was not in a correct format.
at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)
at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)
at System.Int32.Parse(String s)
at MyApp.Program.ParseAge(String input) in Program.cs:line 15
at MyApp.Program.Main(String[] args) in Program.cs:line 8
Разбор:
- Первая строка сообщает тип исключения и его текст.
- Строки
at ...показывают стек вызовов, по которому ошибка "поднималась" вверх. Program.cs:line 15указывает точную строку в пользовательском коде.- Анализируют обычно от пользовательских фреймов к системным, чтобы быстрее найти источник проблемы.
Как читать:
Первая строка — тип исключения и сообщение.
Строки ниже — порядок вызовов "снизу вверх":
- Main вызвал ParseAge
- ParseAge вызвал int.Parse
- int.Parse выбросил исключение.
:line XX — номер строки в файле.
Это главный инструмент при отладке - чтобы понять, в чём проблема, нужно пойти по цепочки снизу вверх, и верхний метод будет говорить об ошибке.
AggregateException
AggregateException — исключения в многопоточности. В асинхронном и параллельном программировании (например, Task.WhenAll, Parallel.ForEach) может возникнуть несколько исключений одновременно. .NET объединяет их в один объект — AggregateException.
Пример:
try
{
await Task.WhenAll(task1, task2, task3);
}
catch (AggregateException ex)
{
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine($"Ошибка: {inner.Message}");
}
}
Разбор:
Task.WhenAll(...)ждёт завершения всех задач одновременно.- При нескольких сбоях ошибки собираются в
AggregateException. ex.InnerExceptionsсодержит список внутренних исключений.foreachпозволяет вывести или обработать каждую ошибку отдельно.
AggregateException содержит коллекцию InnerExceptions, каждая из которых — отдельная ошибка.
Можно создавать свои классы исключений. Главное чтобы они наследовались от исключений.
public class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }
public InsufficientFundsException(decimal balance, decimal amount)
: base($"Недостаточно средств: баланс {balance}, запрошено {amount}.")
{
Balance = balance;
Amount = amount;
}
}
Разбор:
- Создаётся пользовательское доменное исключение, унаследованное от
Exception. - Свойства
BalanceиAmountсохраняют полезный контекст ошибки. - Конструктор формирует понятное сообщение через
base(...). - Такой тип удобно ловить отдельно и использовать в бизнес-правилах финансовых операций.