Copyright (C) А.Гавва V-0.4w май 2004

10. Исключения

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

- ошибки, которые обнаруживаются на этапе компиляции программы
- ошибки, которые обнаруживаются во время выполнения программы

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

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

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

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

10.1 Предопределенные исключения

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

Constraint_Error  -  Ошибка ограничения
Numeric_Error  -  Ошибка числа
Program_Error  -  Ошибка программы
Storage_Error  -  Ошибка памяти
Tasking_Error  -  Ошибка задачи

10.1.1 Исключение Constraint_Error

Исключение Constraint_Error возбуждается в следующих случаях:

Рассмотрим пример:


procedure Constraint_Demo is

    X : Integer range 1..20;
    Y : Integer;

begin
    Put("enter a number ");
    Get(Y);
    X := Y;
    Put("thank you");
end Constraint_Demo;

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


    Put("thank you");

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

Рассмотрим пример в котором выполняется нарушение ограничения диапазона индексных значений для массива:


procedure Constraint_Demo2 is

    X : array (1..5) of Integer := (1, 2, 3, 4, 5);
    Y : Integer := 6;

begin
    X(Y) := 37;
end Constraint_Demo2;

Здесь, исключение Constraint_Error будет генерироваться когда мы будем пытаться обратиться к несуществующему индексу массива.

10.1.2 Исключение Numeric_Error

Исключение Numeric_Error возбуждается в случае когда предопределенная численная операция не может предоставить математически корректный результат Это может произойти при арифметическом переполнении, делении на нуль, а также не возможности обеспечить требуемую точность при выполнении операций с плавающей точкой. Следует заметить, что в Ada95 Numeric_Error переопределена таким образом, что является тем же самым, что и Constraint_Error.


procedure Numeric_Demo is

    X : Integer;
    Y : Integer;

begin
    X := Integer'Last;
    Y := X + X;             -- вызывает Numeric_Error
end Numeric_Demo;

10.1.3 Исключение Program_Error

Исключение Program_Error возбуждается в следующих случаях:

Кроме того, это исключение может возбуждаться в случае возникновения ошибки элаборации.


procedure Program_Demo is

    Z : Integer;

    function Y(X : Integer) return Integer is
    begin
        if X < 10 then
            return X;
        elsif  X < 20 then
            return X
        end if;
    end Y;                -- если мы попали в эту точку, то это значит,
                          -- что return не был выполнен

begin
    Z := Y(30);
end Program_Demo;

10.1.4 Исключение Storage_Error

Исключение Storage_Error возбуждается в следующих случаях:

10.1.5 Исключение Tasking_Error

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

10.2 Исключения определяемые пользователем

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

10.2.1 Описание исключения пользователя

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


My_Very_Own_Exception : exception;
Another_Exception     : exception;

10.2.2 Возбуждение исключений

Указание возбуждения исключения достаточно простое. Для этого используется инструкция raise. Например:


raise Numeric_Error;

Сгенерированное таким образом исключение не будет ничем отличаться от "истинного" исключения Numeric_Error.

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

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

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

Обработчик исключения может размещаться в конце тела подпрограммы, пакета или настраиваемого модуля, в конце тела задачи или входа, а также в конце инструкции блока или инструкции принятия (accept). Заметим, что обработчик исключения не является обязательной частью этих конструкций.

Рассмотрим следующий пример:


declare

    X : Integer range 1..20;

begin
    Put("please enter a number ");
    Get(X);
    Put("thank you");

exception
    when Constraint_Error =>
        Put("that number should be between 1 and 20");
    when others =>
        Put("some other error occurred");
end;

Здесь описаны два обработчика. В одном выполняется обработка только исключений ограничения (Constraint_Error). Второй обработчик выполняет обработку всех остальных исключений (others). Таким образом, если пользователь вводит число в диапазоне от 1 до 20, то ошибки не происходит и появляется сообщение "thank you". В противном случае, перед завершением выполнения появляется сообщение обработчика исключения Constraint_Error: "that number should be between 1 and 20". В случае возникновения какого-либо другого исключения появится сообщение от второго обработчика: "some other error occurred".

Можно описать обработчик исключений так, чтобы он обрабатывал несколько указанных исключений. Для выполнения этого, исключения должны разделяться символом '|':


    . . .
exception
    . . .
    when Constraint_Error | Storage_Error =>
        . . .

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

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


