Функция Some Function () помещает некоторую часть кода в блок, помеченный ключевым словом try. Любая функция, вызываемая в этом блоке (или функция, вызы ваемая функцией, вызываемой в этом блоке — и так далее...), рассматривается как вы званная в данном try-блоке.
Непосредственно за блоком try следует ключевое слово catch с блоком, которому передается управление в случае, если где-то в try-блоке произошла ошибка. Аргумент catch-блока — объект класса Exception (или некоторого подкласса Exception).
Однако catch-блок не обязан иметь аргументы: пустой catch перехватывает все
исключения, как и catch(Exception): catch
Если вам не нужен доступ к информации из объекта перехваченного исключения, вы можете указать в блоке только тип исключения:
catch (MyException)
{
// Действия, которые не требуют обращения к объекту // исключения
}
Блок finally— если таковой имеется в вашем исходном тексте — выполняется даже в случае перехвата исключения, не говоря уже о том, что он выполняется при нор мальной работе. Обычно он предназначается для "уборки" — закрытия открытых фай лов, освобождения ресурсов и т.п.
В отличие от исключений С++, в которых аргументом catch может быть объ ект произвольного типа, исключения С# требуют, чтобы он был объектом клас са Exception или производного от него.
Итак, где-то в дебрях кода на неизвестно каком уровне вызовов в функции SomeO-
t h e r F u n c t i o n ( )
случилась ошибка... Функция сообщает об этом, генерируя исклю-
Глава 18. Эти
исключительные исключения
401
чение в виде объекта Exception и передает его с помощью оператора throw вверх цепочке вызовов в первый же блок, который в состоянии перехватить его и обработан!
Иногда обработчик try/catch располагается в той же функции, в которой нерировано исключение. Первая же функция, владеющая достаточным количес вом контекстуальной информации для выполнения действий по его обработке может перехватить и обработать его; если данная функция не в состоянии этого сделать, исключение передается дальше вверх по цепочке вызовов.
В демонстрационной программе FactorialException приведены ключевые элементы механизма исключений.
//FactorialException - создание функции вычисления
//факториала, которая сообщает о некорректном аргументе с
//использованием исключений
using System;
namespace FactorialException
{
//MyMathFunctions - набор созданных мною математических
//функций
public class MyMathFunctions
{
//Factorial - возвращает факториал переданного
//аргумента
public static double Factorial(int nValue)
{
// Проверка: отрицательные значения запрещены if (nValue < 0)
{
// Сообщение об отрицательном аргументе string s = String.Format(
"Отрицательный аргумент в вызове Factorial { о } " , nValue);
throw new Exception(s); // Генерация исключения...
}
//начинаем со значения аккумулятора,
//равного 1
double dFactorial = 1.0;
//Цикл со счетчиком nValue, уменьшающимся до 1, с
//умножением на каждой итерации значения аккумулятора
//на величину счетчика
do
{
dFactorial *= nValue,- } while(--nValue > 1);
// Возвращаем вычисленное значение return dFactorial;
402
Часть VII. Дополнительные главн
public class Program
{
public static void Main(string[] args)
{
try // Исключения от функции Factorial() "всплывут" до // этого блока
{
//Вызов функции вычисления факториала в
//цикле от 6 до -6
for (int i = 6; i > -6; i--)
{
// Вычисление факториала
double dFactorial = MyMathFunctions.Factorial(i); // Вывод результата на каждой итерации Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));
catch(Exception e) // ... перехват исключения
{
Console.WriteLine("Ошибка:");
Console.WriteLine(e.ToString());
}
// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +
"завершения программы...");
Console.Read();
Эта "исключительная" версия функции Main () практически полностью находится в try-блоке.
Всегда помещайте содержимое функции Main () в try-блок, поскольку функ ция Main () — начальная и конечная точка программы. Любое исключение, не перехваченное где-то в другом месте, будет передано функции Main ( ) . Это последняя возможность перехватить исключение перед тем, как оно попадет прямо в Windows, где это сообщение об ошибке будет гораздо сложнее интер претировать.
Блок catch в конце функции Main () перехватывает объект Exception и использует его метод ToString () для вывода информации об ошибке, содержащейся в этом объекте в виде строки.
Более консервативное свойство Exception.Message возвращает более удо бочитаемую, но менее информативную информацию по сравнению с предос тавляемой методом е . ToString ().
Эта версия функции Factorial () включает ту же проверку на отрицательность пе реданного аргумента, что и предыдущая (для экономии места в ней опущена проверка того, что аргумент — целое число). Если аргумент отрицателен, функция Factorial ()
403
форматирует сообщение об ошибке с описанием ситуации, включая само отрицательное значение, вызвавшее ошибку. Затем функция Factorial () вносит информацию в вновь создаваемый объект типа Exception, который передается с помощью механизма исключений вызывающей функции.
Вывод этой программы выглядит следующим образом:
i
=
6,
факториал
=
720
i
=
5,
факториал
=
120
i
=
4,
факториал
=
24
i
=
3,
факториал
=
6
i
=
2,
факториал
=
2
i
=
1,
факториал
=
1
i = 0, факториал = 0 Ошибка:
System.Exception: Отрицательный аргумент в вызове Factorial -1
at
Factorial(Int32 nValue) in
с:\c#programs\Factorial\Program.cs:line
21
at
FactorialException.Program.Main(String[]
args) in
с:\c#programs\Factorial\Program.cs:line
49
Нажмите <Enter> для завершения программы...
В первых нескольких строках выводятся корректно вычисленные факториалы число от 6 до 09. Попытка вычислить факториал -1 приводит к генерации исключения.
В первой строке сообщения об ошибке выводится информация, сгенерированная в функции Factorial ( ) . Эта строка описывает природу ошибки, включая вызвавшее
неприятности значение аргумента
1.
В оставшейся части вывода выполняется трассировка стека. В первой строке указы вается, в какой функции сгенерировано исключение. В данном случае это было сделано в функции Factorial (int) — а именно в 21 строке исходного файла Program.cs Функция Factorial () была вызвана из функции Main (string [] ) в строке 49 того же файла. На этом трассировка файла прекращается, поскольку функция Main () содер жит блок, перехвативший и обработавший указанное исключение.
Трассировка стека доступна в одном из окон отладчика Visual Studio.
Вы должны согласиться, что это весьма впечатляюще. Сообщение об ошибке описы вает случившееся и позволяет указать аргумент, приведший к ней. Трассировка стека полностью отслеживает, где именно и в результате какой последовательности вызовов произошла ошибка. При такой диагностике поиск ошибки и ее причины не должен со ставить никакого труда.
Получение подробной информации, такой как трассировка стека, удобно в про цессе разработки и отладки, но при выходе программы к конечным пользовате лям вам наверняка захочется, чтобы она выдавала более простые и понятные со общения. (При этом все равно останется возможность записи трассировки стека в журнальный файл, чтобы вы могли владеть всей необходимой информацией, когда пользователь обратится к вам за помощью.)
9 Еще раз напомним читателю, что в математике принято считать, что 0! = 1. — Примеч. ред.
404
Часть VII. Дополнительные главы
Стандартный класс исключения Exception, предоставляемый библиотекой С#, в состоянии предоставить вам достаточное количество информации. Вы можете запро сить объект исключения о том, где он был сгенерирован, какая строка была передана ему генерирующей функцией. Однако в ряде случаев стандартного класса Exception быва ет недостаточно. У вас может оказаться слишком много информации, чтобы разместить ее в одной строке. Например, функция приложения может захотеть передать вызвавший проблемы объект для последующего анализа. Изучение этого объекта может быть полез ным вплоть до полного восстановления после происшедшей ошибки.
Локально определенный класс может наследовать класс Exception так же, как и любой другой класс. Однако пользовательский класс исключения должен наследовать не непосредственно класс Exception, а класс ApplicationException, являющийся подклассом Exception, как показано в следующем фрагменте исходного текста:
//CustomException - добавление ссылки на MyClass к
//стандартному классу исключения
public class CustomException : ApplicationException
{
private MyClass myobject; // Хранит ссылку на вызвавший // проблемы объект
//Предоставляет доступ к объекту, сохраненному в объекте
//исключения
public MyClass MyCustomObject{ get {return myobject;}}
}
Класс CustomException представляет собой самодельный класс для сообщения об ошибке в любой программе, работающей с классом MyClass. Этот подкласс класса Ap plicationException содержит такую же строку, как и исходный класс, но добавляет к ней ссылку на объект MyClass, вызвавший проблемы. Это позволяет произвести де тальное исследование случившегося.
В приведенном далее примере выполняется перехват исключения CustomExcep tion и используется информация об объекте MyClass:
public class Program
{
public void SomeFunction()
{
try
{
//... действия перед вызовом демонстрационной функции SomeOtherFunctionO ;
//... продолжение работы ...
}
catch(MyException me)
[лава{ 18. ЭТИ исключительные исключения
405
//Здесь у вас имеется доступ к методам Exception и
//ApplicationException
string s = me.ToStringО;
// Но у вас есть еще и доступ к методам, уникальным
//для вашего класса исключения MyClass mo = me.MyCustomObject;
//Например, вы можете запросить у объекта MyClass его
//собственное описание
string s = mo.GetDescription();
}
}
public void SomeOtherFunctionO
{
// Создание myobject
MyClass myobject
= new M y C l a s s 0 ;
// ... сообщение об ошибке с участием myobject ..
throw new MyException("Ошибка в объекте
MyClass",
myobj ect) ;
// ... Остальная
часть функции ...
}
}
В этом фрагменте кода
функция
SomeFunction ()
вызывает функцию SomeO-|
therFunction () из охватывающего
блока try. Функция SomeOtherFunction()
создает и использует объект myobject. Где-то в функции SomeOtherFunction() программа проверки ошибок подготавливает исключение к генерации для сообщения о происшедшей ошибке. Вместо создания простого объекта типа Exception или Ap plicationException, функция SomeFunction О применяет разработанный ваш тип MyExcept ion, пригодный не только для передачи текстового сообщения об ошиб ке, но и ссылки на вызвавший ее объект.
Блок catch в функции Main () указывает, что он предназначен для перехвата объ ектов MyExcept ion. После того как такой объект перехвачен, код приложения в со стоянии применить все методы Exception, как, например, метод ToString ( ) . Одна ко в этом catch-блоке может использоваться и другая информация, к примеру, вызов методов объекта MyClass, ссылка на который передана в объекте исключения.
Фрагмент кода в предыдущем разделе продемонстрировал генерацию и перехват ло кально определенного объекта исключения MyExcept ion. Рассмотрим еще раз конст рукцию catch из этого примера:
public void SomeFunction()
{
try
{
SomeOtherFunctionO ;
}
catch(MyException me)
{
}
}
406
Часть VII. Дополнительные главы
Главе
А если функция SomeOtherFunction () сгенерирует простое исключение Excep tion или исключение еще какого-то типа, отличного от MyException? Это будет на поминать ситуацию, когда футбольный вратарь ловит баскетбольный мяч — мяч, ловить который он не научен. К счастью, С# позволяет программе определить несколько блоков catch, каждый из которых предназначен для различного типа исключений.
Блоки catch должны в этом случае следовать один за одним, без разрывов, в порядке от наиболее специализированных классов ко все более общим. С# проверяет каждый catch-блок, последовательно сравнивая сгенерированный объект с аргументами catchолоков, как показано в следующем фрагменте исходного текста:
public void SomeFunction ()
(
t r y
SomeOtherFunctionO ;
catch(MyException me)
//
Наиболее специализированный тип
{
//
исключения
// Здесь перехватываются все объекты MyException
}// Между этими catch-блоками могут находиться блоки с
//другими типами исключений
catch(Exception е)
// Наиболее общий тип исключения
//Все остальные неперехваченные исключения
//перехватываются в этом блоке
Если функция SomeOtherFunction () сгенерирует объект Exception, он минует блок catch (MyException), поскольку Exception не является типом MyExcep tion. Он будет перехвачен в следующем блоке — catch (Exception).
Любой класс, наследующий MyException, ЯВЛЯЕТСЯ MyException: class MySpecialException : MyException
{
// ... что-то там ...
}
В этом случае блок для MyException перехватит и объект MySpecialException. (Наследование всех пользовательских исключений от одного базового пользовательского исключения — неплохая мысль. Само базовое исключение наследуйте от ApplicationException.)
Всегда располагайте catch-блоки от наиболее специализированного к наибо лее общему. Никогда не размещайте более общий блок первым, как это сделано в приведенном фрагменте исходного текста:
public void SomeFunction ()
(
t r y
{
SomeOtherFunction();
}
Глава 18. Эти исключительные исключения
407
catch(Exception me) // Самый общий блок - это неверно!
{
//
Все объекты
MyException
будут перехвачены здесь
}
catch(MyException
е)
{
//
Сюда не доберется ни один объект - все они будут
//
перехвачены
более общим
блоком
}
}
Более общий блок отнимает объекты исключений у более специализированного блощ К счастью, компилятор в состоянии обнаружить такую ошибку и предупредить о ее наличии
Как исключения протекают сквозь пальцы
Что, если С#, пройдя все catch-блоки, так и не найдет подходящего? Или в вы» вающей функции вообще нет catch-блока? Что будет тогда?
// ... а теперь наше исключение Console.WriteLine("\пГенерируем исключение " +
"MyException"); new ProgramO .fl(true) ;
// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +
"завершения программы...");
Console.Read();
Глава 18. Эти исключительные исключения
409
Функция Main () создает объект Program и тут же использует его для вызова мето да f 1 ( ) , который, в свою очередь, вызывает метод f 2 ( ) , который вызывает метод f 3 ( ) , вызывающий метод f 4 ( ) . Функция f 4 () выполняет сложную проверку ошибки которая выливается в генерацию либо исключения MyException, либо обобщенного исключения Exception, в зависимости от аргумента типа bool. Вначале сгенериро ванное исключение Exception передается в функцию f3 ( ) . Здесь С# не находит catch-блока, и управление передается вверх по цепочке в функцию f 2 ( ) , которая пе рехватывает исключения MyException и его наследников. Этот тип исключения не со ответствует обобщенному исключению Exception, и управление передается еще дальше вверх. Наконец, в функции f 1 () находится catch-блок, соответствующий сге нерированному исключению.
Второй вызов в функции Main () заставляет функцию f 4 () сгенерировать объект MyEx ception, который перехватывается в функции £2 ( ) . Это исключение не пересылается функцию f 1 ( ) , поскольку оно перехватывается и обрабатывается функцией f 2 ().
(Может ли функция Main () в действительности создать объект класса, содержащий объект класса, в котором содержится Main () — т.е. класса Program? Конечно, почему бы и нет? См. последний раздел главы 14, "Интерфейсы и структуры".)
Вывод программы выглядит следующим образом:
Сначала генерируем обобщенное исключение Перехват обобщенного исключения в fl() Обобщенное исключение в f4()
'Генерируем исключение MyException Перехват MyException в f2() Генерация MyException в f4()
Нажмите <Enter> для завершения программы...
Функция наподобие f3 ( ) , не содержащая ни одного catch-блока, вовсе не ред кость. Можно сказать даже больше — такие функции встречаются гораздо чаще, чем функции с catch-блоками. Функция не должна перехватывать исключения, если она не готова их обработать. Должна ли некоторая математическая функция ComputeX() в которой вызывается функция Factorial () как часть вычислений, перехватывать исключение, которое может быть сгенерировано функцией Factorial () ? Функция ComputeXO может совершенно не представлять, откуда взялись неверные входные данные для функции Factorial О и что теперь делать. В этом случае функция ComputeX ( ) , конечно же, не должна содержать catch-блока и перехватывать генери руемые исключения.
Функция наподобие f 2 () перехватывает только один тип исключений. Она ожида ет только один определенный тип ошибки, который умеет обрабатывать. Например MyException может быть исключением, определенным для выдающейся библиотеки классов гениального автора, написанной, понятное дело, мной, и так и называющей с я — BrilliantLibrary. Функции, составляющие BrilliantLibrary, генери руют и перехватывают только исключения MyException.
Однако функции BrilliantLibrary могут также вызывать функции обычной стандартной библиотеки System. Функции BrilliantLibrary могут не знать, как следует обрабатывать обобщенные исключения System, в особенности если они вызва ны некорректными входными данными.