Читать в оригинале

<< ПредыдущаяОглавлениеСледующая >>


1.3.2. Схема обработки исключений в C#

Язык C# наследовал схему исключений языка С++, внеся в нее свои коррективы. Рассмотрим схему подробнее и начнем с синтаксиса конструкции try- catch-finally:

try {... }
catch (T1 e1) {...}
...
catch(Tk ek) {...}
finally {...}

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

Выбрасывание исключений. Создание объектов Exception

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

Синтаксически оператор throw имеет вид:

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

Выражение throw задает объект класса, являющегося наследником класса Exception. Обычно это выражение new, создающее новый объект. Если оно отсутствует, то повторно выбрасывается текущее исключение. Если исключение выбрасывается операционной системой, то она сама классифицирует исключение, создает объект соответствующего класса и автоматически заполняет его поля.

В рассматриваемой нами модели исключения являются объектами, класс которых представляет собой наследника класса Exception. Этот класс и многочисленные его наследники является частью библиотеки FCL, хотя и разбросаны по разным пространствам имен. Каждый класс задает определенный тип исключения в соответствии с классификацией, принятой в Framework .Net. Вот лишь некоторые классы исключений из пространства имен System: ArgumentException, ArgumentOutOfRangeException, ArithmeticException, Badlmage- FormatException, DivideByZeroException, OverflowException. В пространстве имен System.IO собраны классы исключений, связанных с проблемами ввода-вывода: DirectoryNotFoundException, FileNotFoundException и многие другие. Имена всех классов исключений заканчиваются словом Exception. Разрешается создавать собственные классы исключений, наследуя их от класса Exception.

При выполнении оператора throw создается объект te, класс TE которого характеризует текущее исключение, а поля содержат информацию о возникшей исключительной ситуации. Выполнение оператора throw приводит к тому, что нормальный процесс вычислений на этом прекращается. Если это происходит в охраняемом try-блоке, то начинается этап «захвата» исключения одним из обработчиков исключений.

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

Блок catch - обработчик исключения имеет следующий синтаксис:

catch (T e) {...}

Класс T, указанный в заголовке catch-блока, должен принадлежать классам исключений. Блок catch с формальным аргументом e класса T потенциально способен захватить текущее исключение te класса TE, если и только если объект te совместим по присваиванию с объектом e. Другими словами, потенциальная способность захвата означает допустимость присваивания e = te, что возможно, когда класс TE является потомком класса T. Обработчик, класс T которого является классом Exception, является универсальным обработчиком, потенциально он способен захватить любое исключение, поскольку все они являются его потомками.

Потенциальных захватчиков может быть много, исключение захватывает лишь один - тот из них, кто стоит первым в списке проверки. Каков порядок проверки? Он довольно естественный. Вначале проверяются обработчики в порядке следования их за try-блоком, и первый потенциальный захватчик становится активным, захватывая исключение и выполняя его обработку. Отсюда становится ясно, что порядок следования в списке catch-блоков крайне важен. Первыми идут наиболее специализированные обработчики, далее по мере возрастания универсальности. Так, вначале должен идти обработчик исключения DivideByZeroException, а уже за ним - ArithmeticException. Универсальный обработчик, если он есть, должен стоять последним. За этим наблюдает статический контроль типов. Если потенциальных захватчиков в списке catch-блоков нет (сам список может отсутствовать), то происходит переход к списку обработчиков охватывающего блока. Напомним, что try-блок может быть вложен в другой try-блок. Когда же будет исчерпаны списки вложенных блоков, а потенциальный захватчик не будет найден, то произойдет подъем по стеку вызовов. На рис. 5 показана цепочка вызовов, начинающаяся с процедуры Main.

Рис. 5. Цепочка вызовов, хранящаяся в стеке вызовов

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

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

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

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

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

Блок finally

До сих пор ничего не было сказано о важном участнике схемы обработки исключений - блоке finally. Так как рассматриваемая схема является схемой без возобновления, то это означает, что управление вычислением неожиданно покидает try-блок. Просто так в большинстве случаев этого делать нельзя - нужно выполнить определенную чистку. Прежде всего, удаляются все локальные объекты, созданные в процессе работы блока. В языке С++ эта работа требовала вызова деструкторов объектов. В C#, благодаря автоматической сборке мусора, освобождением памяти можно не заниматься, достаточно освободить стек. Но в блоке try могли быть заняты другие ресурсы - открыты файлы, захвачены некоторые устройства. Освобождение ресурсов, занятых try-блоком, выполняет finally-блок. Если он присутствует, он выполняется всегда, сразу же после завершения работы try-блока, как бы последний ни завершился. Блок try может завершиться вполне нормально без всяких происшествий, и управление достигнет конца блока, выполнение может быть прервано оператором throw, управление может быть передано другому блоку из-за выполнения таких операторов как goto, return - во всех этих случаях, прежде чем управление будет передано по предписанному назначению (в том числе, прежде чем произойдет захват исключения), предварительно будет выполнен finally-блок, который освобождает ресурсы, занятые try-блоком, а параллельно будет происходить освобождение стека от локальных переменных.

 



<< ПредыдущаяОглавлениеСледующая >>