Принципы объектно-ориентированного программирования

         

Обработка ошибок с помощью исключений



ГЛАВА 12
Обработка ошибок с помощью исключений

Обзор обработки исключений Основной синтаксис обработки исключений Передача исключения Улавливание исключения Повторная передача исключения Освобождение ресурсов с помощью finally

Сравнение методов обработки ошибок Преимущества обработки исключений над применением кодов возврата Обработка ошибок в правильном контексте Улучшение читабельности кода Передача исключений из конструкторов Класс System. Exception Конструирование объекта Exception Свойство StackTrace Улавливание множества типов исключений Получение собственных классов Exception Разработка собственного кода с обработкой исключений Проблемы создания блоков try Проблемы создания блоков catch

Одно из основных назначений .NET Common Language Runtime (CLR) — недопущение ошибок (что достигается такими средствами, как автоматическое управление памятью и ресурсами в управляемом коде) или хотя бы их обнаружение во время компиляции (благодаря строго типизированной системе). Однако некоторые ошибки можно обнаружить только в период выполнения, а значит, для всех языков, соответствующих спецификации Common Language Specification (CLS), должен быть предусмотрен единый метод реакции на ошибки. Эта глава посвящена системе обработки ошибок, реализованной в CLR, — обработке исключений. Сначала мы изучим общий механизм и основы синтаксиса обработки исключений. Вы увидите, как обработка исключений соотносится с наиболее распространенными на сегодняшний день методами обработки ошибок, и поймете преимущества обработки исключений над другими методиками. Затем мы углубимся в наиболее специфические вопросы обработки исключений в .NET, такие как применение класса Exception и производных от него собственных классов исключений. Последний раздел посвящен созданию приложений с обработкой исключений.



Класс System. Exception



Класс System. Exception

Как вы помните, все исключения должны иметь тип System.Exception (или производный от него). По сути класс System.Exception является базовым для нескольких классов исключений, которые можно применять в вашем

С#-коде. Большинство унаследованных от System.Exception классов не добавляют функциональности базовому классу. Тогда зачем было суетиться с производными классами, если они существенно не отличаются от базового? Причина в том, что один блок try может иметь несколько блоков catch, каждый из которых определяет особый тип исключения (вы это скоро увидите). Это позволяет коду обрабатывать различные исключения в соответствии с их типом.



Конструирование объекта Exception



Конструирование объекта Exception

На момент написания этой книги для класса System.Exception существовало четыре различных конструктора:

public Exception ();

public Exception(String);

protected Exception(SerializationInfo, StreamingContext);

public Exception(String, Exception);

Первый используется по умолчанию. У него нет аргументов, и он просто устанавливает переменные-члены по умолчанию. Такое исключение обычно передается так:

// Обнаружена ошибка, throw new ExceptionQ;

Единственный аргумент второго конструктора — строковое значение, представляющее сообщение об ошибке — именно эту форму вы встречали в большинстве примеров этой главы. Код, уловивший исключение, получает это сообщение через свойство System.Exception.Message. Вот пример обеих частей, участвующих в обработке исключения:

using System;

class ThrowExceptionS {

class FileOps {

public void FileOpen(String fileName) {

// ...

throw new Exception("Hy достали...");

}

public void FileReadQ

{

}

}

public static void MainQ {

// Код, улавливающий исключение.

try

{

FileOps fileOps = new FileOpsO;

fileOps.FileOpen("c:\\test.txt"); fileOps. FileReadO; >

catch(System.Exception e) <

Console.WriteLine(e.Message); } } }

Третий конструктор инициализирует экземпляр класса Exception последовательными данными.

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

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

В следующем коде клиент улавливает на верхнем уровне System.Exception и выводит сообщение, содержащееся во внутреннем объекте-исключении. Если когда-нибудь метод DoWork будет передавать другие типы исключений (через внутреннее исключение объекта System.Exception), клиентский код будет продолжать работать.

using System;

using System.Globalization;

class FooLib

