Перейти к содержимому
Теория · Исключения · урок 1 из 9

Что такое исключение

Чему вы научитесь

Чему вы научитесь:

  • читать отчёт о необработанном исключении и находить по нему место ошибки;
  • перехватывать исключения блоками try и catch и выбирать обработчик по классу;
  • объяснять, когда выполняется finally, в том числе после return;
  • выбрасывать свои исключения через throw и освобождать через using то, что держит объект.
Здание в разрезе: тревога с этажа int.Parse спускается через этаж Sum на этаж Main, где её некому принять

Представьте офисное здание. На пятом этаже задымилась розетка. Сотрудник не тушит её сам: он нажимает кнопку пожарной тревоги.

Сигнал идёт с этажа на этаж — на четвёртый, на третий, и так до охраны на первом. Кто-то по пути должен ответить на тревогу. Если не ответит никто, здание эвакуируют, и работа в нём останавливается целиком.

Программа устроена похоже. Этажи — это вызовы методов: внизу Main; над ним метод, который он вызвал; выше следующий вызов. Это тот же стек вызовов из темы о методах: последний вызов лежит сверху.

Когда метод не может выполнить свою работу, он поднимает тревогу: выбрасывает исключение. Исключение спускается по цепочке вызовов этаж за этажом: от метода к тому, кто его вызвал, и так до Main. Если его никто не перехватит, программа остановится.

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

С исключениями вы уже встречались. Каждое из них останавливало программу:

  • FormatException — int.Parse получил строку, в которой нет числа;
  • DivideByZeroException — целое число разделили на ноль;
  • OverflowException — число переполнилось внутри checked;
  • IndexOutOfRangeException — индекс вышел за границы массива;
  • ArgumentOutOfRangeException — Substring попросили кусок за концом строки;
  • KeyNotFoundException — в словаре нет такого ключа;
  • NullReferenceException — к члену объекта обратились через null.

Посмотрим, что происходит, когда ошибка случается не в инструкциях верхнего уровня, а в методе, который они вызвали:

C#
Console.WriteLine("Start"); // Start
int total = Sum("12", "ten");
Console.WriteLine(total);

int Sum(string a, string b)
{
    int x = int.Parse(a);
    int y = int.Parse(b); // ✗
    return x + y;
}

Путь исключения

Давайте разберём программу по шагам:

  1. Console.WriteLine("Start") печатает Start.
  2. Sum("12", "ten") вызывает метод, и инструкции верхнего уровня ждут его результат.
  3. В Sum строка int x = int.Parse(a); кладёт в x число 12.
  4. int.Parse(b) не может превратить "ten" в число и выбрасывает FormatException.
  5. Sum прерывается на этой строке: return x + y; не выполняется, и результата нет.
  6. Исключение возвращается к строке с вызовом Sum. Там его тоже никто не перехватывает, поэтому Console.WriteLine(total) не выполняется.
  7. Программа печатает отчёт об ошибке и останавливается.

Первая строка отчёта:

TEXT
Unhandled exception. System.FormatException: The input string 'ten' was not in a correct format.

Под ней идут строки, которые начинаются с at. Это путь исключения по цепочке вызовов, от места, где его выбросили, до точки входа <Main>$. Первыми стоят методы самой .NET, например System.Int32.Parse.

Ниже — ваш Sum с пометкой line 8 и <Main>$ с пометкой line 2: это номера строк файла. По ним вы и найдёте в отчёте место ошибки. Имя Sum там записано как <<Main>$>g__Sum|0_0: так компилятор называет метод, объявленный среди инструкций верхнего уровня.

Исключение, которое так никто и не перехватил, называют необработанным. Это и значат первые слова отчёта: Unhandled exception. На собеседовании часто просят объяснить, что с ним происходит: оно проходит всю цепочку вызовов, и программа завершается с отчётом.

Исключение — это объект

System.FormatException — полное имя класса: пространство имён System и имя FormatException. В момент ошибки .NET создаёт объект этого класса и кладёт в него сообщение. В отчёте видно и то, и другое:

Часть отчётаЧто она значит
Unhandled exception.исключение никто не перехватил
System.FormatExceptionкласс исключения: что за ошибка случилась
The input string 'ten' was…сообщение: подробности этой ошибки

Классы исключений связаны наследованием: все они производные от одного базового класса Exception. Класс говорит, какая беда случилась, а сообщение — что именно пошло не так.

Ошибка компиляции и исключение

Ошибку компиляции находит компилятор ещё до запуска. Программа с ней не запустится вовсе, а в сообщении будет код вроде CS1002.

Исключение возникает во время работы и зависит от данных. Программа с Sum("12", "ten") компилируется без единого предупреждения. Та же программа с Sum("12", "30") отработала бы без ошибок и напечатала 42. Компилятор не знает, какой текст придёт в a и b, поэтому такую ошибку видно только при запуске.

Частая ошибка

Нужно сложить числа из массива строк. Ждут, что программа пропустит строку "x" и напечатает 8:

C#
string[] items = ["3", "x", "5"];
int sum = 0;
foreach (string item in items)
{
    sum += int.Parse(item); // ✗
}
Console.WriteLine(sum);
TEXT
Unhandled exception. System.FormatException: The input string 'x' was not in a correct format.

Программа компилируется, но на втором элементе останавливается. Исключение прерывает не один проход цикла, а весь foreach и всю программу: до Console.WriteLine(sum) дело не доходит. Чтобы пропускать такие строки, их проверяют через int.TryParse из темы о методах или перехватывают исключение.

Главное

  • Метод, который не может выполнить работу, выбрасывает исключение, и оно идёт назад по цепочке вызовов до Main.
  • Каждый метод на этом пути прерывается: строки после места ошибки не выполняются.
  • Необработанное исключение останавливает программу. Первая строка отчёта называет класс и сообщение, строки at — путь и номера строк.
  • Исключение — объект класса, производного от Exception, с сообщением внутри.