Файл: Отладка приложений. Организация обработки исключений. Корректность и устойчивость программных систем Корректность и устойчивость.docx
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 49
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Блок finally
До сих пор ничего не было сказано о важном участнике схемы обработки исключений – блоке finally. Напомню, рассматриваемая схема является схемой без возобновления. Это означает, что управление вычислением неожиданно покидает try-блок. Просто так этого делать нельзя – нужно выполнить определенную чистку. Прежде всего удаляются все локальные объекты, созданные в процессе работы блока. В языке С++ эта работа требовала вызова деструкторов объектов. В C# благодаря автоматической сборке мусора освобождением памяти можно не заниматься, достаточно освободить стек. Но в блоке try могли быть заняты другие ресурсы – открыты файлы, захвачены некоторые устройства. Освобождение ресурсов, занятых try-блоком, выполняет finally-блок. Если он присутствует, он выполняется всегда, сразу же после завершения работы try-блока, как бы последний не завершался. Блок try может завершиться вполне нормально без всяких происшествий и управление достигнет конца блока, выполнение может прервано оператором throw, управление может передано другому блоку из-за выполнения таких операторов как goto, return – во всех этих случаях прежде чем управление будет передано по предписанному назначению ( в том числе прежде чем произойдет захват исключения) предварительно будет выполнен finally-блок, освобождающий ресурсы, занятые try-блоком, параллельно будет происходить освобождение стека от локальных переменных.
Схема Бертрана обработки исключительных ситуаций
Схема обработки исключительных ситуаций, предложенная в языке C# обладает одним существенным изъяном – ее можно применять некорректно. Она позволяет в случае возникновения исключительной ситуации уведомить о ее возникновении и спокойно продолжить работу, что в конечном счете приведет к неверным результатам. Из двух зол – прервать вычисление с уведомлением о невозможности продолжения работы, или закончить вычисления с ошибочным результатом вычисления – следует выбирать первое. Некорректно примененная схема C# приведет к ошибочным результатам. Приведу несколько примеров. Представьте, оформляется заказ на отдых где-нибудь на Канарах. В ходе оформления возникает исключительная ситуация – нет свободных мест в гостинице – обработчик исключения посылает уведомление с принесением извинений, но оформление заказа продолжается. Вероятнее, предпочтительнее отказаться от отдыха на Канарах, и выбрать другое место, чем оказаться без крыши над головой, ночуя на берегу океана. Эта ситуация не является критически важной. А что если в процессе подготовки операции выясняется, что проведение ее в данном случае опасно. Никакие извинения не могут избавить от вреда, нанесенного операцией. Операция должна быть отменена.
Бертран Мейер в книге [1], в которой все механизмы, используемые в объектной технологии, тщательно обосновываются, предложил следующую схему обработки исключительных ситуаций. В основе ее лежит подход к проектированию программной системы на принципах Проектирования по Контракту. Модули программной системы, вызывающие друг друга заключают между собой контракты. Вызывающий модуль обязан обеспечить истинность предусловия, необходимого для корректной работы вызванного модуля. Вызванный модуль обязан гарантировать истинность постусловия по завершению своей работы. Если в вызванном модуле возникает исключительная ситуация, то это означает, что он не может выполнить свою часть контракта. Что должен делать обработчик исключительной ситуации? – у него только две возможности – Retry и Rescue. Первая (Retry)– попытаться внести некоторые коррективы – и вернуть управление охраняемому модулю, который может предпринять очередную попытку выполнить свой контракт. Модуль может, например в следующей попытке запустить другой алгоритм, использовать другой файл, другие данные. Если все закончится успешно, работа модуля соответствует его постусловию, то появление исключительной ситуации можно рассматривать как временные трудности, успешно преодоленные. Если же ситуация возникает вновь и вновь, тогда обработчик события применяет вторую стратегию (Rescue), выбрасывая исключение и передавая управление вызывающему модулю, который и должен теперь попытаться исправить ситуацию. Важная тонкость в схеме, предложенной Бертраном, состоит в том, что исключение, выбрасываемое обработчиком исключения, следует рассматривать не как панику, не как бегство, а как отход на заранее подготовленные позиции. Обработчик исключения должен позаботиться о восстановлении состояния, предшествующего вызову модуля, приведшего к исключительной ситуации, что гарантирует нахождение всей системы в корректном состоянии.
Схема Бертрана является схемой с возобновлением, и она наиболее точно описывает разумную стратегию обработки исключительных ситуаций. Не следует думать, что эта схема не может быть реализована на C#, просто она требует понимания сути и определенной структурной организации модуля. Приведу возможную реализацию такой схемы на C#:
public void Pattern()
{
do
{
try
{
bool Danger = false;
Success = true;
MakeJob();
Danger = CheckDanger();
if (Danger)
throw (new MyException());
MakeLastJob();
}
catch (MyException me)
{
if(count > maxcount)
throw(new MyException("Три попытки были безуспешны"));
Success = false; count++;
//корректировка ситуации
Console.WriteLine("Попытка исправить ситуацию!");
level +=1;
}
}while (!Success);
}
Приведу краткие комментарии к этой процедуре, которую можно рассматривать как некоторый образец организации обработки исключительной ситуации:
-
Конструкция try-catch блоков помещается в цикл do-while(!Success), завершаемый в случае успешной работы охраняемого блока, за чем следит булева переменная Success. -
В данном образце предполагается, что в теле охраняемого блока анализируется возможность возникновения исключительной ситуации и в случае обнаружения опасности выбрасывается собственное исключение, класс которого задан программно. В соответствии с этим тело try-блока содержит вызов метода MakeJob, выполняющего некоторую часть работы, после чего вызывается метод CheckDanger, выясняющий, не возникла ли опасность нарушения спецификации и может ли работа быть продолжена. Если все нормально, то выполняется метод MakeLastJob, выполняющий заключительную часть работы. Управление вычислением достигает конца try-блока, он успешно завершается и, поскольку остается истинной переменная Success, значение true которой установлено в начале try-блока, то цикл while, окаймляющий охраняемый блок и его обработчиков исключений, успешно завершается. -
Если в методе CheckDanger выясняется, что нормальное продолжение вычислений невозможно, то выбрасывается исключение класса MyException. Это исключение перехватывает обработчик исключения, стоящий за try-блоком, поскольку класс MyException указан, как класс формального аргумента. -
Для простоты приведен только один catch-блок. В общем случае их может быть несколько, но все они строятся по единому образцу. Предполагается, что обработчик исключения может сделать несколько попыток исправить ситуацию, после чего повторно выполняется охраняемый блок. Если же число попыток, за которым следит переменная count, превосходит максимально допустимое, то обработчик исключения выбрасывает новое исключение, задавая дополнительную информацию, передавая тем самым обработку ошибки на следующий уровень – вызываемой программе. -
Когда число попыток еще не исчерпано, то обработчик исключения переменной Success дает значение false, гарантирующее повтор выполнения try-блока, увеличивает счетчик числа попыток и пытается исправить ситуацию. -
Как видите, эта схема реализует два корректных исхода обработки исключительной ситуации – Retry и Rescue – повтору с надеждой выполнить обязательства, и передачи управления вызывающей программе, чтобы она предприняла попытки исправления ситуации, когда вызванная программа не могла с этим справиться.
Доведем этот образец до реально работающего кода, где угроза исключения зависит от значения генерируемого случайного числа, а обработчик исключения может изменять границы интервала, повышая вероятность успеха.
Определим первым делом собственный класс исключений:
public class MyException :Exception
{
public MyException()
{}
public MyException (string message) : base(message)
{}
public MyException (string message, Exception e) : base(message, e)
{}
}
Минимально, что нужно сделать, определяя свои исключения, – это задать три конструктора класса, вызывающие соответствующие конструкторы базового класса Exception.
В классе Excepts, методом которого является наш образец Pattern, определим следующие поля класса:
Random rnd = new Random();
int level = -10;
bool Success; //true - нормальное завершение
int count =1; // число попыток выполнения
const int maxcount =3;
Определим теперь методы, вызываемые в теле охраняемого блока:
void MakeJob()
{
Console.WriteLine("Подготовительные работы завершены");
}
bool CheckDanger()
{
//проверка качества и возможности продолжения работ
int low = rnd.Next(level,10);
if ( low > 6) return(false);
return(true);
}
void MakeLastJob()
{
Console.WriteLine("Все работы завершены успешно");
}
В классе Testing зададим метод, вызывающий метод Pattern:
public void TestPattern()
{
Excepts ex1 = new Excepts();
try
{
ex1.Pattern();
}
catch (Exception e)
{
Console.WriteLine("исключительная ситуация при вызове Pattern");
Console.WriteLine(e.ToString());
}
}
Обратите внимание, вызов метода Pattern находится внутри охраняемого блока. Поэтому, когда Pattern не справится с обработкой исключительной ситуации, ее обработку возьмет на себя универсальный обработчик, стоящий за try-блоком.
Показаны три варианта запуска метода TestPattern. В одном из них исключительной ситуации при вызове метода Pattern вообще не возникало, в другом – ситуация возникала, но коррекция обработчика исключения помогла и при повторе выполнения охраняемого блока в Pattern все прошло нормально. В третьем варианте метод Pattern не смог справиться с исключительной ситуацией, и она обрабатывалась в catch-блоке метода TestPattern.
Класс Exception
Рассмотрим устройство базового класса Exception, что поможет понять, какую информацию может получить обработчик исключения, когда ему передается объект, задающий текущее исключение.
Основными свойствами класса являются:
-
Message – строка, задающая причину возникновения исключения. Значение этого свойства устанавливается при вызове конструктора класса, когда создается объект, задающий исключение. -
HelpLink – ссылка (URL) на файл, содержащий подробную справку о возможной причине возникновения исключительной ситуации и способах ее устранения. -
InnerException – ссылка на внутреннее исключение. Когда обработчик исключение выбрасывает новое исключение для передачи обработки на следующий уровень, то текущее исключение становится внутренним для вновь создаваемого исключения. -
Source – имя приложения, ставшего причиной исключения. -
StackTrace – цепочка вызовов – методы, хранящиеся в стеке вызовов в момент возникновения исключения. -
TargetSite – метод, выбросивший исключение.
Из методов класса отметим метод GetBaseException, – при подъеме по цепочке вызовов позволяет получить исходное исключение – первопричину возникновения последовательности выбрасываемых исключений.
Класс имеет четыре конструктора, из которых три уже упоминалось. Один из них – конструктор без аргументов, второй – принимает строку, становящуюся свойством Message, третий – имеет еще один аргумент – исключение, передаваемое свойству InnerException.
В предыдущий пример внесем некоторые изменения. В частности, добавим еще один аргумент при вызове конструктора исключения в catch-блоке метода Pattern:
throw(new MyException("Все попытки Pattern безуспешны", me));
В этом случае у создаваемого исключения заполняется свойство InnerExceptions. Для слежения за свойствами исключений добавил метод печати всех свойств, вызываемый во всех обработчиках исключений:
static public void PrintProperties(Exception e)
{
Console.WriteLine("Свойства исключения:");
Console.WriteLine("TargetSite = {0}", e.TargetSite);
Console.WriteLine("Source = {0}", e.Source);
Console.WriteLine("Message = {0}",e.Message);
if (nerException == null)
Console.WriteLine("InnerException = null");
else Console.WriteLine("InnerException = {0}", nerException.Message);
Console.WriteLine("StackTrace = {0}", e.StackTrace);
Console.WriteLine("GetBaseException = {0}", e.GetBaseException());
}
Корректное применение механизма исключений должно поддерживаться целенаправленными усилиями программиста. Следует помнить о двух важных правилах:
-
обработка исключений должна быть направлена не столько на уведомление о возникновении ошибки, сколько на корректировку возникшей ситуации; -
если исправить ситуацию не удается, то программа должна быть прервана, не приводя к получению некорректных результатов, не удовлетворяющих спецификациям программы.