{

protected bool IsValidParam(string value)

{

bool success = false;

if (value.Length == 3)

{

char c1 = value[0]; if (Char.IsNumber(cD)

{

char c2 = value[2]; if (Char.IsNumber(c2))

{

if (value[1] == '.')

success = true; } > >

return success; }

public void DoWork(string value)

<

if (!IsValidParam(value))throw new Exception

("", new FormatExceptionC'yKaaaH недопустимый "+

"параметр"));

Console.WriteLine("Выполнено с кодом '{0}'", value); > }

class FooLibClientApp

{

public static void Main(string[] args)

{

FooLib lib = new FooLibO;

try

{

lib.DoWork(args[0]);

}

catch(Exception e)

{

Exception inner = e.InnerException;

k

Console.WriteLine(inner.Message); } >

>

 

Обработка ошибок в правильном контексте



Обработка ошибок в правильном контексте

Один из основных принципов хорошего программирования — сильная связность (tight cohesion), когда речь идет о назначении методов. Сильно связанными называются методы, выполняющие единственную задачу. Главное достоинство сильной связности в том, что если некий метод выполняет единственное действие, он с большой степенью вероятности будет переносимым и сможет применяться в разных сценариях. Разумеется, такой метод гораздо проще отлаживать и сопровождать. Однако по отношению к обработке ошибок сильно связный код порождает одну главную проблему. Рассмотрим пример.

Класс Access Database применяется для генерации и работы с БД Microsoft Access. Допустим, у этого класса есть статический метод Generate-Database. Поскольку он используется для создания новой БД Access, ему предстоит выполнить несколько задач. Например, он должен создать физический файл БД и определенные таблицы (включая нужные вашему приложению строки и столбцы), а также определить все индексы и связи. Возможно, этот метод должен даже создать некоторых пользователей по умолчанию и разрешения.

Проблема со структурой программы такова: какой метод и как должен обрабатывать ошибку, возникшую в методе Createlndexesl Кажется очевидным, что обрабатывать ошибку должен метод, вызвавший Gene-rateDatabase, но как он это будет делать? Он не будет понимать, как обрабатывать ошибку, которая возникла на несколько уровней ниже. Как мы говорили, вызывающий метод находится не в том контексте, чтобы обрабатывать ошибку. Другими словами, поддающуюся интерпретации информацию об ошибке может создать только тот метод, в котором произошел сбой. В таком случае, если в классе Access Database используются коды возврата, каждый метод должен проверять каждую отдельную ошибку, которую каждый другой метод обязан возвращать. Очевидной проблемой является то, что вызывающему методу верхнего уровня потенциально придется обрабатывать безумное число кодов ошибок. Кроме того, это затрудняет сопровождение. Каждый раз, когда для любого метода будет добавляться новый код ошибки, нужно будет обновлять каждый экземпляр, в котором есть обращение к измененному методу, чтобы он обрабатывал новый код ошибки. Излишне говорить, что это недешевое решение в терминах совокупной стоимости владения (total cost of ownership, TCO).

Обработка исключений решает все эти проблемы, позволяя вызывающему методу реагировать на определенные типы исключений. В нашем примере, если класс AccessDatabaseException является производным от Exception, его можно использовать для любых типов ошибок, возникающих в любом из методов AccessDatabase. (Я расскажу о классе Exception и о создании собственных производных классов исключений в разделе "Класс System. Exception" .) В таком случае, если в методе Create Indexes произойдет сбой, он должен построить и передать исключение типа AccessDatabseException. Вызывающий метод уловит это исключение и сможет проанализировать объект Exception, чтобы разобраться, что именно произошло. Таким образом, вместо того чтобы обрабатывать каждый возможный код возврата, который возвращает GenerateDatbase и любой из вызываемых им методов, вызывающий метод уверен, что если сбой произойдет в любом из методов, то будет возвращена корректная информация об ошибке. Обработка исключений дает еще одно преимущество: поскольку информация об ошибке содержится в классе, при добавлении новых кодов ошибок вызывающий метод изменять не нужно. А разве расширяемость — возможность создания чего-либо и дальнейшее добавление без изменения или разрушения существующего кода — не одно из основных качеств ООП, с которого все начинается? Так что концепция улавливания и обработки ошибок в правильном контексте — одно из самых значительных преимуществ обработки исключений.

 

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



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

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

Большинство исключений связано и с другой проблемой — контекстом. Рассмотрим пример. Допустим, вы пишете код с сильными связями — код, в котором один метод отвечает за одно свое действие. Он может выглядеть примерно так:

public void Foo() <

File file = OpenFileCString fileName);

while (Ifile.IsEOFO)

<

String record = file.ReadRecordO;

>

CloseFileO; >

public void OpenFileCString fileName) {

// Пытаемся заблокировать и открыть файл.

}

Если возникнет сбой в методе OpenFile, этот метод не сможет обработать ошибку. Дело в том, что он отвечает только за открытие файлов. Он не может определить, с чем связана проблема при открытии файла: с катастрофической ошибкой или просто недоразумением. Следовательно, OpenFile не может обработать ошибку, поскольку он, что называется, не находится в верном контексте.

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

 

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



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

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



Освобождение ресурсов с помощью finally



Освобождение ресурсов с помощью finally

Один трудный вопрос с обработкой ошибок связан с тем, что код всегда выполняется независимо от того, уловлено ли исключение. Например, вы выделили ресурс, такой как физическое устройство или файл данных. Теперь предположим, что вы открыли этот ресурс и вызвали метод, передавший исключение. Независимо от того, может ли ваш метод продолжить работу с ресурсом, вам в любом случае нужно освободить или закрыть ресурс. Здесь-то и применяется ключевое слово finally:

using System;

public class ThrowExceptionlApp {

public static void ThrowExceptionQ

{

throw new ExceptionO;

}

public static void MainQ {

try {

Console.WriteLine("try..."); >

catch(Exception e) {

Console.WriteLine("catch..."); }

finally {

Console.WriteLine("finally"); } } >

Как видите, finally позволяет избежать двойного кодирования освобождения ресурса: в блоке catch и после блоков try/catch. Независимо от того, передано ли исключение, будет выполнен код в блоке finally.



Передача исключений из конструкторов



Передача исключений из конструкторов

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

try {

// Если конструктор объекта AccessDatabase не сможет нормально

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

AccessDatabase accessDb = new AccessDatabaseO; >

catch(Exception e) {

// Анализ уловленного исключения. }

 

Передача исключения



Передача исключения

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

throw [выражение];

Мы рассмотрим разные способы передачи исключений чуть позже. Сейчас достаточно представлять себе, что при передаче исключения вам нужно передать объект типа System.Exception (или производный класс). Далее приведен пример метода, определившего, что произошла неисправимая ошибка и ему нужно передать исключение вызывающему методу. Обратите внимание, как создается новый экземпляр объекта System. Exceptions и передается вызывающему методу.

public void SomeMethodO {

// Обнаружена ошибка.

throw new Exception(); }

 

Подведем итоги



Подведем итоги

Синтаксис обработки исключений в С# прост и понятен, а реализация обработки исключений в вашем приложении сводится к простой реализации некотррых методов. Основная цель .NET CLR — помочь предотвратить ошибки периода выполнения за счет строгой системы типов и эффективной обработки возникающих во время выполнения ошибок. Применение кода обработки исключений в ваших приложениях сделает их более надежными, крепкими и простыми в сопровождении.



Получение собственных классов Exception



Получение собственных классов Exception

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

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

using System;

public class TestException : Exception

{

// Вероятней всего у вас здесь будут дополнительные // методы и свойства, расширяющие .NET Exception

// Конструктор базового класса Exception, public TestExceptionO

:base() {} public TestException(String message)

:base(message) {} public TestException(String message, Exception innerException)

:base(message, innerException) {}

}

public class DerlvedExceptionTestApp

{

public static void ThrowExceptionO

{

throw new TestException("ошибка"); >

public static void Main() {

try

{

ThrowExceptionO;

}

catch(Exception e)

{

Console. WriteLine(e.ToStringO);

} } }

Метод ToString выводит комбинацию свойств: текстовое представление имени класса исключения, строковое сообщение, переданное конструктору исключения и StackTrace.

TestException; ошибка

at DerivedExceptionTestApp.Main()



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



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

Случается, что метод, уловивший исключение и сделавший все, что он может в своем контексте, затем повторно передает (rethrow) исключение выше. Это легко реализовать с помощью ключевого слова throw:

using System;

class RethrowApp {

static public void MainQ

{

Rethrow rethrow = new RethrowO;

try {

rethrow.Foo(); }

catch(Exception e) {

Console.WriteLine(e.Message); } }

public void Foo() {

try {

Bar(); }

catch(Exception) {

// Обработка ошибки, throw; > }

public void Bar() {

throw new Exception("Передано от Rethrow.Bar"); }

}

В этом примере Main вызывает Foo, который вызывает Bar. Bar передает исключение, которое улавливает Foo. После этого Foo неким образом обрабатывает исключение и повторно передает его наверх методу Main с помощью ключевого слова throw.

 

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



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

При использовании кодов возврата вызываемый метод возвращает код ошибки, и причина ошибки обрабатывается вызывающим методом. Поскольку обработка происходит вне области видимости вызываемого метода, нет гарантии, что вызывающий метод проверит возвращенный код ошибки. Скажем, вы пишете класс CommaDelimitedFile, реализующий чтение и запись стандартных файлов с разделителями. Ваш класс, в частности, должен иметь методы для открытия и чтения данных из файла. При старом подходе уведомления об ошибках эти методы должны возвращать переменные, которые должны проверяться вызывающим методом, чтобы определить, успешно ли выполнился вызов. Если пользователь вашего класса вызвал метод CommaDelimitedFile.Open, а затем пытается вызвать метод CommaDelimitedFile.Read, не проверив, успешно ли завершился Open, это может привести к очень неприглядным результатам (и скорей всего это случится, когда вы демонстрируете свою программу очень важному заказчику). Но если метод Open этого класса передаст исключение, вызывающий метод будет поставлен перед фактом, что в методе Open произошел сбой. Дело в том, что при каждой передаче исключения управление передается наверх, пока исключение не улавливается. Такой код может выглядеть следующим образом:

using System;

class ThrowException2App

{

class CommaOelimitedFile

{

protected string fileName;

public void Open(strlng fileName)

{

this.fileName = fileName;

// Пытаемся открыть файл

// и передаем исключение при возникновении ошибки, throw new Exception("Открыть файл не удалось"); }

public bool Read(string record) {

// Код для чтения файла, return false; // EOF } }

public static void Maln() {

try

{

Console.WriteLlne("Пытаемся открыть файл");

CommaDelimltedFlle file = new CommaDelimltedFileO; file.Open("c:\\test.csv");

string record = "";

Console.WrlteLine("Читаем файл");

while (file.Read(record) == true) {

Console.Writeline(record); }

Console.WriteLine("Чтение файла закончено"); }

catch (Exception e) {

Console.WrlteLine(e.Message); } } }

Здесь, если метод CommaDelimitedFile.Open или CommaDelimited-File.Read передаст исключение, вызывающий метод вынужден будет на него прореагировать. Если ни вызывающий, ни другие методы в текущем кодовом сегменте не уловят исключение данного типа, приложение прервется. Заметьте: обращение к методу Open находится в блоке и, следовательно, попытка недопустимого чтения (если Open передаст исключение) производиться не будет. Это объясняется тем, что управление будет передано из блока try, в котором производится обращение к Open, к первой строке блока catch. Таким образом, одно из самых больших преимуществ обработки исключений над кодами возврата в том, что исключения гораздо труднее проигнорировать.



Проблемы создания блоков catch



Проблемы создания блоков catch

Единственное, что должно быть в блоке catch, — это код, хотя бы частично обрабатывающий уловленное исключение. Например, можно произвести некоторые действия, возможные на момент получения исключения, и повторно передать его, чтобы была возможной его дальнейшая обработка. Это иллюстрирует следующий пример. Foo вызывает Ваг, который работает с БД. Поскольку используются транзакции, Ваг должен улавливать любую возникающую ошибку и откатывать незафиксированные изменения, прежде чем передать исключение наверх методу Foo. using System;

class WhenToCatchApp {

public void Foo() {

try {

Bar(); }

catch(Exception e) {

Console.WriteLine(e.Message); } }

public void Bar() {

try {

// Вызов метода, устанавливающего "границы фиксации" Console.WriteLine("устанавливает границу фиксации");

// Call Baz to save data.

Console.WriteLine("вызываем Baz для сохранения данных");

Baz();

Console.WriteLine("фиксируем сохраненные данные"); } catch(Exception)

{

// В данном случае Bar следует уловить исключение, // поскольку он делает нечто существенное //(откатывает незафиксированные изменения в БД).

Console.WriteLine("Откатываем незафиксированные "+

"изменения и затем повторно передаем исключение");

throw; > }

public void BazQ {

throw new ExceptionC'e Baz ошибка при работе с БД");

>

public static void MainQ

{

WhenToCatchApp test = new WhenToCatchAppQ; test.Foo(); // Этот метод в конечном счете выведет

// сообщение об ошибке. > }

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



Проблемы создания блоков try



Проблемы создания блоков try

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

using System;

class WhenNotToCatchApp {

public void Foo() {

try {

Bar(); }

catch(Exception e) {

Console.WriteLine(e.Message); } }

public void Bar() {

try

{

Baz();

}

catch(Exception e)

<

// Bar не нужно это улавливать, поскольку он ничего // не делает, а только передает исключение наверх.

throw;

} }

public void Baz() {

throw new Exception("Исключение, изначально переданное Baz"); }

public static void Main() {

WhenNotToCatchApp test = new WhenNotToCatchAppO; test.FooO; // Этот метод в конечном счете // выведет сообщение об ошибке. } }

В этом примере .йя/-обнаруживает исключение, посылаемое Baz, хотя ничего с ним не делает и лишь пересылает его Foo. Уловив повторно переданное исключение, Foo обрабатывает информацию (в данном случае отображает сообщение об ошибке, являющееся свойством объекта Exception). Вот пара причин, по которым методу, осуществляющему лишь повторную передачу исключения, не следует его улавливать.

Поскольку метод ничего не делает с исключением, лучше всего опустить ненужный блок catch. Какой смысл в коде-бездельнике? Если тип исключения, переданного Baz, изменяется, вам придется менять блоки catch и в Ваг и в Foo. Зачем создавать ситуацию, когда вам придется изменять код, который ничего не делает?

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



Разработка собственного кода с обработкой исключений



Разработка собственного кода с обработкой исключений

Итак, мы рассмотрели базовые концепции применения обработки исключений и семантику передачи и улавливания исключений. Теперь обратимся к не менее важному аспекту обработки исключений — разработке собственной системы с обработкой исключений. Допустим, у вас три метода: Foo, Bar и Baz. Foo вызывает Bar, который в свою очередь вызывает Ват.. Если про Baz известно, что он передает исключения, должен ли Ваг улавливать исключения, даже если он этого не может или не хочет ничего с ними делать? Как разбить код на блоки try и catch!

 

Сравнение методов обработки ошибок



Сравнение методов обработки ошибок

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

Стандартным подходом к обработке ошибок всегда была передача кода ошибки вызывающему методу, который должен был декодировать возвращаемое значение и действовать соответствующим образом. Воз-вр^щаемое значение может быть простым, как, скажем, базовый тип С или C++, или это может быть указатель на более сложный объект, содержащий всю информацию, необходимую для оценки и понимания ошибки. Более развитые методы обработки ошибок включают соответствующую законченную подсистему. В этом случае вызванный метод указывает ошибку подсистеме, а затем возвращает код ошибки вызывающему методу. Вызывающий метод затем вызывает глобальную функцию, экспортируемую из подсистемы обработки ошибок, чтобы определить причину последней зарегистрированной подсистемой ошибки. Пример такого подхода — Microsoft Open Database Connectivity (ODBC) SDK. Однако независимо от конкретной семантики концепция остается той же: активизирующий метод так или иначе вызывает другой метод и анализирует возвращаемое значение, чтобы узнать, успешно ли завершился вызванный метод. Этот подход, хотя и был стандартом долгие годы, во многом очень устарел. В следующих разделах описываются случаи, в которых обработка исключений имеет громадные преимущества над применением кодов возврата.

 

Свойство StackTrace



Свойство StackTrace

Другое полезное свойство класса System.Exception — StackTrace. Оно позволяет определить — в любой точке, где имеется допустимый объект System.Exception, — как выглядит текущий стек вызовов. Взгляните на код:

using System;

class StackTraceTestApp {

public void Open(String fileName) {

Lock(fileName);

// ... }

public void Lock(String fileName) {

// Возникла ошибка.

throw new Exception("Невозможно блокировать файл"); }

public static void Main() <

StackTraceTestApp test = new StackTraceTestAppO;

try {

test.Open("c:\\test.txt");

// Работа с файлом. >

catch(Exception e) <

Console.WriteLine(e.StackTrace); } > >

Этот пример отобразит следующее: at StackTraceTest.Main()

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



Улавливание исключения



Улавливание исключения

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

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

Для обнаружения исключения нужно ограничить код, который вы собираетесь выполнить, блоком try, а затем указать, какие типы исключений этот код может обрабатывать в блоке catch. Все операторы из блока try будут обрабатываться по порядку, если только один из вызванных методов не передаст исключение. Если это произойдет, управление будет передано на первую строку соответствующего блока catch. Под "соответствующим блоком" я подразумеваю блок, определенный для улавливания исключений данного типа. Вот пример метода (Foo), вызывающего и обнаруживающего исключения, посылаемые другим методом (Ваг):

public void Foo() {

try

{

BarQ;

}

catch(System.Exception e)

{

// Обработка ошибки.

> }

У вас может возникнуть вопрос: "Что будет, если Ваг передаст исключение, a Foo его не уловит?" (Это может иметь место, если обращение к Ваг не содержится в блоке try.) Результат зависит от структуры приложения. При передаче исключения управление передается по стеку вызовов наверх, пока не будет найден блок catch для исключения данного типа. Если метод с подходящим блоком catch не обнаруживается, приложение прерывается. Следовательно, если один метод вызывает другой — передающий исключение — структура приложения должна быть такова, чтобы метод в стеке вызовов смог обработать исключение.



Улавливание множества типов исключений



Улавливание множества типов исключений

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

try {

Foo(); // Может передавать FooException.

Bar(); // Может передавать BarException. }

catch(FooException e) {

// Обработка ошибки. }

catch(BarException e) {

// Обработка ошибки. >

catch(Exception e) { >

Теперь исключение каждого типа может быть обработано в отдельном блоке catch (код обработки ошибки). Однако самое важное здесь то, что базовый класс обрабатывается последним. Это понятно: ведь все исключения являются производными от System.Exception, и если вы поместите блок catch для базового класса первым, до других блоков catch выполнение не дойдет. С учетом этого следующий код будет забракован компилятором:

try {

РоЪ(); // Может передавать FooException.

BarQ; // Может передавать BarException. }

catch(Exception e) {

// ""ОШИБКА - ЭТО КОМПИЛИРОВАТЬСЯ НЕ БУДЕТ }

catch(FooException e) {

// Обработка ошибки. }

catch(BarException e) <

// Обработка ошибки. }

// следуют в большинстве программ на С#), называть свои классы

// исключений, добавляя в конец слово "Exception". Например, если

// вы хотите сделатькласс Afv/ia/icyO-opAto производным от класса

Другое эмпирическое правило: при создании собственного производного класса исключений реализовывать все четыре конструктора 5vjfem.£xcepf/on. Повторю: это не обязательно, но это улучшиг совмее^имоси. с другим ходом на С#, которщй будет применять ваш пользователь.

 

Улучшение читабельности кода



Улучшение читабельности кода

При использовании кода с обработкой исключений программа становится гораздо читабельнее, что напрямую сказывается на снижении затрат на ее сопровождение. Причина такого улучшения — в синтаксисе обработки исключений по сравнению с обработкой кодов возврата. При использовании кодов возврата в методе AccessDatabase.GenerateDatbase потребовалась бы примерно такая обработка ошибок:

public bool GenerateDatabaseO {

if (CreatePhysicalDatabaseO) <

If (CreateTablesQ) {

if (CreatelndexesQ) {

return true; }

else {

// Обработка ошибки, return false; > }

else {

// Обработка ошибки, return false; } >

else {

// Обработка ошибки, return false; } >

Добавьте к этому коду еще несколько проверок, и у вас получится жуткая мешанина из кода, реализующего бизнес-логику, и проверок ошибок. Если вы делаете для каждого блока в коде отступ в 4 пробела, первый символ строки у вас может оказаться в 20-й позиции или еще дальше. Коду как таковому это не угрожает, но он становится трудным для чтения и сопровождения, а трудно сопровождаемый код — рассадник ошибок. Посмотрим, как тот же пример выглядит при использовании обработки исключений:

// Вызывающий код. '

try

{

AccessDatabase accessDb = new AccessDatabaseO;

accessDb.GenerateDatabaseO; >

catch(Exception e) {

// Анализируем уловленное исключение. }

// Определение метода AccessDatabase.GenerateOatabase. public void GenerateDatabaseQ {

CreatePhysicalDatabase();

CreateTablesO;

CreatelndexesO; }

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