Назад | Начало урока | Вперед
Содержание

Глава 20

Исключительные ситуации



В этой главе...

Одним из этапов создания программы является ее тестирование и отладка. Но даже
если вы устраните все ошибки и программа начнет работать, это еще не означает,
что проблемы не возникнут в будущем. Причина в том, что вы не можете заранее предвидеть всех действий пользователей, производимых в отношении вашей программы, и всех ситуаций, с которыми может столкнуться созданный вами программный продукт. Единственный способ проверить, как будет вести себя программа в различных экстремальных условиях, — создать эти условия и посмотреть, что произойдет. Кстати, небезызвестные законы Мэрфи во многом обязаны своим происхождением всем тем проблемам, которые возникают между
пользователями и программным обеспечением.

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

Как следствие, все перечисленные ситуации (и множество им подобных} могут привести к сбоям в работе программы. К счастью, существует такое средство, как обработка исключений, которое поможет вам с достоинством справиться со всеми непредвиденными проблемами.

Вверх

Как это было раньше

Прежде чем осваивать метод обработки исключений, вам полезно будет узнать, как подобные проблемы решались ранее.
Процесс устранения ошибок можно разбить на два этапа: определение факта наличия
ошибки, и ее нахождение и ликвидация. Первый этап — определение того, что ошибка действительно есть, — обычно не представляет никаких сложностей. Просто в программе в нужных местах необходимо добавить несложные коды, определяющие наличие ошибки и сообщающие что-то наподобие; "Ага, недостаточно данных. Есть ошибка!"
Когда установлено, что в программе присутствует ошибка, нужно найти источник ее возникновения и ликвидировать его. Локализация ошибки может оказаться более сложной задачей. Коды программы, обнаружившие существование ошибки, могли быть вызваны функцией, которая была вызвана другой функцией, которая, в свою очередь, была вызвана еще одной функцией, и т.д. Желательно, чтобы все эти функции могли сами определить наличие ошибки и решить, как поступать далее. Обычно к ним добавляются коды, проверяющие наличие ошибки и в случае ее отсутствия позволяющие функциям работать далее в обычном режиме,
а в случае обнаружения ошибки — досрочно прекращающие их работу.
Другими словами, набор кодов
//Вызов нескольких функций

ReadSongs();
ReadSizes();
ReadMyLips();

желательно заменить такими кодами;

//Вызов нескольких функций. Если возвращаемый функцией результат
//меньше нуля, это расценивается как ошибка
nTemp = ReadSongsO ;
if (nTemp < 0)
{

return nTemp;
}
nTemp = ReadSizes();
if (nTemp < 0)
{
return nTemp;
}
nTemp = ReadMyLips();
if (nTemp < 0)
{
return nTemp;
}

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

И наконец, после того как вы добавите все эти коды, нужно будет создать еще функцию, которая сможет обработать информацию об ошибке и сообщить что-то вроде: "Ага, я знаю как справиться с этой ошибкой". Такая функция должна уметь различать, ошибка какого типа произошла, и знать, как на это реагировать.
В общем, осуществить все это на практике — задача не из легких. На написание всех этих сложных кодов, обрабатывающих возможные ошибки, может уйти больше времени и усилий, чем на написание реально работающей части программы. К тому же эти дополнительные коды делают программу громоздкой и трудночитаемой.

Вверх

Новый усовершенствованный способ обработки ошибок

Возможность обработки исключений позволяет избавиться от необходимости добавления
проверяющих кодов после каждого вызова какой-либо функции.
Сам механизм обработки исключений состоит из двух частей: одна процедура определяет исключительную ситуацию (ее задача сказать: "О да, здесь есть ошибка!") и вторая процедура (которая, собственно, и обрабатывает найденное исключение) берет на себя управление, если какое-то исключение найдено.

