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

throw и свои исключения

До сих пор тревогу поднимал код самой .NET: int.Parse, деление, индекс массива. Но кнопка пожарной тревоги есть на каждом этаже, и нажать её может любой. Ваш метод тоже может выбросить исключение, если ему передали данные, с которыми он не справится.

Возьмём метод, который снимает деньги со счёта. Что ему делать, если просят больше, чем лежит на счёте? Вернуть отрицательный остаток неверно. Вернуть 0 тоже: вызывающий код не узнает, что деньги так и не сняли.

Инструкция throw

Метод может выбросить исключение сам. Для этого создают объект исключения через new и передают его инструкции throw:

C#
Console.WriteLine(Withdraw(100, 30)); // 70
Console.WriteLine(Withdraw(100, 250));

int Withdraw(int balance, int amount)
{
    if (amount > balance)
    {
        throw new ArgumentException("Not enough money.");
    }
    return balance - amount;
}

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

  1. Withdraw(100, 30) проходит проверку и возвращает 70.
  2. В вызове Withdraw(100, 250) условие amount > balance истинно.
  3. new ArgumentException("Not enough money.") создаёт объект исключения с сообщением.
  4. throw выбрасывает его. Метод сразу прерывается, и return не выполняется.
  5. Исключение никто не перехватывает, и программа останавливается с отчётом.
TEXT
Unhandled exception. System.ArgumentException: Not enough money.

Ваше исключение ведёт себя так же, как исключения .NET: его можно перехватить через catch (ArgumentException ex), а ex.Message вернёт текст, переданный в конструктор.

Для своих проверок берите готовый класс .NET, если его название подходит к ошибке:

  • ArgumentException — аргумент метода неверный;
  • ArgumentOutOfRangeException — аргумент вышел за допустимые границы;
  • InvalidOperationException — действие сейчас выполнить нельзя.

Свой класс исключения

Готового класса может не найтись. Скажем, в кафе закончился чай. Это не неверный аргумент, а особая ситуация, и вызывающему коду понадобится перехватывать её отдельно. Тогда объявляют свой класс исключения, производный от Exception.

C#
try
{
    throw new OutOfStockException("No tea left.");
}
catch (OutOfStockException ex)
{
    Console.WriteLine("Sorry: " + ex.Message); // Sorry: No tea left.
}

class OutOfStockException : Exception
{
    public OutOfStockException(string message) : base(message) { }
}

Разберём пример по частям:

  • class OutOfStockException : Exception объявляет новый класс, производный от Exception. Его имя по соглашению заканчивается на Exception.
  • Конструктор принимает сообщение и через : base(message) передаёт его конструктору Exception. Оттуда ex.Message и берёт текст.
  • throw new OutOfStockException(…) выбрасывает объект нового класса, а catch (OutOfStockException ex) перехватывает именно его.

Мы выбросили исключение прямо в try, чтобы пример был коротким. В настоящей программе throw стоит в методе, как в Withdraw, а catch — в коде, который этот метод вызывает.

throw; или throw ex;

Бывает, что обработчик не может решить проблему до конца. Например, он записывает ошибку в журнал, а что делать дальше, решает вызывающий код. Тогда исключение выбрасывают дальше, и это называют повторным выбросом. Для него пишут throw; без объекта:

C#
try
{
    Console.WriteLine(int.Parse("ten"));
}
catch (FormatException ex)
{
    Console.WriteLine("Log: " + ex.Message); // Log: The input string 'ten' was not in a correct format.
    throw;
}

Программа печатает строку журнала, а затем исключение идёт дальше. Больше его никто не перехватывает, поэтому программа останавливается с отчётом о FormatException.

Этот вопрос часто задают на собеседовании: чем throw; отличается от throw ex;? Оба выбрасывают тот же объект, но по-разному обходятся с путём в отчёте. throw; сохраняет путь целиком, а throw ex; начинает его заново — со строки, где стоит throw ex;. Если в строке 8 стоит throw; или throw ex;, в отчёте этой программы остаются такие строки at:

Повторный выбросЧто видно в строках at
throw;System.Int32.Parse, затем line 3 — строка с int.Parse
throw ex;только line 8 — строка с самим throw ex;

С throw ex; из отчёта пропало место, где ошибка возникла на самом деле. При компиляции такой программы появляется предупреждение:

TEXT
warning CA2200: Re-throwing caught exception changes stack information

Поэтому для повторного выброса пишут throw;.

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

Свой класс исключения объявили без : Exception, а поле для сообщения добавили сами:

C#
throw new OutOfStockException("No tea left."); // ✗

class OutOfStockException
{
    public string Message = "";
    public OutOfStockException(string message) => Message = message;
}
TEXT
error CS0029: Cannot implicitly convert type 'OutOfStockException' to 'System.Exception'

Программа не компилируется. Выбросить можно только объект класса Exception или производного от него, а этот класс с Exception никак не связан. Допишите : Exception, уберите поле Message и передайте сообщение через : base(message), как в примере выше.

Главное

  • throw new ArgumentException("…") выбрасывает исключение: метод сразу прерывается, и исключение идёт к вызывающему коду.
  • Подходит готовый класс — берите его. Нет — объявите класс, производный от Exception, и передайте сообщение через : base(message).
  • throw; в catch выбрасывает исключение дальше и сохраняет его путь, а throw ex; начинает путь заново со своей строки.