Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 5. Использование инструкций составного присваивания и итераций
Дальше: Часть II. Основные сведения об объектной модели C#

6. Обработка ошибок и исключений

Прочитав эту главу, вы научитесь:

обрабатывать исключения с использованием инструкций try, catch и finally;

управлять целочисленным переполнением с использованием ключевых слов checked и unchecked;

выдавать исключения из своих собственных методов с использованием ключевого слова throw;

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

Основные инструкции C#, знание которых позволит выполнить наиболее распространенные задачи, в том числе написание методов, объявление переменных, использование операторов для создания значений, написание инструкций if и switch для выборочного выполнения кода и написание инструкций while, for и do для многократного выполнения кода, вы уже видели. Но в предыдущих главах не рассматривалась возможность (или вероятность) того, что что-то может пойти не по сценарию.

Гарантировать, что какая-либо часть кода всегда будет работать абсолютно ожидаемым образом, очень трудно. Сбои может вызвать широкий спектр причин, со многими из которых вы, как программист, в состоянии справиться. Любые создаваемые вами приложения должны быть в состоянии обнаруживать сбои и обрабатывать их подобающим образом: либо выполняя корректирующие действия, либо, если исправить ситуацию невозможно, доводя до пользователя в максимально понятном виде причины сбоя. В заключительной главе первой части книги вы узнаете, как в C# в целях оповещения о возникновении ошибки используются исключения и как для обработки ошибок, представленных этими исключениями, применяются инструкции try, catch и finally.

К концу этой главы у вас будет сформирован прочный фундамент из знаний обо всех основных элементах C#, на котором вы сможете построить все, что будет изложено во второй части книги.

Борьба с ошибками

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

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

Попытка выполнения кода и перехват исключений

Ошибки могут случаться в любое время, и использование традиционных технологий, предусматривающих добавление вручную кода обнаружения ошибок в отношении каждой инструкции, — занятие слишком обременительное, затратное по времени и не гарантирующее отсутствия собственных ошибок в этом коде. К счастью, путем использования исключений и их обработчиков C# облегчает отделение кода обработки ошибок от кода, реализующего основную логику программы. Чтобы создавать программы, поддерживающие исключения, нужно сделать две вещи:

