throw и свои исключения
До сих пор тревогу поднимал код самой .NET: int.Parse, деление, индекс массива. Но кнопка пожарной тревоги есть на каждом этаже, и нажать её может любой. Ваш метод тоже может выбросить исключение, если ему передали данные, с которыми он не справится.
Возьмём метод, который снимает деньги со счёта. Что ему делать, если просят больше, чем лежит на счёте? Вернуть отрицательный остаток неверно. Вернуть 0 тоже: вызывающий код не узнает, что деньги так и не сняли.
Инструкция throw
Метод может выбросить исключение сам. Для этого создают объект исключения через new и передают его инструкции throw:
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;
}Давайте разберём программу по шагам:
Withdraw(100, 30)проходит проверку и возвращает70.- В вызове
Withdraw(100, 250)условиеamount > balanceистинно. new ArgumentException("Not enough money.")создаёт объект исключения с сообщением.throwвыбрасывает его. Метод сразу прерывается, иreturnне выполняется.- Исключение никто не перехватывает, и программа останавливается с отчётом.
Unhandled exception. System.ArgumentException: Not enough money.Ваше исключение ведёт себя так же, как исключения .NET: его можно перехватить через catch (ArgumentException ex), а ex.Message вернёт текст, переданный в конструктор.
Для своих проверок берите готовый класс .NET, если его название подходит к ошибке:
ArgumentException— аргумент метода неверный;ArgumentOutOfRangeException— аргумент вышел за допустимые границы;InvalidOperationException— действие сейчас выполнить нельзя.
Свой класс исключения
Готового класса может не найтись. Скажем, в кафе закончился чай. Это не неверный аргумент, а особая ситуация, и вызывающему коду понадобится перехватывать её отдельно. Тогда объявляют свой класс исключения, производный от Exception.
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; без объекта:
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; из отчёта пропало место, где ошибка возникла на самом деле. При компиляции такой программы появляется предупреждение:
warning CA2200: Re-throwing caught exception changes stack informationПоэтому для повторного выброса пишут throw;.
Частая ошибка
Свой класс исключения объявили без : Exception, а поле для сообщения добавили сами:
throw new OutOfStockException("No tea left."); // ✗
class OutOfStockException
{
public string Message = "";
public OutOfStockException(string message) => Message = message;
}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;начинает путь заново со своей строки.