Перейти к основному содержимому

5.05. Исключения

Разработчику Архитектору

Исключения

Исключение (exception) — это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения и требует специальной обработки. Название «исключение» (exception) не случайно: оно означает отклонение от нормального сценария, а не просто «ошибку».

Ошибка (error) — это, как правило, системный сбой, который невозможно обработать (например, нехватка памяти). Исключение (exception) — это управляемое отклонение, которое можно предвидеть, перехватить и обработать.

Пример: Пользователь ввёл текст вместо числа. Это не «ошибка системы», а исключительная ситуация, которую программа может и должна обработать — например, попросить ввести число ещё раз. Таким образом, исключения — это не сбои, а часть логики приложения.

Все исключения в C# основаны на классе System.Exception. Это — базовый класс для всех исключений. .NET framework предоставляет класс System.Exception для обработки различных типов исключений, которые имеют место. Класс исключений является базовым классом среди других классов исключений.

System.Exception
├── System.SystemException (внутренние исключения .NET)
├── System.ApplicationException (устарело, не рекомендуется)
├── ArgumentException
│ ├── ArgumentNullException
│ └── ArgumentOutOfRangeException
├── InvalidOperationException
├── NullReferenceException
├── IOException
├── FormatException
└── DivideByZeroException

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

Основные блоки – try, catch, finally.

try {
// Блок кода, в котором может произойти ошибка
}
catch (ExceptionType ex) {
// Обработка исключения
}
finally {
// Код, который выполнится всегда (например, освобождение ресурсов)
}

try — блок с «опасным» кодом. Содержит код, который может выбросить исключение.

try
{
int number = int.Parse(input); // Может выбросить FormatException
Console.WriteLine(100 / number); // Может выбросить DivideByZeroException
}

catch — обработка исключения. Выполняется, если в try возникло исключение указанного типа.

catch (FormatException ex)
{
Console.WriteLine("Неверный формат числа.");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Деление на ноль!");
}

Можно ловить общее исключение:

catch (Exception ex)
{
Console.WriteLine($"Произошла ошибка: {ex.Message}");
}

Ловить Exception — последнее средство. Лучше указывать конкретные типы.

Можно использовать несколько catch-блоков, чтобы обработать разные типы исключений по-разному:

try {
// ...
} catch (FileNotFoundException ex) {
Console.WriteLine("Файл не найден: " + ex.Message);
} catch (UnauthorizedAccessException ex) {
Console.WriteLine("Нет доступа: " + ex.Message);
} catch (Exception ex) {
Console.WriteLine("Другая ошибка: " + ex.Message);
}

При наличии нескольких catch-блоков важен порядок. Более специфичные исключения должны идти раньше, чем общие.

finally — блок, который выполняется всегда. Выполняется в любом случае: было ли исключение, был ли return, break или goto.

finally
{
// Освобождение ресурсов
file?.Close();
connection?.Dispose();
}

Он используется для закрытия файлов, освобождения соединений, очистки ресурсов. Начиная с C# 8, для таких задач лучше использовать using, но finally всё ещё актуален.

throw — генерация исключения вручную


if (age < 0)
{
throw new ArgumentException("Возраст не может быть отрицательным.", nameof(age));
}

Можно выбрасывать:

  • Новые исключения: throw new ArgumentException(...);
  • Перебрасывать текущее: throw; (сохраняет стек-трейс);
  • Перебрасывать с изменением: throw ex; (теряет оригинальный стек — плохо!).

Типы исключений:

ИсключениеКогда возникает?
ArgumentExceptionПередан недопустимый аргумент
ArgumentNullExceptionАргумент равен null
ArgumentOutOfRangeExceptionАргумент выходит за пределы допустимого диапазона
InvalidOperationExceptionНедопустимая операция в текущем состоянии объекта
IOExceptionОшибки ввода/вывода (файлы, сеть)
NullReferenceExceptionПопытка вызвать метод у null-объекта
IndexOutOfRangeExceptionВыход за границы массива
FormatExceptionНеверный формат данных
DivideByZeroExceptionДеление на ноль (для целых чисел)

Совет: не ловите NullReferenceException — лучше проверяйте на null заранее.

Как читать стек-трейс (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

Как читать:

Первая строка — тип исключения и сообщение.

Строки ниже — порядок вызовов «снизу вверх»:

  • Main вызвал ParseAge
  • ParseAge вызвал int.Parse
  • int.Parse выбросил исключение.

:line XX — номер строки в файле.

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

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}");
}
}

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;
}
}