Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C# для чайников

.pdf
Скачиваний:
198
Добавлен:
27.03.2015
Размер:
15.52 Mб
Скачать

Часть VII

Дополнительные главы

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

Глава 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 ( ) , существенно более удобочитаемой.

Обычно по соглашению для имен констант используются строчные буквы, а слова в имени разделены символами подчеркивания.

Обращение к этим константам выполняется посредством имени класса, как

MyMathClass. NEGATIVE_NUMBER. Константные переменные автоматиче­

ски являются статическими, что делает их свойствами класса, разделяемыми всеми объектами. Другие варианты работы с константами описаны во врезке "Немного о константах".

7 Этот "разумный" результат некорректен, так как в математике принято, что факториал 0 ра­

вен 1. — Примеч. ред.

Глава

18.

Эти исключительные исключения

397

Немного о константах

Предпочтительный способ записи констант почти всегда использует следую» щий, более гибкий по сравнению с применением const, подход.

public static readonly int NEGATIVE_NUMBER = -1;

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

Еще один способ определения констант — в данном случае группы связанных кон- стант—посредством ключевого слова enum, как описано в главе 15, "Обобщенное программирование". Типы ошибок для MyMathClass могут быть определены еледующим образом:

enum MathErrors

NegativeNumber,

NonlntegerValue

Функция Factorial () может возвращать значение MathErrors, и вы можете проверить его в своей программе следующим образом (как можно часто увидеть в классах

.NET Framework):

MathErrors meResult = MyMathFunctions.Factorial(6); if(meResult == MathErrors.NegativeNumber) ...

Теперь функция 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

//описывает ошибку

'Далее будет использоваться выражение "генерирует исключение". — Примеч. ред.

400

Часть VII. Дополнительные главы

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]