• поместить код внутри блока try (это слово в C# является ключевым). Когда код запускается, предпринимается попытка выполнения всех инструкций, имеющихся в блоке try, и если ни одна из инструкций не выдает исключения, они выполняются одна за другой до завершения блока. Но если создаются условия для возникновения ошибки, выдается исключение, которое приводит к передаче управления от блока try другому фрагменту кода, разработанному для перехвата и обработки исключений, — обработчику исключения;

• создать один или несколько обработчиков исключений с применением еще одного ключевого слова C#, catche, помещаемого сразу же после блока try, для обработки любых возможных условий возникновения ошибки. Обработчик исключений предназначен для перехвата и обработки исключения указанного типа, и после блока try у вас может быть указано сразу несколько обработчиков исключений, каждый из которых разработан для перехвата и обработки конкретного исключения. Это позволяет вам предоставлять различные обработчики для различных ошибок, которые могут возникать в блоке try. Если любая из инструкций внутри блока try вызывает ошибку, среда выполнения выдает исключение. Затем эта же среда исследует обработчики исключений, которые находятся после блока try, и передает управление непосредственно первому подходящему обработчику.

Рассмотрим пример блока try, содержащего код, предпринимающий попытку преобразования строк, введенных пользователем в текстовые поля формы, в целочисленные значения. Затем код вызывает метод для вычисления значения и записи результата в другое текстовое поле. Преобразование строки в целое число требует, чтобы в строке содержался подходящий набор цифр, а не какая-нибудь произвольная последовательность символов. Если в строке содержатся неподходящие символы, метод int.Parse выдает исключение FormatException и управление передается соответствующему обработчику исключения. Когда обработчик исключения завершит свою работу, выполнение программы продолжится передачей управления первой же следующей за обработчиком инструкции. Учтите, что в отсутствие обработчика, соответствующего исключению, это исключение считается необработанным (данная ситуация вскоре будет рассмотрена):

try

{

    int leftHandSide = int.Parse(lhsOperand.Text);

    int rightHandSide = int.Parse(rhsOperand.Text);

        int answer = doCalculation(leftHandSide, rightHandSide);

        result.Text = answer.ToString();

    }

    catch (FormatException fEx)

    {

        // Обработка исключения

        ...

    }

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

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

Необработанные исключения

А что произойдет, если блок try выдаст исключение, но соответствующего обработчика не окажется? В предыдущем примере существует возможность того, что в текстовом поле lhsOperand будет содержаться строковое представление допустимого целого числа, но такого, которое выходит за пределы диапазона поддерживаемых в C# целых чисел (например, 2 147 483 648). В таком случае инструкция int.Parse выдаст исключение переполнения OverflowException, которое не будет перехвачено обработчиком FormatException. Если такое случится и блок try окажется частью метода, тут же произойдет выход из метода и управление будет возвращено тому методу, который его вызывал. Если вызывавший метод использует блок try, среда выполнения предпримет попытку найти соответствующий обработчик исключения для этого блока try и выполнит код обработчика. Если вызывавший метод не использует блок try или в нем не имеется соответствующего обработчика исключения, тут же произойдет выход из метода и управление будет возвращено тому методу, который его вызывал, где процесс повторится. Если соответствующий обработчик исключения в конце концов найдется, он отработает и выполнение продолжится с первой инструкции, следующей за обработчиком исключения того метода, в котором оно было перехвачено.

174844.png

ВНИМАНИЕ Учтите, что после перехвата исключения выполнение продолжается в том методе, который содержит блок catch, перехвативший исключение. Если исключение произошло в методе, отличном от того, который содержит обработчик исключения, управление не будет возвращено тому методу, выполнение которого стало причиной выдачи исключения.

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

Изучить исключения, выданные вашим приложением, нетрудно. Если при запуске приложения в Microsoft Visual Studio 2015 в режиме отладки (для чего нужно в меню Отладка выбрать пункт Начать отладку) будет выдано исключение, появится диалоговое окно, похожее на показанное на рис. 6.1, и приложение встанет на паузу, помогая вам определить причину выдачи исключения.

06_01.tif 

Рис. 6.1

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

Использование нескольких обработчиков исключений

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

try

{

    int leftHandSide = int.Parse(lhsOperand.Text);

    int rightHandSide = int.Parse(rhsOperand.Text);

    int answer = doCalculation(leftHandSide, rightHandSide);

    result.Text = answer.ToString();

}

catch (FormatException fEx)

{

    //...

}

catch (OverflowException oEx)

{

    //...

}

Если код в блоке try выдает исключение FormatException, будут запущены инструкции в блоке catch, предназначенном для исключения FormatException. Если код выдаст исключение OverflowException, будет запущен блок catch, предназначенный для исключения OverflowException.

173390.png

ПРИМЕЧАНИЕ Если код в блоке catch FormatException выдает исключение OverflowException, это не становится причиной запуска примыкающего блока catch для OverflowException. Вместо этого, как было рассмотрено ранее в данном разделе, исключение распространяется на метод, вызвавший этот код.

Обработка нескольких исключений

Механизм перехвата исключений, предоставляемый C# и средой Microsoft .NET Framework, весьма совершенен. Среда .NET Framework определяет многие типы исключений, и большинство из них может быть выдано любой написанной вами программой. Вряд ли вам захочется создавать обработчики исключений для каждого возможного исключения, которое в состоянии выдать ваш код, — следует помнить, что приложение должно быть способно обрабатывать исключения, о которых вы при написании кода можете даже не подозревать! Так как же гарантировать перехват и обработку вашей программой всех возможных исключений?

Ответ на этот вопрос кроется в способах взаимосвязанности различных исключений. Все исключения сводятся в семейства, называемые иерархиями наследования. (О наследовании вы узнаете в главе 12 «Работа с наследованием».) Исключения FormatException и OverflowException, как и ряд других, принадлежат семейству по имени SystemException. А само семейство SystemException входит в более обширное семейство, которое называется просто Exception и является прапрадедушкой всех исключений. Если перехватывается Exception, то в обработчик попадают все выдаваемые исключения.

173396.png

ПРИМЕЧАНИЕ Семейство Exception включает широкое разнообразие исключений, многие из которых предназначены для использования различными частями среды .NET Framework. Некоторые из этих исключений случаются крайне редко, но разобраться в том, как они перехватываются, все равно не помешает.

Следующий пример показывает, как можно перехватить все возможные исключения:

try

{

    int leftHandSide = int.Parse(lhsOperand.Text);

    int rightHandSide = int.Parse(rhsOperand.Text);

    int answer = doCalculation(leftHandSide, rightHandSide);

    result.Text = answer.ToString();

}

catch (Exception ex) // это общий обработчик исключений

{

    //...

}

173404.png

ПРИМЕЧАНИЕ Если требуется перехватить Exception, его имя в перехватчике исключения можно не указывать, поскольку этот тип исключения используется по умолчанию:

    catch

    {

        // ...

    }

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

Остается еще один, последний вопрос, который должен быть задан к этому моменту: что произойдет, если одно и то же исключение соответствует нескольким обработчикам исключений, расположенным за блоком try? Если FormatException и Exception перехватываются в двух различных обработчиках, то какой из них будет запущен (или же будут выполнены оба обработчика)?

Когда выдается исключение, среда выполнения использует первый же найденный ею обработчик, соответствующий исключению, игнорируя при этом все остальные. Это означает, что при помещении обработчика для Exception перед обработчиком для FormatException обработчик FormatException не будет запущен никогда. Поэтому после блока try более конкретизированные обработчики следует помещать выше обработчиков более общего плана. Если ни один из конкретных обработчиков не будет соответствовать исключению, ему будет соответствовать общий обработчик исключений.

Фильтры исключений

Фильтры исключений являются новым свойством C#, оказывающим влияние на способ соответствия исключений их обработчикам. Фильтр исключения позволяет указать дополнительные условия, при которых используется обработчик исключения. Эти условия принимают форму булева выражения, перед которым ставится ключевое слово when. Синтаксис показан в следующем примере:

catch (Exception ex) when (ex.GetType() != typeof(System.OutOfMemoryException))

{

   // Обработка всех ранее не перехваченных исключений, кроме того,

   // которое называется OutOfMemoryException

}

В этом примере перехватываются все исключения (типа Exception), но фильтр указывает, что если по типу исключение будет относиться к нехватке памяти (out-of-memory), этот обработчик будет проигнорирован. (Метод GetType возвращает тип переменной, указанной в качестве аргумента.) Тем самым предоставляется конкретный способ обработки всех исключений, кроме исключения, выдаваемого при нехватке памяти. Если возникают условия для выдачи этого исключения, среда авполнения продолжит поиск, чтобы использовать другой обработчик исключения, и если не сможет его найти, исключение будет считаться необработанным.

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

Наблюдение за тем, как приложение оповещает о необработанных исключениях

Откройте в среде Visual Studio 2015 решение MathsOperators, которое находится в папке \Microsoft Press\VCSBS\Chapter 6\MathsOperators вашей папки документов.

Это версия программы из главы 2 «Работа с переменными, операторами и выражениями», демонстрирующей работу различных арифметических операторов.

Щелкните в меню Отладка на пункте Запуск без отладки.

173410.png

ПРИМЕЧАНИЕ Для этого упражнения очень важно запустить приложение без отладки.

Появится форма. Теперь вы намереваетесь ввести в поле Left Operand текст, который вызовет выдачу исключения. Эта операция продемонстрирует ненадежность текущей версии программы.

Наберите в поле Left Operand строку John, а в поле Right Operand — цифру 2. Щелк­ните на пункте + Addition, а затем на кнопке Calculate.

Этот ввод запустит в Windows обработку исключения по умолчанию: приложение просто прекратит работу, а вы вернетесь на Рабочий стол!

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

Создание блоков инструкций try и catch

Вернитесь в Visual Studio 2015. Щелкните в меню Отладка на пункте Начать отладку. Когда появится форма, наберите в поле Left Operand строку John, а в поле Right Operand — цифру 2. Щелкните на пункте + Addition, а затем на кнопке Calculate.

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

Среда Visual Studio выведет ваш код и выделит инструкцию, вызвавшую выдачу исключения. Она также выведет на экран диалоговое окно с описанием исключения, которое в данном случае будет таким: «Input string was not in a correct format». («Введенная строка не соответствовала допустимому формату») (рис. 6.2).

Вы можете увидеть, что исключение было выдано внутри метода addValues из-за вызова метода int.Parse. Проблема в том, что данный метод не в состоянии преобразовать текст «John» в допустимое число.

Щелкните в диалоговом окне исключения на пункте Просмотр сведений (View Detail). Откроется еще одно диалоговое окно, в котором будет выведена дополнительная информация об исключении. Если раскрыть пункт System.FormatException, станет видна следующая информация (рис. 6.3).

183917.png

СОВЕТ Некоторые исключения становятся результатом других, ранее выданных исключений. То исключение, о котором оповещает среда Visual Studio, — это всего лишь последнее исключение в цепочке, но обычно имеется более раннее исключение, высвечивающее реальную причину проблемы. Вы можете добраться до этих ранее выданных исключений, если раскроете свойство InnerException в диалоговом окне Просмотр сведений. У внутреннего исключения могут быть свои внутренние исключения, и вы можете продолжать углубляться, пока не увидите исключение, у которого свойство InnerException установлено в null (см. рис. 6.3). Это будет означать, что вы достигли исходного исключения, и, как правило, это и есть исключение, для устранения выдачи которого нужно внести изменения.

06_02.tif 

Рис. 6.2

06_03.tif 

Рис. 6.3

Щелкните в диалоговом окне Просмотр сведений на кнопке OK, а затем в меню Отладка в среде Visual Studio щелкните на пункте Остановить отладку.

Выведите в окно редактора код файла MainPage.xaml.cs и найдите метод addValues.

Добавьте выделенный ниже жирным шрифтом блок try (включая фигурные скобки), охватив им инструкции внутри этого метода, и добавьте обработчик исключения FormatException:

try

{

    int lhs = int.Parse(lhsOperand.Text);

    int rhs = int.Parse(rhsOperand.Text);

    int outcome = 0;

 

    outcome = lhs + rhs;

    expression.Text = $"{lhs} + {rhs}";

    result.Text = outcome.ToString();

}

catch (FormatException fEx)

{

    result.Text = fEx.Message;

}

Если будет выдано исключение FormatException, обработчик исключения выведет текст, содержащийся в свойстве исключения Message в текстовом поле результата, расположенном в нижней части формы.

Щелкните в меню Отладка на кнопке Начать отладку.

При появлении формы наберите в поле Left Operand строку John, а в поле Right Operand — цифру 2. Щелкните на пункте + Addition, а затем на кнопке Calculate.

Обработчик исключения успешно перехватит исключение FormatException, и в текстовое поле Result будет записано сообщение «Input string was not in a correct format». Теперь приложение стало немного надежнее.

Замените строку John числом 10. Наберите в поле Right Operand строку Sharp, а затем щелкните на кнопке Calculate.

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

Замените в поле Right Operand строку Sharp числом 20, щелкните на пункте + Addition, а затем на кнопке Calculate. Теперь приложение работает, как и ожидалось, и показывает в поле Result значение 30.

Замените в поле Left Operand число 10 строкой John, щелкните на пункте - Subtraction, а затем на кнопке Calculate.

Среда Visual Studio передаст управление отладчику и снова выведет оповещение об исключении FormatException. На этот раз ошибка произошла в методе subtractValues, который не включает в себя необходимую структуру выдачи и обработки исключения try-catch.

Щелкните в меню Отладка на кнопке Остановить отладку.

06_04.tif 

Рис. 6.4

Распространение исключений

Добавление блоков try и catch к методу addValues позволило повысить его надежность, но вам нужно применить такую же обработку исключений и к другим методам: subtractValues, multiplyValues, divideValues и remainderValues. Код для каждого из обработчиков исключений будет практически одинаковым, и получится, что вы будете вписывать в каждый метод один и тот же код. Каждый из этих методов вызывается методом calculateClick после того, как пользователь щелкает кнопкой мыши на кнопке Calculate. Стало быть, чтобы избежать дублирования кода обработки исключений, имеет смысл переместить его в метод calculateClick. Если в методе subtractValues, multiplyValues, divideValues или remainderValues будет выдано исключение FormatException, оно распространится в обратном направлении на метод calculateClick для обработки в том порядке, который был рассмотрен ранее в этой главе, в разделе «Необработанные исключения».

Распространение исключения с возвратом управления в вызывающий метод

Выведите в окно редактора код файла MainPage.xaml.cs и найдите метод addValues. Удалите из этого метода блок try и обработчик исключения, вернув его в исходное состояние:

private void addValues()

{

    int leftHandSide = int.Parse(lhsOperand.Text);

    int rightHandSide = int.Parse(rhsOperand.Text);

    int outcome = 0;

 

    outcome = lhs + rhs;

    expression.Text = lhsOperand.Text + " + " + rhsOperand.Text

    result.Text = outcome.ToString();

}

Найдите метод calculateClick. Добавьте к нему код блока try и обработчика исключения, выделенный в следующем примере жирным шрифтом:

private void calculateClick(object sender, RoutedEventArgs e)

{

    try

    {

        if ((bool)addition.IsChecked)

        {

            addValues();

        }

        else if ((bool)subtraction.IsChecked)

        {

            subtractValues();

        }

        else if ((bool)multiplication.IsChecked)

        {

            multiplyValues();

        }

        else if ((bool)division.IsChecked)

        {

            divideValues();

        }

        else if ((bool)remainder.IsChecked)

        {

            remainderValues();

        }

    }

    catch (FormatException fEx)

    {

        result.Text = fEx.Message;

    }

}

Щелкните в меню Отладка на кнопке Начать отладку.

После появления формы наберите в поле Left Operand строку John, а в поле Right Operand — число 2. Щелкните на пункте + Addition, а затем на кнопке Calculate.

Как и раньше, обработчик исключения успешно справится с перехватом исключения FormatException и в текстовое поле Result будет записано сообщение «Input string was not in a correct format». Но не забудьте, что исключение было фактически выдано в методе addValues, а перехвачено обработчиком в методе calculateClick.

Щелкните на пункте - Subtraction, а затем на кнопке Calculate. На этот раз исключение будет выдано методом subtractValues, но распространится в обратном направлении на метод calculateClick и обработается так же, как и раньше.

Проверьте работу программы, выбирая пункты * Multiplication, / Division и % Remainder, и убедитесь в том, что исключение FormatException перехватывается и обрабатывается правильно.

Вернитесь в среду Visual Studio и остановите отладку.

173418.png

ПРИМЕЧАНИЕ Решение о том, в каком именно методе следует перехватывать необработанные исключения, зависит от характера создаваемого приложения. Иногда имеет смысл перехватывать исключения как можно ближе к тому месту, из которого они были выданы. А иногда сподручнее дать исключению возможность распространиться в обратном направлении на метод, вызвавший код, выдавший исключение, и обработать ошибку именно там.

Использование проверяемой и непроверяемой целочисленной арифметики

В главе 2 рассматривались вопросы использования бинарных арифметических операторов, таких как + и *, в отношении простых типов данных, таких как int и double. Там же говорилось, что эти простые типы данных имеют фиксированный размер. Например, в C# размер типа int составляет 32 бита. Поскольку int имеет фиксированный размер, вам точно известен диапазон значений, которые могут в нем содержаться: от –2 147 483 648 до 2 147 483 647.

175157.png

СОВЕТ Если в коде нужно сослаться на минимальное или максимальное значение int, можно воспользоваться свойством int.MinValue или int.MaxValue.

Фиксированный размер int-типа является источником проблем. К примеру, что произойдет, если прибавить единицу к int-переменной, значение которой уже 2 147 483 647? Ответ зависит от способа компиляции приложения. По умолчанию компилятор C# создает код, позволяющий создать переполнение, в результате чего вы получите неверный ответ. (Фактически вычисление пройдет по кругу, остановившись на самом большом отрицательном числе и выдав результат –2 147 483 648.) Причиной такого поведения является необходимость обеспечения высокой производительности: целочисленная арифметика встречается чуть ли не в каждой программе, и издержки на проверке переполнения каждого целочисленного выражения могут привести к очень низкому уровню производительности. Во многих случаях риск является вполне оправданным, поскольку вы знаете (или надеетесь!), что ваши int-значения не достигнут своих лимитов. Если такой подход вам не нравится, можно включить проверку переполнения.

175163.png

СОВЕТ В среде Visual Studio 2015 включение и отключение проверки переполнения осуществляется установкой свойств проекта. Щелкните в обозревателе решений на имени YourProject, где в качестве YourProject должно фигурировать фактическое имя проекта. Щелкните в меню Проект на пункте Свойства: YourProject. В диалоговом окне свойств проекта щелкните на вкладке Сборка. Теперь в правом нижнем углу страницы щелкните на кнопке Дополнительно. В диалоговом окне Дополнительные параметры сборки установите или снимите флажок Проверять арифметические переполнения и потери точности.

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

Запись инструкций checked

Инструкция checked представляет собой блок, предваряемый ключевым словом checked. В том случае, когда целочисленное вычисление в блоке приводит к переполнению, вся целочисленная арифметика, находящаяся в инструкции checked, всегда выдает исключение OverflowException:

int number = int.MaxValue;

checked

{

    int willThrow = number++;                // эта инструкция вызовет переполнение

    Console.WriteLine("this won't be reached");  // а до этой программа не дойдет

}

174850.png

ВНИМАНИЕ Проверке на переполнение будет подвергаться только та целочисленная арифметика, которая находится непосредственно в блоке checked. Например, если одной из инструкций в блоках checked будет вызов метода, проверка не будет применяться к коду, выполняемому в вызываемом методе.

Для создания блока инструкций, не подвергаемых проверке, можно также воспользоваться ключевым словом unchecked. Вся целочисленная ариф­метика в блоке unchecked не проверяется и никогда не выдает исключение Over­flow­Exception, например:

    int number = int.MaxValue;

    unchecked

    {

    int wontThrow = number++;

    Console.WriteLine("this will be reached");

}

Запись проверяемых выражений

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

int wontThrow = unchecked(int.MaxValue + 1);

int willThrow = checked(int.MaxValue + 1);

Операторы составного присваивания (+= и –=), а также операторы инкремента (++) и декремента (--) также являются арифметическим операторами, ими можно управлять с помощью ключевых слов checked и unchecked. Вспомним, что x += y — это то же самое, что x = x + y.

174855.png

ВНИМАНИЕ Использовать ключевые слова checked и unchecked для управления арифметикой с плавающей точкой (нецелочисленной арифметикой) нельзя. Они применимы только к целочисленной арифметике, использующей данные, имеющие типы int и long. Арифметика с плавающей точкой никогда не выдает исключение OverflowException, даже если разделить число на 0,0. (Вспомним, что в главе 2 уже говорилось о том, что в среде .NET Framework для операций с плавающей точкой имеется специальное представление для бесконечности.)

В следующем упражнении вы увидите, как проверяемая арифметика выполняется при использовании среды Visual Studio 2015.

Использование проверяемых выражений

Откройте в Visual Studio 2015 меню Отладка и щелкните на кнопке Начать отладку. Теперь попробуйте перемножить два больших значения.

В поле Left Operand наберите число 9876543, а в поле Right Operand9876543. Щелк­ните на пункте * Multiplication, затем на кнопке Calculate. В поле Result формы появится значение –1195595903. Оно отрицательное и не может быть правильным. Это значение является результатом операции умножения, которая допустила молчаливое переполнение 32-битного лимита, характерного для типа int.

Вернитесь в среду Visual Studio и остановите отладку.

Выведите в окно редактора код файла MainPage.xaml.cs и найдите метод multiplyValues, который должен иметь следующий вид:

private void multiplyValues()

{

    int lhs = int.Parse(lhsOperand.Text);

    int rhs = int.Parse(rhsOperand.Text);

    int outcome = 0;

 

    outcome = lhs * rhs;

    expression.Text = $"{lhs} * {rhs}";

    result.Text = outcome.ToString();

}

В инструкции outcome = lhs * rhs; содержится операция умножения, которая допустила молчаливое переполнение. Отредактируйте эту инструкцию, чтобы вычисляемое значение проходило проверку:

outcome = checked(lhs * rhs);

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

Щелкните в меню Отладка на кнопке Начать отладку. Наберите в поле Left Operand число 9876543, а в поле Right Operand9876543. Щелкните на пункте * Multiplication, а затем на кнопке Calculate. Среда Visual Studio перейдет в режим отладки и сообщит, что умножение выдало исключение OverflowException. Теперь для перехвата этого исключения нужно добавить обработчик исключения и вместо простого выхода с ошибкой получить его более изящную обработку.

Щелкните в меню Отладка на кнопке Остановить отладку. В окне редактора, показывающего код файла MainPage.xaml.cs, найдите метод calculateClick.

Добавьте следующий обработчик исключения (выделен жирным шрифтом) сразу же после уже существующего в этом методе обработчика исключения FormatException:

private void calculateClick(object sender, RoutedEventArgs e)

{

    try

    {

        ...

    }

    catch (FormatException fEx)

    {

        result.Text = fEx.Message;

    }

    catch (OverflowException oEx)

    {

        result.Text = oEx.Message;

    }

}

Логика у этого обработчика исключения точно такая же, как и у обработчика исключения FormatException. Но пока вместо записи общего обработчика Exception есть смысл отделять эти обработчики друг от друга, поскольку в будущем может быть принято решение обрабатывать их исключения по-разному.

Чтобы выполнить сборку и запуск приложения, щелкните в меню Отладка на кнопке Начать отладку. Наберите в поле Left Operand число 9876543, а в поле Right Operand9876543. Щелкните на пункте * Multiplication, а затем на кнопке Calculate.

Второй обработчик исключения успешно перехватит OverflowException и выведет в поле Result сообщение «Arithmetic operation resulted in an overflow» («Арифметическая операция привела к переполнению»).

Вернитесь в Visual Studio и остановите отладку.

Обработка исключений и отладчик Visual Studio

По умолчанию отладчик Visual Studio лишь останавливает выполнение приложения, запущенного в режиме отладки, и оповещает об исключениях, не прошедших обработку. Иногда будет полезно иметь возможность отладить сами обработчики исключений, и в таком случае вам нужно получить возможность трассировки исключений от момента их выдачи приложением до перехвата. Сделать это совсем нетрудно. Щелкните в меню Отладка на пункте Окна, а затем на пункте Параметры исключений. Ниже окна редактора появится панель Параметры исключений (рис. 6.5).

06_05.tif 

Рис. 6.5

Раскройте на панели Параметры исключений строку Common Language Runtime Exceptions, прокрутите список и установите флажок System.OverflowException (рис. 6.6).

06_06.tif 

Рис. 6.6

Теперь при выдаче таких исключений, как OverflowException, Visual Studio будет переходить в отладчик, и для шага с заходом в отладчик исключения можно воспользоваться кнопкой Шаг с заходом на панели инструментов отладчика.

Выдача исключений

Предположим, вы создаете метод по имени monthName, который получает один int-аргумент и возвращает название соответствующего месяца. Например, monthName(1) возвращает January, monthName(2) возвращает February и т.д. Вопрос в том, что должен возвратить метод, если целочисленный аргумент меньше 1 или больше 12? Лучшим ответом будет, что метод вообще не должен ничего возвращать, он должен выдать исключение. Библиотеки классов .NET Framework содержат множество классов исключений, специально разработанных для подобных ситуаций. В большинстве случаев окажется, что один из таких классов описывает условия именно вашей исключительной ситуации. (Если нет, вы легко сможете создать собственный класс исключения, но прежде чем сделать это, вам нужно будет расширить свои познания в языке C#.) В данном случае вам как раз подойдет имеющийся в .NET Framework класс ArgumentOutOfRangeException. Как показано в следующем примере, выдать исключение можно, воспользовавшись инструкцией throw:

public static string monthName(int month)

{

    switch (month)

    {

        case 1 :

            return "January";

        case 2 :

            return "February";

        ...

        case 12 :

            return "December";

        default :

            throw new ArgumentOutOfRangeException("Bad month");

    }

}

Инструкции throw для выдачи исключения нужен объект исключения. Этот объект содержит подробности исключения, в том числе любые сообщения об ошибках. В данном примере используется выражение, создающее новый объект ArgumentOutOfRangeException. Этот объект инициализируется строкой, которая заполняет его свойство Message путем использования конструктора. Более подробно конструкторы рассматриваются в главе 7 «Создание классов и объектов и управление ими».

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

173426.png

ПРИМЕЧАНИЕ Это упражнение носит несколько надуманный характер, поскольку при любой более или менее качественной конструкции приложению будет предоставляться оператор по умолчанию, но в данном случае оно играет сугубо иллюстративную роль.

Выдача исключения

Щелкните в меню Отладка среды Visual Studio 2015 на пункте Начать отладку. Наберите в поле Left Operand число 24, а в поле Right Operand — число 36, после чего щелкните на кнопке Calculate.

В полях Expression и Result ничего не появится. Тот факт, что вы не выбрали вариант оператора, может обнаружиться не сразу. Неплохо бы было написать в поле Result диагностическое сообщение.

Вернитесь в Visual Studio и остановите отладку. В окне редактора, отображающего код файла MainPage.xaml.cs, найдите и изучите метод calculateClick, который должен иметь следующий вид:

private int calculateClick(object sender, RoutedEventArgs e)

{

    try

    {

        if ((bool)addition.IsChecked)

        {

            addValues();

        }

        else if ((bool)subtraction.IsChecked)

        {

            subtractValues();

        }

        else if ((bool)multiplication.IsChecked)

        {

            multiplyValues();

        }

        else if ((bool)division.IsChecked)

        {

            divideValues();

        }

        else if ((bool)remainder.IsChecked)

        {

            remainderValues();

        }

    }

    catch (FormatException fEx)

    {

        result.Text = fEx.Message;

    }

    catch (OverflowException oEx)

    {

        result.Text = oEx.Message;

    }

}

Поля сложения, вычитания, умножения, деления и извлечения остатка от деления представляют собой появляющиеся в форме переключатели. У каждого переключателя есть свойство по имени IsChecked, в котором указывается, что пользователь выбрал именно его. Свойство IsChecked является обнуляемым булевым значением, устанавливаемым в true, если переключатель выбран, или false, если не выбран. (Обнуляемые значения будут более подробно рассмотрены в главе 8 «Основные сведения о значениях и ссылках».) Каскад из инструкций if поочередно проверяет каждый переключатель, отыскивая тот из них, который был выбран. (Переключатели обладают свойством взаимоисключения, поэтому пользователь может выбрать только одно положение.) Если не выбрано ни одно из положений, ни одно из условий if-инструкций не будет вычисляться в true и не будет вызван ни один из методов вычислений.

Можно попытаться решить проблему путем добавления к каскаду if-else еще одной инструкции else для записи сообщения в имеющееся в форме текстовое поле result, но лучше будет отделить обнаружение ошибки и оповещение о ее возникновении от ее перехвата и обработки.

Добавьте к концу списка инструкций if-else еще одну инструкцию else и выдайте исключение InvalidOperationException. Соответствующий код выделен в следующем примере жирным шрифтом:

if ((bool)addition.IsChecked)

{

    addValues();

}

...

else if ((bool)remainder.IsChecked)

{

    remainderValues();

}

else

{

    throw new InvalidOperationException("No operator selected");

}

Для сборки и запуска приложения щелкните в меню Отладка на кнопке Начать отладку.

Наберите в поле Left Operand число 24, в поле Right Operand — число 36, а затем щелкните на кнопке Calculate.

Среда Visual Studio обнаружит, что ваше приложение выдает исключение InvalidOperationException, и откроет диалоговое окно исключения. Приложение выдало исключение, но код его пока не перехватил.

Щелкните в меню Отладка на кнопке Остановить отладку.

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

Перехват исключения

В окне редактора, отображающем код файла MainPage.xaml.cs, добавьте в метод calculateClick, сразу же за двумя уже имеющимися обработчиками исключений, следующий обработчик, выделенный жирным шрифтом:

...

catch (FormatException fEx)

{

    result.Text = fEx.Message;

}

catch (OverflowException oEx)

{

    result.Text = oEx.Message;

}

catch (InvalidOperationException ioEx)

{

    result.Text = ioEx.Message;

}

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

Щелкните в меню Отладка на кнопке Начать отладку. Наберите в поле Left Operand число 24, в поле Right Operand — число 36, а затем щелкните на кнопке Calculate. В поле Result появится сообщение «No operator selected» («Не выбран ни один из операторов»).

173432.png

ПРИМЕЧАНИЕ Если вы попадете в отладчик Visual Studio, то, вероятнее всего, среда Visual Studio у вас настроена на перехват всех исключений общеязыковой среды выполнения (common language runtime) сразу же, как только они будут выданы. В таком случае щелкните в меню Отладка на пункте Продолжить. Как только завершите выполнение данного упражнения, не забудьте отключить перехват в Visual Studio CLR-исключений по мере их выдачи!

Вернитесь в Visual Studio и остановите отладку.

Теперь приложение стало гораздо надежнее. Но некоторые неперехватываемые исключения все же могут выдаваться, что приведет к аварийному завершению работы приложения. Например, если будет предпринята попытка деления на нуль, это приведет к выдаче необрабатываемого исключения DivideByZeroException. (В отличие от деления на нуль числа с плавающей точкой деление на нуль целого числа приводит к выдаче исключения.) Один из способов решения этой проблемы заключается в написании в методе calculateClick еще большего количества обработчиков исключений. Другое решение предусматривает добавление в конец списка обработчиков исключений общего обработчика исключений, перехватывающего исключения Exception. Он станет отлавливать все неожиданные исключения, о которых вы можете забыть или которые могут быть вызваны в результате каких-либо действительно необычных обстоятельств.

173442.png

ПРИМЕЧАНИЕ Использование общего обработчика для перехвата исключения Exception не может служить оправданием для игнорирования перехвата конкретных исключений. Чем определеннее подход к обработке исключений, тем проще будет поддерживать ваш код и выявлять причины любых присущих ему или часто повторяющихся проблем. Исключение Exception должно использоваться только для действительно исключительных ситуаций. Чтобы сделать следующее упражнение нагляднее, в эту категорию зачислено и исключение «деление на нуль». Но убедившись в том, что данное исключение с большой долей вероятности может выдаваться в реальном приложении, лучше будет добавить в него для исключения Divide­ByZero­Exception отдельный обработчик.

Перехват необработанных исключений

В окне редактора, отображающего код файла MainPage.xaml.cs, добавьте к методу calculateClick в конец перечня уже имеющихся обработчиков исключений следующий обработчик исключения:

catch (Exception ex)

{

    result.Text = ex.Message;

}

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

Щелкните в меню Отладка на кнопке Начать отладку. Теперь вы будете предпринимать попытки выполнения ряда вычислений, о которых заведомо известно, что они станут причиной выдачи исключений, и убеждаться в том, что все эти исключения обрабатываются должным образом.

Наберите в поле Left Operand число 24, в поле Right Operand — число 36, а затем щелкните на кнопке Calculate. Убедитесь в том, что в поле Result по-прежнему появляется диагностическое сообщение «No operator selected». Это сообщение было создано обработчиком исключения InvalidOperationException.

Наберите в поле Left Operand строку John, щелкните на пункте + Addition, а затем на кнопке Calculate. Убедитесь в том, что в поле Result появилось диагностическое сообщение «Input string was not in a correct format». Это сообщение было создано обработчиком исключения FormatException.

Наберите в поле Left Operand число 24, а в поле Right Operand — число 0. Щелкните на пункте / Division, а затем на кнопке Calculate. Убедитесь в том, что в поле Result появилось диагностическое сообщение «Attempted to divide by zero». Это сообщение было создано общим обработчиком исключений Exception.

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

Использование блока finally

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

Посмотрите на следующий пример, позаимствованный из кода главы 5 «Использование инструкций составного присваивания и итераций». Нетрудно предположить, что вызов reader.Dispose всегда будет происходить по завершении цикла while, ведь он находится в коде именно после цикла:

TextReader reader = ...;

...

string line = reader.ReadLine();

while (line != null)

{

    ...

    line = reader.ReadLine();

}

reader.Dispose();

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

Способ обеспечения безусловного запуска инструкции независимо от того, было или не было выдано исключение, состоит в записи инструкции внутри блока finally. Этот блок должен находиться сразу же после блока try или сразу же после последнего обработчика, расположенного после блока try. Как только программа входит в блок try, связанный с блоком finally, код, находящийся в блоке finally, будет безусловно выполнен, даже если будет выдано исключение. Если исключение выдается и обрабатывается локально, то сначала выполняется код обработчика исключения, а затем уже блок finally. Если же исключение не перехватывается локально (то есть среда выполнения, чтобы найти обработчик, вынуждена вести поиск по перечню вызывающих методов), то сначала выполняется блок finally. Как бы то ни было, блок finally выполняется всегда.

В качестве решения проблемы с выполнением метода reader.Dispose может послужить следующий код:

TextReader reader = ...;

...

try

{

    string line = reader.ReadLine();

    while (line != null)

    {

        ...

        line = reader.ReadLine();

    }

}

finally

{

    if (reader != null)

    {

        reader.Dispose();

    }

}

Даже если исключение выдается в момент чтения файла, блок finally гарантирует безусловное выполнение инструкции reader.Dispose. Еще один способ выхода из этой ситуации будет показан в главе 14 «Использование сборщика мусора и управление ресурсами».

Выводы

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

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 7.

Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелк­ните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Перехватить конкретное исключение

Напишите обработчик исключения, перехватывающий конкретный класс исключений, например:

try

{

    ...

}

catch (FormatException fEx)

{

    ...

}

Обеспечить постоянную проверку целочисленной арифметики на переполнение

Воспользуйтесь ключевым словом checked, например:

int number = Int32.MaxValue;

checked

{

    number++;

}

Выдать исключение

Воспользуйтесь инструкцией throw, например:

throw new FormatException(source);

Перехватить все исключения в одном обработчике исключений

Напишите обработчик исключений, перехватывающий Exception, например:

try

{

    ...

}

catch (Exception ex)

{

    ...

}

Гарантировать безусловное выполнение кодового фрагмента, даже если выдано исключение

Запишите код внутри блока finally, например:

try

{

    ...

}

finally

{

    // выполняется всегда

}

Назад: 5. Использование инструкций составного присваивания и итераций
Дальше: Часть II. Основные сведения об объектной модели C#

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com