This lesson is not translated yet – the Russian original is below.
Не всякую ошибку можно предусмотреть проверкой. Файла может не оказаться на месте, сеть – отвалиться, пользователь – ввести буквы вместо цифр. Для таких ситуаций в C# есть исключения.
Что происходит без обработки
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[10]);
Программа аварийно завершится, напечатав тип ошибки, сообщение и путь вызовов:
Unhandled exception. System.IndexOutOfRangeException:
Index was outside the bounds of the array.
at Program.Main() in Program.cs:line 2
Это стек вызовов – список методов от места ошибки до точки входа. Читается снизу вверх и почти всегда отвечает на вопрос «откуда это взялось».
Не закрывайте окно с ошибкой, не прочитав первые три строки. Тип исключения и номер строки обычно объясняют проблему целиком, а поиск по одному лишь тексту сообщения уводит в чужие истории.
try и catch
try
{
int number = int.Parse(Console.ReadLine());
Console.WriteLine(100 / number);
}
catch (FormatException)
{
Console.WriteLine("Это не число");
}
catch (DivideByZeroException)
{
Console.WriteLine("На ноль делить нельзя");
}
В try – код, который может не получиться. В catch – что делать, если не получилось. Как только
внутри try возникло исключение, оставшиеся строки блока пропускаются и управление уходит в
подходящий catch.
Блоки проверяются сверху вниз, и срабатывает первый подходящий. Отсюда правило: частные типы идут выше общих.
catch (Exception ex)
{
Console.WriteLine($"Что-то пошло не так: {ex.Message}");
}
Exception – предок всех исключений, он ловит всё. Поставив его первым, вы сделаете остальные
блоки недостижимыми – компилятор об этом предупредит.
Что внутри исключения
Объект исключения несёт больше, чем текст:
| Свойство | Что содержит |
|---|---|
Message |
описание для человека |
StackTrace |
путь вызовов до места ошибки |
InnerException |
исходное исключение, если это обёртка над другим |
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
Никогда не пишите пустой catch { }. Программа продолжит работу в состоянии, которого вы не ожидали, и настоящая причина сбоя проявится позже и совсем в другом месте. Если исключение действительно можно игнорировать, напишите в блоке комментарий, почему.
finally
Блок, который выполняется всегда – и после удачного завершения, и после catch, и даже если из
try сработал return:
StreamReader reader = null;
try
{
reader = new StreamReader("data.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
Console.WriteLine("Файла нет");
}
finally
{
reader?.Close();
}
Смысл в освобождении ресурсов: файл нужно закрыть в любом случае, иначе он останется заблокирован.
Для объектов, которые умеют освобождаться сами, есть более короткая форма – using:
using var reader = new StreamReader("data.txt");
Console.WriteLine(reader.ReadToEnd());
Файл закроется при выходе из области видимости, даже если случится исключение. Компилятор
разворачивает эту строку в try-finally – вручную писать то же самое не нужно.
Бросить исключение самому
Когда метод не может выполнить свою работу, он сообщает об этом словом throw:
static int Divide(int a, int b)
{
if (b == 0)
{
throw new ArgumentException("Делитель не может быть нулём", nameof(b));
}
return a / b;
}
Готовые типы под частые ситуации уже есть, и брать нужно самый точный:
| Тип | Когда |
|---|---|
ArgumentException |
аргумент бессмысленный |
ArgumentNullException |
аргумент равен null, а не должен |
ArgumentOutOfRangeException |
аргумент вне допустимого диапазона |
InvalidOperationException |
вызов неуместен в текущем состоянии объекта |
NotSupportedException |
операция не поддерживается |
Оператор nameof(b) подставляет имя параметра строкой. Переименуете параметр – текст обновится
сам, потому что это не строка, а ссылка на имя.
Исключения – не замена проверкам
Главная ошибка новичка – использовать исключения как обычное ветвление:
try
{
int number = int.Parse(input);
}
catch (FormatException)
{
number = 0;
}
Здесь исключение обрабатывает ожидаемую ситуацию: пользователь вполне может ввести что угодно. Правильный вариант – проверка:
int.TryParse(input, out int number);
Разница не только в красоте. Выброс исключения обходится в тысячи процессорных тактов – это дорогая операция, рассчитанная на редкие случаи.
Отсюда простой критерий. Ситуация ожидаемая и проверяемая заранее (пустой ввод, отсутствующий ключ) – проверяйте условием. Ситуация исключительная и от вас не зависящая (сеть отвалилась, диск заполнен) – ловите исключение.
Что ловить, а что нет
Ловить исключение стоит, только если вы знаете, что с ним делать: показать понятное сообщение, попробовать ещё раз, взять запасной источник данных.
Если делать с ним нечего – не ловите. Пусть оно поднимется выше, к тому коду, который в состоянии принять решение. Исключение, проглоченное в глубине программы, превращает понятную аварию в загадочное неправильное поведение – а его отлаживать в разы дороже.