Файл: Отладка приложений. Организация обработки исключений. Корректность и устойчивость программных систем Корректность и устойчивость.docx

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 30.11.2023

Просмотров: 50

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.


  • Функцией, возвращающей значение.

  • Методом интерфейса.

  • Методом с модификатором override. Возможно его задание для virtual-метода. В этом случае атрибут наследуется методами потомков.

Атрибут Conditional обычно с аргументом DEBUG сопровождает модули, написанные для целей отладки. Но использование атрибута Conditional не ограничивается интересами отладки. Зачастую проект может использоваться в нескольких вариантах, например облегченном и более сложном. Методы, вызываемые в сложных ситуациях, например ComplexMethod, имеющий атрибут условной компиляции, будут вызываться только в той конфигурации, где определена константа COMPLEX.

Классы Debug и Trace

Атрибут условной компиляции Conditional характеризует метод, но не отдельный оператор. Иногда хотелось бы иметь условный оператор печати, не создавая специального метода, как это было сделано в предыдущем примере. Такую возможность и многие другие полезные свойства предоставляют классы Debug и Trace.

Классы Debug и Trace – это классы-двойники. Оба класса находятся в пространстве имен Diagnostics, имеют идентичный набор статических свойств и методов с идентичной семантикой. В чем же разница? Методы класса Debug имеют атрибут условной компиляции с константой DEBUG, действуют только в Debug-конфигурации проекта и игнорируются в Release-конфигурации. Методы класса Trace включают два атрибута Conditional с константами DEBUG и TRACE и действуют в обеих конфигурациях.

Одна из основных групп методов этих классов – методы печати данных: Write, WriteIf, WriteLine, WriteLineIf. Методы перегружены, в простейшем случае позволяют выводить некоторое сообщение. Методы со словом If позволяют сделать печать условной, задавая условие печати в качестве первого аргумента метода, что иногда крайне полезно. Методы со словом Line позволяют дополнять сообщение символом перехода на новую строку.

По умолчанию методы обоих классов направляют вывод в окно Output. Однако это не всегда целесообразно, особенно для Release-конфигурации. Замечательным свойством методов классов Debug и Trace является то, что они могут иметь много «слушателей», направляя вывод каждому из них. Свойство Listeners этих классов возвращает разделяемую обоими классами коллекцию слушателей – TraceListenerCollection. Как и всякая коллекция она имеет ряд методов для добавления в коллекцию новых слушателей: Add, AddRange, Insert, возможность удаления слушателей из коллекции: Clear, Remove, RemoveAt и другие методы. Объекты этой коллекции в качестве предка имеют абстрактный класс TraceListener. Библиотека FCL включает три неабстрактных потомка этого класса:


  • DefaultTraceListener – слушатель этого класса, добавляемый в коллекцию по умолчанию, направляет вывод, поступающий при вызове методов классов Debug и Trace, в окно Output;

  • EventLogTraceListener – посылает сообщения в журнал событий Windows;

  • TextWriterTraceListener – направляет сообщения объектам класса TextWriter или Stream, обычно один из объектов этого класса направляет вывод на консоль, другой – в файл.

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

Помимо свойства Listeners и методов печати, классы Debug и Trace имеют и другие важные методы и свойства:

  • Assert и Fail, проверяющие корректность хода вычислений – о них мы поговорим особо.

  • Flush – метод, отправляющий содержание буфера слушателю (в файл, на консоль и так далее). Следует помнить, что данные буферизуются, поэтому применение метода Flush зачастую необходимо, иначе метод может завершиться, а данные останутся в буфере.

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

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

Отладка и инструментальная среда Visual Studio .Net

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

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

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



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

Обработка исключений в языках C/C++

Для стиля программирования на языке C характерно описание методов класса как булевых функций, возвращающих true в случае нормального завершения метода и false – при возникновении исключительной ситуации. Вызов метода встраивался в If-оператор, обрабатывающий ошибку в случае неуспеха завершения метода:

bool MyMethod(…){…}

if !MyMethod(){// обработка ошибки}

{//нормальное выполнение}

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

Поэтому в C/C++ применяется схема try/catch блоков, суть которой в следующем. Участок программы, в котором может возникнуть исключительная ситуация оформляется в виде охраняемого try-блока. Если при его выполнении возникает исключительная ситуация, то происходит прерывание выполнения try-блока c классификацией исключения. Это исключение начинает обрабатывать один из catch-блоков, соответствующий типу исключения. В C/C++ применяются две такие схемы. Одна из них – схема с возобновлением – соответствует так называемым структурным или С-исключениям. Вторая схема – без возобновления – соответствует С++ исключениям. В первой схеме обработчик исключения – catch-блок – возвращает управление в некоторую точку try-блока. Во второй схеме управление не возвращается в try-блок.

С некоторыми синтаксическими отличиями схема с возобновлением применяется в языках VB/VBA.

Схема обработки исключений в 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: Argument Exception, ArgumentOutOfRangeException, ArithmeticException , BadImageFormatException, 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 совместим по присваиванию c объектом e. Другими словами потенциальная способность захвата означает допустимость присваивания e = te, что возможно, когда класс TE является потомком класса T. Обработчик, класс T которого является классом Exception, является 
универсальным обработчиком, потенциально он способен захватить любое исключение, поскольку все они являются его потомками.

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

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

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

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

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