loop
    declare
        . . .

    begin
        . . .

        Get(X);
        exit;

    exception
        when Constraint_Error =>
          Put("that number ...
    end;

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

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

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

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

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


procedure Exception_Demo is

    ---------------------------------
    procedure Level_2 is
        -- здесь нет обработчика исключений
    begin
        raise Constraint_Error;
    end Level_2;

    ---------------------------------
    procedure Level_1 is
    begin
        Level_2;
    exception
        when Constraint_Error =>
            Put("exception caught in Level_1");
    end Level_1;

begin
    Level_1;

exception 
    when Constraint_Error =>
        Put("exception caught in Exception_Demo");
end Exception_Demo;

После запуска этой программы будет выдано только сообщение "exception caught in Level_1". Следовательно, обработанное исключение не распространяется дальше.

Модифицируем процедуру Level_1 поместив инструкцию raise в ее обработчик исключения. Наш предыдущий пример будет иметь следующий вид:


procedure Exception_Demo is

    ---------------------------------
    procedure Level_2 is
        -- здесь нет обработчика исключений
    begin
        raise Constraint_Error;
    end Level_2;

    ---------------------------------
    procedure Level_1 is
    begin
        Level_2;
    exception
        when Constraint_Error =>
            Put("exception caught in Level_1");
            raise;        -- регенерация текущего исключения;
                          -- дает возможность другим подпрограммам
                          -- произвести обработку возникшего
                          -- исключения
    end Level_1;

begin
    Level_1;

exception 
    when Constraint_Error =>
        Put("exception caught in Exception_Demo");
end Exception_Demo;

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

Инструкцию raise очень удобно использовать в секции others обработчика исключений:


    . . .

exception

    . . .

    when others =>
        raise;        -- регенерация текущего исключения;
                      -- дает возможность другим подпрограммам
                      -- произвести обработку возникшего
                      -- исключения
end;

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

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

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


with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is
                
    procedure Problem_In_Scope is
        Cant_Be_Seen : exception;
    begin
        raise Cant_Be_Seen;
    end Problem_In_Scope;

begin
    Problem_In_Scope;

exception 
    when Cant_Be_Seen =>
        Put("just handled an_exception");
end Demo;

Этот пример не корректен. Проблема в том, что область видимости исключения Cant_Be_Seen ограничивается процедурой Problem_In_Scope, которая, собственно и является источником этого исключения. То есть, исключение Cant_Be_Seen не видимо и о нем ничего не известно за пределами процедуры Problem_In_Scope. Поэтому, это исключение не может быть точно обработано процедурой Demo.

Решить эту проблему можно использованием опции others в обработчике исключений внешней процедуры Demo:


with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is
                
    procedure Problem_In_Scope is
            Cant_Be_Seen : exception;
    begin
            raise Cant_Be_Seen;
    end Problem_In_Scope;

begin
    Problem_In_Scope;

exception
    when others =>
        Put("just handled some exception");
end Demo;

Другая проблема возникает тогда, когда в соответствии с правилами области видимости, исключение, описываемое в одной процедуре, перекрывает (или прячет) исключение, описываемое в другой процедуре:


with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is

    Fred : exception;            -- глобальное исключение

    ------------------------------------
    procedure P1 is
    begin
        raise Fred;
    end P1;

    ------------------------------------
    procedure P2 is

        Fred : exception;        -- локальное исключение

    begin
        P1;

    exception
        when Fred =>
            Put("wow, a Fred exception");
    end P2;
    ------------------------------------

begin
    P2;

exception
    when Fred =>
        Put("just handled a Fred exception");
end Demo;

Выводом такой процедуры будет "just handled a Fred exception". Исключение, обрабатываемое в процедуре P2, будет локально описанным исключением. Такое поведение подобно ситуации с областью видимости обычных переменных.

Для решения этой проблемы, процедуру P2 можно переписать следующим образом:


------------------------------------
procedure P2 is

    Fred : exception;

begin
    P1;

exception
    when Fred =>
        -- локальное исключение
        Put("wow, an_exception");

    when Demo.Fred =>
        -- "более глобальное" исключение
        Put("handeled Demo.Fred exception");
        raise;
end P2;

Теперь, обработчик исключения процедуры P2 выдаст сообщение "handeled Demo.Fred exception" и, с помощью инструкции raise, осуществит передачу исключения Demo.Fred в обработчик исключения процедуры Demo, который, в свою очередь, выдаст сообщение "just handled a Fred exception".

10.3.4 Пакет Ada.Exceptions

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

Описанный в нем объект:


Event : Exception_Occurence;

и подпрограммы:

функция Exception_Name(Event)  -  возвращает строку имени исключения, начиная от корневого библиотечного модуля
   
функция Exception_Information(Event)  -  возвращает строку детальной информации о возникшем исключении
  
функция Exception_Message(Event)  -  возвращает строку краткого объяснения исключения
  
процедура Reraise_Occurence(Event)  -  выполняет повторное возбуждение исключения Event
  
процедура Reraise_Exception(e, "Msg")  -  выполняет возбуждение исключения e с сообщением "Msg"

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


    . . .

exception

    . . .

    when The_Event: others =>
        Put("Unexpected exeption is ";
        Put(Exeption_Name(The_Event));
        New_Line;

10.4 Подавление исключений

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

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

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

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

Директива Supress имеет большое количество опций, позволяющих подавлять проверки различного вида на уровне типа, объекта или на функциональном уровне. Следует заметить, что многогранность директивы Supress сильно зависит от реализации конкретного компилятора, и различные реализации компиляторов свободны в предоставлении (или игнорировании) любых свойств этой директивы.

Исключение Constraint_Error имеет несколько подавляемых проверок:


pragma Suppress (Access_Check);
pragma Suppress (Discriminant_Check);
pragma Suppress (Idex_Check);
pragma Suppress (Length_Check);
pragma Suppress (Range_Check);
pragma Suppress (Division_Check);
pragma Suppress (Owerflow_Check);

Исключение Program_Error имеет только одну подавляемую проверку:


pragma Suppress (Elaboration_Check);

Исключение Storage_Error также имеет только одну подавляемую проверку:


pragma Suppress (Storage_Check);

10.4.2 Выполнение подавления исключений

Мы можем подавить проверку исключений для индивидуального объекта:


pragma Suppress (Idex_Check, on => table);

Подавление проверки исключений также может относиться к какому-то определенному типу:


type Employee_Id is new Integer;
pragma Suppress (Range_Check, Employee_Id);

Более полным примером использования директивы Supress может служить код показанный ниже. В этом случае область действия директивы распространяется до конца блока.


declare

    pragma Suppress(Range_Check);
    subtype Small_Integer is Integer range 1..10;

    A : Small_Integer;
    X : Integer := 50;

begin
    A := X;
end;

Этот код не будет генерировать ошибок ограничения (Constraint_Error).


Copyright (C) А.Гавва V-0.4w май 2004