Перейти к содержанию
Educora
Средний17 мин8 / 16

Исключения: перехват и генерация ошибок

Обрабатывай ошибки, возникающие во время работы программы, с помощью `try`, `catch` и `finally`, выбрасывай собственные исключения через `throw` и узнай, когда их лучше не использовать.

Проверь себя
В этом уроке ты узнаешь
  • Отличать ошибку компиляции от исключения во время выполнения
  • Перехватывать исключения с помощью try, нескольких блоков catch и finally
  • Выбрасывать исключение через throw и создавать собственный класс исключения
  • Выбирать для ожидаемых ошибок такие способы, как TryParse

Компилятор находит многие ошибки ещё до запуска программы, но не все. Пользователь вводит «abc» вместо возраста, файл удалён, пропал интернет, кто-то делит на ноль — такие проблемы проявляются только во время работы программы. В C# в таких случаях возникает исключение (exception). Если его не перехватить, программа остановится с сообщением «Unhandled exception»; если перехватить — можно понятно объяснить проблему пользователю и продолжить работу.

try и catch

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

C#
string[] inputs = ["42", "abc", "99999999999"];

foreach (string input in inputs)
{
    try
    {
        int number = int.Parse(input);
        Console.WriteLine($"OK: {number}");
    }
    catch (FormatException)
    {
        Console.WriteLine($"'{input}' is not a number");
    }
    catch (OverflowException)
    {
        Console.WriteLine($"'{input}' is too big for int");
    }
}
Ожидаемый результат
OK: 42
'abc' is not a number
'99999999999' is too big for int

Исключение — тоже объект: в catch (IndexOutOfRangeException ex) переменная ex содержит его данные, например ex.Message — текст пояснения. Все исключения наследуются от класса Exception, поэтому catch (Exception ex) ловит любую ошибку — пиши его всегда последним. Блок finally выполняется всегда, была ошибка или нет: в нём удобно закрыть файл или освободить ресурс.

C#
int[] scores = [90, 75, 88];

try
{
    Console.WriteLine(scores[5]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
finally
{
    Console.WriteLine("This line always runs.");
}

try
{
    int zero = 0;
    Console.WriteLine(10 / zero);
}
catch (Exception ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
Ожидаемый результат
Error: Index was outside the bounds of the array.
This line always runs.
DivideByZeroException: Attempted to divide by zero.
ИсключениеКогда возникает
FormatExceptionint.Parse("abc") — текст не является числом
DivideByZeroExceptionцелое число делится на ноль
IndexOutOfRangeExceptionиндекс, которого нет в массиве
KeyNotFoundExceptionключ, которого нет в словаре
NullReferenceExceptionобращение к члену переменной, равной null
FileNotFoundExceptionоткрываемого файла не существует
Самые частые исключения

throw: генерация исключения

Если метод не может правильно выполнить свою работу, он должен выбросить исключение через throw, а не тихо вернуть неверный результат. Готовых типов много: ArgumentException — неверный аргумент, InvalidOperationException — операция, недопустимая в текущем состоянии объекта. При необходимости можно создать собственный класс исключения, наследуемый от Exception, — настоящее применение наследования из прошлого урока.

C#
var wallet = new Wallet(100);
try
{
    wallet.Pay(30);
    wallet.Pay(250);
    wallet.Pay(10);
}
catch (NotEnoughMoneyException ex)
{
    Console.WriteLine($"Payment failed: {ex.Message}");
}
Console.WriteLine($"Left: {wallet.Balance} AZN");

class NotEnoughMoneyException(string message) : Exception(message);
class Wallet(decimal balance)
{
    public decimal Balance { get; private set; } = balance;
    public void Pay(decimal amount)
    {
        if (amount > Balance)
            throw new NotEnoughMoneyException($"need {amount} AZN, have {Balance} AZN");
        Balance -= amount;
        Console.WriteLine($"Paid {amount} AZN");
    }
}
Ожидаемый результат
Paid 30 AZN
Payment failed: need 250 AZN, have 70 AZN
Left: 70 AZN
wallet.Pay(10) так и не выполнился: после исключения остаток блока try пропускается.

Когда не стоит использовать исключения

Исключения — для непредвиденных ситуаций. А то, что пользователь введёт что-то неправильно, вполне ожидаемо, поэтому для проверки текста int.TryParse лучше, чем try/catch: код короче, работает быстрее и ясно показывает намерение. То же правило действует и для словарей: TryGetValue проверяет отсутствие ключа без всякой ошибки.

C#
string[] answers = ["15", "fifteen", "-3"];

foreach (string answer in answers)
{
    if (!int.TryParse(answer, out int age))
        Console.WriteLine($"'{answer}': please type digits");
    else if (age is < 0 or > 120)
        Console.WriteLine($"'{answer}': age must be 0-120");
    else
        Console.WriteLine($"Age accepted: {age}");
}
Ожидаемый результат
Age accepted: 15
'fifteen': please type digits
'-3': age must be 0-120

Главное

  • Исключение — ошибка во время работы программы; если его не перехватить, программа остановится.
  • Когда в блоке try возникает исключение, оставшиеся строки пропускаются и срабатывает подходящий catch.
  • Частные блоки catch пишут первыми, catch (Exception) — последним; finally выполняется всегда.
  • throw new ...Exception("...") сообщает о недопустимой ситуации; собственные исключения наследуй от Exception.
  • Для ожидаемых ошибок TryParse и TryGetValue лучше исключений.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какое исключение вызовет int.Parse("abc")?