Второе преимущество механизма обработки исключений состоит в том, что любые локальные объекты, т.е. те. которые создаются в процессе выполнения какой-то функции, автоматически уничтожаются. Продемонстрируем это на таком примере. Предположим, что функция А создает локальный объект ObjectA, затем вызывает функцию В, которая создает локальный объект OhjectB и вызывает функцию, которая находит ошибку. Объекты ObjectA и ObjeclB должны быть удалены. Таким образом, все используемые ими файлы, данные, фрагменты памяти и т.п. автоматически освобождаются.

Фрагмент кода, который может послужить причиной возникновения исключительной си-
туации, необходимо поместить внутри фигурных скобок, а перед скобками набрать ключевое слово try. Это будет командой компилятору: "Попробуй выполнить этот фрагмент кодов и посмотри, что будет". Далее нужно набрать ключевое слово catch и в фигурных скобках поместить коды, определяющие реакцию на возникновение ошибки. Компилятор это воспримет так: "Если возникнет какая-то проблема, действуй в соответствии с приведенными здесь инструкциями".

Вверх

Вот как это выглядит на практике

Лучше один раз увидеть, чем сто раз услышать. Поэтому перейдем к демонстрации сказанного на конкретном примере. Приведенная ниже функция предназначена для получения числа от пользователя и преобразования его к значению типа integer. (Функция взята из программы Factorial^-, вычисляющей факториал заданного числа.) Казалось бы, ничего сложного сдесь нет:

int GetNumberO
{

int nNumber;
Console::WriteLine(S"Укажите число");
nNumber = Int32 :: Parse(Console::ReadLine());
return nNumber;
}

Но что произойдет, если пользователь наберет что-то неверное или бессмысленое, например слово "печенька"? Поскольку это не может быть преобразовано к значению типа integer (это вообще не число), на экране появится сообщение об ошибке и работа программы будет моментально завершена.

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

