В оригинальном издании книги эти главы были помещены на прила гаемый к книге компакт-диск. В русском издании книги их решено опубликовать в виде отдельной, дополнительной части.
Глава 18
Эти исключительные исключения
> Обработка ошибок с помощью кодов ошибки
УИспользование механизма исключений вместо кодов ошибки
}Создание собственного класса исключения
}Перекрытие ключевых методов в классе исключения
не сомнения, трудно смириться с тем, что иногда метод (или функция) не делает то, для чего он предназначался. Это раздражает программистов ничуть не меньше, чем пользователей их программ, тоже часто являющихся источником недоразу мений. В книге встречалась программа, в которой пользователь должен был вводить це лое число как строку. Такой метод можно написать так, что он будет просто игнориро вать введенный пользователем мусор вместо реального числа, но хороший программист напишет функцию таким образом, чтобы она распознавала неверный ввод пользователя
и докладывала об ошибке.
Здесь говорится об ошибках времени выполнения, а не времени компиляции, с кото рыми С# разберется сам при сборке вашей программы.
Механизм исключений представляет собой средство для сообщения о таких ошибках способом, который вызывающая функция может лучше понять и использовать для реше ния возникшей проблемы.
Промолчать о том, что произошла ошибка времени выполнения — это всегда наи худшее решение, применимое только в том случае, если вы не намерены отлаживать программу и вас не интересует результат ее работы...
В приведенной далее демонстрационной программе FactorialWithEr- r o r показано, что может произойти, если не выявить ошибку. Эта програм ма вычисляет и выводит значение факториала для ряда значений.
Факториал числа N равен N*(N-l)*(N-2)*... *1. Например, факториал 4 равен 4*3*2*1 = 24. Функция вычисления факториала работает только для поло жительных целых чисел. Это банальный программистский пример для ил люстрации ситуации, когда требуется обработка ошибок.
//FactorialWithError - пример функции вычисления
//факториала, в которой отсутствует проверка ошибок
using System;
namespace FactorialWithError
{
//MyMathFunctions - набор созданных мною математических
//функций
public class MyMathFunctions
{
//Factorial - возвращает факториал переданного
//аргумента
public static double Factorial(double dValue)
{
//Начинаем со значения аккумулятора, равного 1 double dFactorial = 1.0;
//Цикл со счетчиком nValue, уменьшающимся до 1, с
//умножением на каждой итерации значения аккумулятора
//на величину счетчика
do
{
dFactorial *= dValue; dValue -= 1.0;
} while(dValue > 1);
// Возвращаем вычисленное значение return dFactorial;
}
}
public class Program
{
public static void Main(string[] args)
{
//Вызов функции вычисления факториала в
//цикле от 6 до -6
for (int i = 6; i > -6; I--)
{
// Вывод результата на каждой итерации Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));
}
// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +
"завершения программы.. . ") ;
Console.Read();
}
}
}
Функция Factorial () начинается с инициализации переменной-аккумулятора значе нием 1. Затем функция входит в цикл, в котором на каждой итерации выполняется умноже ние на последовательно уменьшающееся значение счетчика nValue, пока nValue не дос тигнет 1. Накопленное в аккумуляторе значение возвращается вызывающей функции.
Алгоритм Factorial () выглядит корректно— пока вы не начнете вызывать эту функцию. Функция Main () также содержит цикл, в котором вычисляются значения фак ториала для ряда убывающих значений. Однако вместо того чтобы остановиться на значе нии394 1, функция Main () продолжает вычисления для отрицательныхЧасть VII. Дополнительныезначе й — д -6.главы
Врезультате на экране получается следующее:
i= 6, факториал = 72 0
i = 5, факториал = 12 0
i = 4,
факториал = 24
i=3,
факториал =
6
i = 2,
факториал = 2
1=1,
факториал =
1
| = 0 ,
факториал =
0
i = -1,
факториал = -1
i = -2,
факториал = -2
i = -3,
факториал = -3
i = -4,
факториал = -4
i - -5, факториал = -5
Нажмите <Enter> для завершения программы...
Как видите, часть результатов не имеет смысла. Во-первых, значение факториала не может быть отрицательным. Во-вторых, обратите внимание, что отрицательные значения растут совсем не так, как положительные. Понятно, что здесь присутствует ошибка.
Если попытаться изменить цикл внутри Factorial () и записать его как do{ . . . }while (dValue ! =0), то программу при передаче отрицательного
значения просто ждет крах. Поэтому никогда не пишите такой оператор срав нений — while (dValue ! =0), поскольку ошибка приближения может в лю бом случае привести к неверному результату проверки на равенство 0.
В особенности при работе с числами с плавающей точкой избегайте условий наподобие dValue ! =0, в которых требуется точное сравнение для выхода из цикла. Используйте менее строгое условие, как, например, dValue>l. Не большая ошибка приближения— такая как dValue = 0.00001— может привести к бесконечному циклу. Об ошибках приближения рассказывается в главе 3, "Объявление переменных-значений".
Возврат индикатора ошибки
Несмотря на свою простоту, функция Factorial О требует проверки ошибочной ситуации: факториал отрицательного числа не определен. Функция Factorial () должна включать проверку этого условия.
Но что должна делать функция Factorial ( ) , столкнувшись с ошибкой? Лучшее, что она может сделать в такой ситуации — это сообщить об ошибке вызывающей функции в надежде на то, что источник ошибки знает, почему она произошла и как с ней справиться.
Классический способ указать на происшедшую ошибку в функции — это возвратить значение, которое функция не в состоянии вернуть при безошибочной работе. Например, значение факториала не может быть отрицательным. Таким образом, факториал может возвращать значение -1, если ему передается отрицательный аргумент, -2для нецелого аргумента и так далее — для каждой ошибки некоторое соответствующее ей число. Та кие числа называются кодами ошибки. Вызывающая функция может проверить, не вер нула ли вызываемая функция отрицательное значение, и если д а — то вызывающая функция будет знать о том, что произошла ошибка. Значение возвращаемого кода ошиб ки позволяет определить ее природу.
Глава
18.
Эти исключительные исключения
395
jjjdWfeK Указанные изменения внесены в код демонстрационной программы Facto
//FactorialErrorReturn - создание функции вычисления
//факториала, которая возвращает код ошибки, если что-то
//идет не так
using System;
namespace FactorialErrorReturn
{
//MyMathFunctions - набор созданных мною математических
//функций
public class MyMathFunctions
{
//Следующие коды ошибок представляют некорректные
//значения
public
const
int NEGATIVE_NUMBER =
-1;
public
const
int NON_INTEGER_VALUE =
-2;
//Factorial - возвращает факториал переданного
//аргумента
public static double Factorial(double dValue)
{
// Проверка: отрицательные значения запрещены if (dValue < 0)
{
return NEGATIVE NUMBER;
}
// Проверка: передано ли целое значение аргумента int nValue = (int)dValue;
if (nValue != dValue)
{
return NON INTEGER VALUE;
}
//Тесты пройдены, начинаем со значения аккумулятора,
//равного 1
double dFactorial = 1.0;
//Цикл со счетчиком nValue, уменьшающимся до 1, с
//умножением на каждой итерации значения аккумулятора
//на величину счетчика
do
{
dFactorial *= dValue; dValue -= 1.0;
} while(dValue > 1);
// Возвращаем вычисленное значение return dFactorial;
}
}
public class Program
{
public static void Main(stririg[] args)
396 {
Часть VII. Дополнительные главы
//Вызов функции вычисления факториала в
//цикле от 6 до -6
for (int i = 6; i > -6; i--)
{
double dFactorial = MyMathFunctions.Factorial(i); if (dFactorial == MyMathFunctions.NEGATIVE NUMBER)
{
Console.WriteLine
("Factorial() получила отрицательный параметр"); break;
}
if (dFactorial == MyMathFunctions.NON INTEGER VALUE)
{' "
Console.WriteLine
("Factorial() получила нецелый параметр"); break;
}
// Вывод результата на каждой итерации Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));
}
// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +
"завершения программы...");
Console.Read ();
Теперь перед началом вычислений функция Factorial () выполняет ряд проверок. Первая проверка — не отрицателен ли переданный функции аргумент. Обратите внима ние, что значение 0 разрешено, поскольку приводит к разумному результату7. Если про верка не пройдена, функция тут же возвращает код ошибки. Затем выполняется второй тест, проверяющий, равен ли переданный аргумент своей целочисленной версии. Если да — дробная часть аргумента равна 0.
Функция Main () проверяет результат, возвращаемый функцией Factorial ( ) , на предмет обнаружения ошибок. Однако значения наподобие -1 и -2 мало информативны для программиста, так что класс MyMathFunctions определяет пару целочисленных
констант. Константа NEGATIVE_NUMBER равна -1, a NON_INTEGER_VALUE 2. Это
ничего не меняет, но делает программу, в особенности функцию Main ( ) , существенно более удобочитаемой.
Обычно по соглашению для имен констант используются строчные буквы, а слова в имени разделены символами подчеркивания.
Обращение к этим константам выполняется посредством имени класса, как
ски являются статическими, что делает их свойствами класса, разделяемыми всеми объектами. Другие варианты работы с константами описаны во врезке "Немного о константах".
7 Этот "разумный" результат некорректен, так как в математике принято, что факториал 0 ра
вен 1. — Примеч. ред.
Глава
18.
Эти исключительные исключения
397
Немного о константах
Предпочтительный способ записи констант почти всегда использует следую» щий, более гибкий по сравнению с применением const, подход.
public static readonly int NEGATIVE_NUMBER = -1;
Значение const вычисляется во время компиляции и может быть инициализировано только числом или строкой. Статическая переменная только для чтения вычисляется во время выполнения программы и может быть инициализирована объектом любого вида, Используйте const только там, где производительность программы сверхкритична.
Еще один способ определения констант — в данном случае группы связанных кон- стант—посредством ключевого слова enum, как описано в главе 15, "Обобщенное программирование". Типы ошибок для MyMathClass могут быть определены еледующим образом:
enum MathErrors
NegativeNumber,
NonlntegerValue
Функция Factorial () может возвращать значение MathErrors, и вы можете проверить его в своей программе следующим образом (как можно часто увидеть в классах
Теперь функция Factorial () сообщает об ошибках функции Main ( ) , которая вы- водит соответствующее сообщение на экран и завершает на этом свою работу:
i
= 6,
факториал =
720
i
= 5,
факториал =
12 0
i = 4, факториал = 24
i = 3, факториал = 6
i = 2, факториал = 2
1 = 1 ,
факториал =
1
i = 0, факториал = 0
Factorial() получила
отрицательный параметр
Нажмите
<Enter> для
завершения программы...
(Здесь я предпочел прекращать работу при обнаружении ошибки.) Указание о про исшедшей ошибке посредством возвращаемого функцией значения повсеместно исполь зуется еще со времен FORTRAN. Зачем же менять этот механизм?
Чем плохи коды ошибок
Что же не так с кодами ошибок? Они были достаточно хороши даже для FORTRAN! Да, но в те времена компьютеры были ламповыми. Увы, но коды ошибок приводят к ря ду проблем.
Этот метод основан на том факте, что у функции имеются значения, которые она не может вернуть при корректной работе. Однако существуют функции, у которых
398
Часть
VII. Дополнительные главы
любые возвращаемые значения корректны. Не всегда везет поработать с функцией, которая должна возвращать только положительные значения. Например, вы не можете получить логарифм отрицательного числа, но само значение логарифма может быть как положительным, так и отрицательным.
Вы можете предложить справиться с этой проблемой, возвращая код ошибки как значение функции, а необходимые данные — посредством ар гумента, описанного как out, но такое решение менее интуитивно и теря ет выразительную силу функции. Сперва познакомьтесь с исключениями, и вы убедитесь, что они предоставляют гораздо более красивый путь ре шения проблемы.
В целом числе не удается разместить большое количество информации. Так, рассматриваемая функция Factorial О возвращает -1,если ее аргумент от рицателен. Локализовать ошибку было бы проще, если бы был известен сам аргумент, но в возвращаемом функцией типе для него просто нет места.
Обработка ошибок является необязательной. Вы не получите никаких пре имуществ от проверок в функции Factorial ( ) , если вызывающая функ ция не будет в свою очередь проверять возвращаемое значение. Конечно, руководитель группы может просто сказать: "Парни, или вы проверяете ко ды ошибок, или занимаете очередь на бирже труда", но совсем иное дело, когда проверку заставляет выполнить сам язык программирования.
Зачастую при проверке кода ошибки, возвращаемого функцией Factorial () или лю бой другой функцией, практически вся вызывающая функция оказывается заполненной проверками всех возможных кодов ошибок от всех вызванных функций, при этом просто не остается ни сил, ни места сделать в функции хоть что-то полезное. Судите сами:
//Вызов SomeFuncO, проверка кода ошибки, его обработка и
//возврат из функции
errRtn = SomeFunc () ;
if (errRtn == SF_ERRORl)
(
Console .WriteLine ("Ошибка типа 1 при вызове SomeFuncO"); return MY ERROR 1;
if (errRtn == SF ERROR2)
Console .WriteLine ("Ошибка типа 2 при вызове SomeFuncO"); return My_ERROR 2;
}
// Вызов другой функции, проверка кода ошибки и так далее..
.errRtn = SomeOtherFuncО; if (errRtn == SOF_ERRORl)
{
Console.WriteLine("Ошибка типа 1 при вызове " + "SomeOtherFunc () ") ;
return MY ERROR 3;
)
if (errRtn == SOF ERROR2)
{
Console.WriteLine("Ошибка типа 2 при вызове " +
Глава 18. Эти исключительные исключения
399
"SomeOtherFunc()");
return MY_ERR0R_4;
}
Такой механизм имеет ряд проблем.
В нем очень много повторов. Дублирование кода обычно очень неприятно по-| пахивает...
Он заставляет пользователя функции поддерживать проверку массы кодов ошибок Код обработки ошибок оказывается перемешан с обычным кодом, что затеняет основную работу программы и делает исходный текст неудобочитаемым.
Все эти проблемы кажутся мелкими в простых примерах, но все становится гораздо хуже с ростом сложности вызываемых функций. В конечном итоге код обработки ошибок не перехватывает все ошибки, которые могут возникнуть.
К счастью, описанный далее механизм исключений решает указанные проблемы.
В С# для перехвата и обработки ошибок используется совершенно иной механизм, называемый исключениями. Он основан на ключевых словах try, catch, throw и fi nally. Набросать схему его работы можно следующим образом. Функция пытается (try) пробраться через кусок кода. Если в нем обнаружена проблема, она бросает (throw) индикатор ошибки, который функции могут поймать (catch), и независимо от того, что именно произошло, в конце (finally) выполнить специальный блок кода, как показано в следующем наброске исходного текста:
public class MyClass
{
public void SomeFunction()
{
// Настройка для перехвата ошибки try
{
//Вызов функции или выполнение каких-то иных
//действий, которые могут генерировать исключение SomeOtherFunction();
//. . . Какие-то иные действия . . .
}
catch(Exception е)
{
//Сюда управление передается в случае, когда в блоке
//try сгенерировано исключение — в самом ли блоке, в
//функции, которая в нем вызывается, в функции,
//которая вызывается функцией, вызванной в try-блоке
//и так далее — словом, где угодно. Объект Exception
//описывает ошибку
'Далее будет использоваться выражение "генерирует исключение". — Примеч. ред.