int GetNumber()
{

int nNumber;
Console::WriteLine(S"Укажите число");
try
{
nNumoer = Int32::Parse (Console : :P.eadLine () ) ;
}
catch (FormatException *e)
{
Console::WriteLine(e->Message);
Console::WriteLine("В следующий раз наберите, пожалуйста,
число. Сейчас введенное вами значение будет
воспринято как число I");
nNumbeг = 1;
}
return nNumber ;
}

Если пользователь набирает какую-то абракадабру, это означает возникновение ошибки FermatExceptiion. Оператор catch идентифицирует эту ошибку, в результате чего на экране отображается дружественное сообщение и возвращается факториал числа, заданного по умолчанию (в данном случае это число 1).

Вверх

Вспомните о пользователях!

Сами по себе пользователи хороши тем, что они существуют. Именно благодаря тому, что они используют создаваемое вами программное обеспечение, вы стяжаете себе славу и известность или по крайней мере сохраняете за собой свое рабочее место. Самое плохое в пользователях то, что они зачастую делают с вашей программой такое, о чем вы и подумать не могли. И именно поэтому программу нужно снабжать кодами, управляющими исключительными ситуациями.

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

Вверх

Гибкость — основное свойство механизма обработки исключений

Механизм обработки исключений языка C++ предоставляет поистине неограниченную
свободу действий. Заключая отдельные фрагменты кодов в фигурные скобки оператора try.
вы можете сами определять, какие из них нужно проврять на наличие ошибок, а какие нет.
Для одного и того же фрагмента кодов можно оставить инструкции на случай возникновения
самых разных ошибок. И наконец, для одной и той же ошибки, возникающей в разных частях
программы, можно оставить различные инструкции, поместив их в фигурные скобки соответ-
ствующих операторов catch.

Предположим, например, что в разных частях программы вызывается функция
AllocateBuf. В одной части программы эта функция вызывается для того, чтобы иыдедичь
память для файла с данными, который должен быть сохранен, а в другой части — чтобы выделить память для фотоизображения.

Это может быть реализовано в таком виде:

//Попытка сохранения файла
try
{

AllocateBuf() ;
SaveFile() ;
}
catch (Errorl e)
{
Console::WriteLine(Б"Файл не может быть сохранен");
}
//Попытка выделить память для фотографии
try
{
AllocateBuff>;
Proce:>sPhoto ( ) ;
}
catch (Errorl e]
{
Console::WriteLine(S"Фотография не может Оыть обработана");
}

Каждый раз вызывается функция AllocateBuf, и каждый раз проверяется наличие
одной и той же ошибки: невозможность выделения памяти. Но в каждом случае про-
грамма реагирует по-разному, и на экране отображаются соответствующие случаю со-
общения.

Вверх

Определение собственных исключительных ситуаций

Предположим, что программа вызывает некую функцию foо и эта функция принимает
значения нескольких параметров. Допустим, что заранее известно: если значение, скажем,
первого параметра будет меньше (или больше) некоторого числа, то ситуация выйдет из под
контроля либо будет развиваться в совершенно неприемлемом направлении. Как в этом случае указать комилятору на наличие ошибки?

Чтобы самостоятельно идентифицировать ошибку, воспользуйтесь командой throw. Ее
выполнение будет означать сигнал о возникновении исключительной ситуации, и компилятор
перейдет к инструкциям catch, следующим за текущим блоком try.

Предположим, например, что вам нужно вычислить квадратный корень числа. Вы знаете,
что эта операция некорректна для отрицательных чисел, а потому можете набрать код, подобный следующему:

double SquareRoot(int nNumber)
{


if (nNumber < 0)
{
throw new SquareRootException(S"Число должно быть
положительным");
}
return Math::Sqrt(nNumber);
}

Эта функция проверяет, не является ли число отрицительным. Если да, фиксируется исключительная ситуация. Если нет, вычисляется квадратный корень этого числа.
Вы можете определить столько собственных исключительных ситуаций, сколько будет необходимо. Каждая команда throw передает отдельный тип данных. Можно создать любое количество типов данных, определить для них конкретные значения и использовать эту информацию как вспомогательную для управления процессом обработки исключения.

Для этого можно использовать даже классы. В таком случае вы можете создать процедуру, управляющую исключительной ситуацией, непосредственно как функцию-член этого
класса и передать объект данного класса кодам c a t c h с помощью команды throw. Например, можно создать класс MyError и при возникновении исключительной ситуации воспользоваться командой throw, которая создаст объект класса MyError и передаст его кодам catch.

Команды catch в качестве параметра принимают типы данных. Так, можно создать
класс ChoiceError, который будет идентифицироваться одной командой catch, и класс
MyError, на который будет реагировать другая команда catch.

При этом также можно использовать мехонизм наследования. Например, класс MyError
может быть производным от класса ChoiceError.

Вверх

Поговорим о синтаксисе

Вот как выглядит синтаксис кодов обработки исключений:
try
{

//Коды, при выполнении которых может возникнуть ошибка
инструкции;
}
catch (тип__ошибки_1)
{
инструкции на случай возникновения этой ошибки;
}
catch (тип_ошибки_2]
{
инструкции на случай возникновения зтсй ошибки;
}

Чтобы зафиксировать факт возникновения ошибки, наберите
throw тип_ошибки;

Обратите внимание, что на количество блоков catch ограничения нет. Когда фиксируется исключительная ситуация (ошибка), ищется тип ошибки, которому она соответствует, и
выполняются оставленные на этот случай инструкции.

Все исключения .NET являются классами, производными от класса Exception.
Объект класса Exception содержит свойство Message (Сообщение). Вы може-
те присвоить значение этому свойству (определить содержание сообщения) в момент создания объекта Exception.

Рассмотрим пример небольшой программы. В процессе ее выполнения пользователю
снова и снова предлагается ввести число, из каждого введенного числа извлекается квадрат-
ный корень, а полученное значение отображается на экране. Если пользователь вводит значе-
ние, которое не может быть преобразовано в число (например, он вводит слово), программа
реагирует на эту ошибку и вычисляет квадратный корень числа, заданного по умолчанию. Ес-
ли же пользователь вводит отрицательное число, программа также реагирует на эту ошибку,
что сопровождается отображением специального сообщения.

// SquareRoot
// Exception handling
#include "stdafx.h"
#using <mscorlib.dll>
using namespace System;

__gc class SquareRootException : public Exception
{

public:
SquareRootException(String *s) : Exception(s) {
};
};

//Returns the square root //Throws an exception if there is a range problem
double SquareRoot(int nNumber)
{

if (nNumber < 0)
{
throw new SquareRootException(S"Number must be positive");
}
return Math::Sqrt(nNumber);
}

//This routine prompts the user for a number.
//It returns the value of the number
int GetNumber()
{

int nNumber;

Console::WriteLine(S"What is the number?");
try
{

nNumber = Int32::Parse(Console::ReadLine());
}
catch (FormatException *e)
{
Console::WriteLine(e->Message);
Console::WriteLine("Next time, please enter an integer. I'm going to guess you meant 1.");
nNumber = 1;
}
return nNumber;
}

// This is the entry point for this application
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{

int nNumber;

//Get numbers from the user, until the user
//types 0
while (nNumber = GetNumber())
{

//Now we will output the result
try
{
Console::WriteLine(S"The square root of {
0
} is {
1
}", nNumber.ToString(), SquareRoot(nNumber).ToString());
} catch (Exception *e) {
Console::WriteLine(e->Message);
}
}

//Now we are finished Console::WriteLine(S"Bye"); //Hang out until the user is finished Console::WriteLine(S"Hit the enter key to stop the program");
Console::ReadLine();

return 0;

}

Начните чтение кодов этой программы с блока try, который предназначен для вычисления и отображения значения квадратного корня числа, введенного пользователем:

try
{

Console::WriteLine(S"Квадратный корень числа {0} равен числу {1}", nNumber.ToString(),
SquareRoot(nNumber).ToString());
}

Если пользователь введет отрицательное число, оно будет передано функции
SquareRoot, которая отреагирует на это фиксированием исключения с помощью команды
throw и передачей сообщения об ошибке конструктору SauareRootException:

if (nNumber < 0 )
{

throw new SquareRootException (S"Число должно быть положительным");
}

Коды catch набраны сразу же после блока try. В данном случае здесь присутствует
только одна команда catch, которая реагирует на все исключения, фиксируемые внутри
блока try. Информация об исключениях передастся в виде объекта класса Exception, который имеет собственное имя. Процедура eaten отображает на экране значения свойства Message этого объекта:

catch (Exception *o)
{

Console::WriteLine (e->Message);
}

Итак, посмотрим, как работает эта программа. Все начинается с запуска цикла. Перед
выполнением каждой итерации пользователю предлагается ввести какое-то число. Если
пользователь вводит то. что не может быть преобразовано к значению типа integer, функ-
ция GetNunber фиксирует ошибку и выдает предупреждающее сообщение. При этом вы-
числяется квадратный корень значения, заданного но умолчанию (в данном случае это зна-
чение равно числу 1). Внутри цикла предпринимается попытка выполнить коды блока try,
цель которых вычислить квадратный корень введенного числа и отобразить полученное
значение нa экране.

Если пользователь ввел положительное число, вызываемая функция SquareRoct не за-
фиксирует ошибку, в резельтате чего вес коды блока t r y будут благополучно выполнены и
пользователь увидит на экране вычисленный результат. Затем программа переходит к сле-
дующей итерации, и, если пользователь не вводит нулевое значение, снова происходит по-
пытка выполнить коды блока try.

Если пользователь вводит отрицательное значение, функция SquarcRoot фиксирует
возникновение исключительной ситуации и передаст управление колам блока catch. В дан-
ном случае их выполнение приводит к отображению на экране специального предупреждаю-
щего сообщения. Затем программа переходит к следующей итерации. Как только пользова-
тель вводит число 0 (нуль), программа завершает работу.

Вверх

Все это хорошо, но несколько запутанно

Благодаря возможности передавать объекты классов при возникновении исключи-
тельных ситуаций решаются две проблемы. Во-первых, можно определить столько ис-
ключений, сколько будет необходимо, а во-вторых, с объектами можно передавать всю
необходимую информацию об ошибке и мерах, которые следует предпринять в связи с
ее возникновением.

Например, если ошибка вызвана отсутствием свободного места на диске, желательно
было бы указать название этого диска. Если ошибка связана с тем, что поврежден какой-то
файл, неплохо было бы указать его имя и адрес последнего удачно считанного фрагмента.
Вы могли бы даже определить процедуру, которая попытается восстановить этот файл.

Но когда вы передаете объект класса с помощью команды throw, как компилятор
определит, какой из блоков catch должен быть использован? Это важный момент.
Компилятор проверяет тип данных, которые передаются командой throw, и ищет ту
команду catch, которая знает, как данные такого типа должны быть обработаны. Так,
вы можете создать классы FileCorrupt (повреждение файла), DiskError (ошибка
диска), MemoryError (ошибка памяти), каждый из которых будет содержать в себе ин-
формацию, описывающую данную проблему. Сами классы могут кардинально отличать-
ся друг от друга.

Затем, на случай обнаружения ошибки диска, наберите следующее:

DiskError foo;
//Здесь наберите коды, наполняющие содержанием объект foo
throw foo;

На случай, если будет поврежден файл, наберите такой код:

CorruptFile bar;
//Здесь наберите коды, наполняющие содержанием объект bar
throw bar;

Затем, благодаря тому что команды catch определяют совпадение типов
данных или классов, управление передается соответствующему блоку

catch:
//Инструкции на случай возникновения ошибки диска
catch (DiskError MyError) { }
//Инструкции на случай повреждения файла
catch (CorruptFile MyError) {}

Использование классов позволяет весьма элегантно достичь впечатляющей гибкости
в управлении исключительными ситуациями, поскольку классов, описывающих ошибки,
можно создать любое количество, а в самих классах можно обозначить всю необходимую информацию.

При желании для определения типа ошибки можно использовать ппростой тип данных,
например char *. Но использовать классы все же гораздо предпочтительнее, поскольку с
ними вы можете передать полную и четкую информацию о том, в чем именно заключается
проблема. Процедура, в которой ошибка была обнаружена, располагает всеми необходимыми
данными. Процедура, которая будет обрабатывать эту ошибку, должна получить эти данные,
чтобы помочь пользователю справиться с возникшей проблемой.
Еще одно преимущество использования классов состоит в том, что вы можете снабдить их функциями-членами. Например, можно создать внутри класса набор процедур,
отображающих сообщения на экране и помогающих корректировать ситуацию или упралять ею.

Вверх

Наследование классов, описывающих исключения

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

При написании неуправляемых кодов вы не сможете наследовать класс
Exception. Вам в любом случае придется самостоятельно создавать собственный класс.

Преобразование типов и работа оператора catch

Если тип ошибки может быть легко преобразован к типу, на который реагирует команда catch, фиксируется соответствие.

Например, тип short можно легко преобразовать к типу int . Поэтому такая команда
catch реагирует на исключение, генерируемое командой throw:

catch (int k) {}
...
throw (short i = 6);

Точно так же, если команда throw возвращает класс Derived, который является производным класса Base, указатель на производный класс может быть воспринят как указатель на базовый класс:

catch (Base foo) {}
...
Derived foo;
throw foo;

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

Компилятор выполняет блок кодов первого оператора catch, который фиксирует соответствие с типом возникшей ошибки. Поэтому, если вы используете сразу несколько производных классов, перечислите вначале соответствующие им блоки catch и только затем наберите блок catch, соответствующий базовому классу. В таком случае программа будет работать правильно, и специфические исключения (представляемые производными классами) будут фиксироваться именно соответствующими им командами catch. В противном случае, если первым будет набран блок catch базового класса, он будет реагировать на все подряд исключительные ситуации, представляемые производными классами.

Вверх

Пять правил исключительного благополучия

Вот пять простых правил, которых следует придерживаться при написании кодов обработки исключений.


Назад | Начало урока | Вверх | Вперед
Содержание