Учебник по программированию.

Первые шаги. Язык программирования PascalABC.

Предыдущий параграф Назад в содержание Следующий параграф


§31. Исключения.

Данная тема не была изучена ранее, хотя пользоваться ей можно было уже давно, по той причине,  что для полного её понимания необходимо знание ООП. Данная тема расписана в разделах справки: «Справочник по языку -> Обработка исключений», ««Справочник по языку -> Операторы -> Оператор raise», «Справочник по языку -> Операторы -> Оператор try … except» и «Справочник по языку -> Операторы -> Оператор try … finally».

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

Если исключительная ситуация возникает, то программа завершает свою работу. Для того, что бы программа продолжила свою работу после исключительной ситуации, и можно было перехватить исключение, существует конструкция try … except … end. После слова try пишется код, при выполнении которого может возникнуть ошибка, после слова except код, который будет выполнен в случае возникновения ошибки. Если ошибки не произойдёт, то код после слова except выполнен не будет. После слова end программа продолжить свою работу в любом случае. Как раз между словами except и end можно перехватить исключение и выполнить какие-либо действия. Исключение можно не перехватывать, так же можно не выполнять ни каких действий, в таком случае при появлении ошибки программа просто не закроется и продолжит работу. Пример:


var f: file;


begin

  Assign(f,'file.dat');

  try

    reset(f);

  except

    writeln('Файл не найден!')

  end

end.

_________________________________

Файл не найден!


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

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

Делается это следующим образом:


procedure ChteniyeFayla(ImyaFayla:string;var pp:pointer);

begin

  if not FileExists(ImyaFayla) then

    raise new Exception('Файл "'+ImyaFayla+'" не найден!');


  ...................//Читаем данные из фала


end;


В коде данного примера появилось два незнакомых слова: raise и exception. Слово raise оператор, который предназначен для «возбуждения» исключения. Если это слово не использовать, то исключение не будет «возбуждено», и при работе самой программы, в конструкции try … except … end, код после слова except выполнен не будет. Так же при использовании данного слова выход из подпрограммы происходит автоматически из того места, где оно находится. После слова raise необходимо создать объект-исключение с помощью операции new.

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


type

  Exception = class

  public

    constructor Create;

    constructor Create(message: string);

    property Message: string; // только на чтение

    property StackTrace: string; // только на чтение

  end;


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

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


var p:pointer

 

begin

  try

    ChteniyeFayla('Data.dat',p);

  except

    on Isklucheniye:Exception do writeln(Isklucheniye.Message);

  end;

end.

_______________________________________________________________

Файл "Data.dat" не найден!


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

Теперь попробуем вызвать нашу процедуру ChteniyeFayla не внутри конструкции try … except … end, а просто в теле программы. Если файла на диске не окажется, то вся программа завершит свою работу с ошибкой, в описании которой будет как раз наше сообщение:



В данном примере строчка writeln(Всё хорошо) так и не была выполнена. Так как программа завершила свою работу из-за того, что было возбуждено исключение.

Думаю, что такие ошибки вы видели уже не раз. Все они заранее продуманы и созданы с помощью исключений, оператора raise и конструкции try … except … end, точно так же как и в нашем примере с процедурой ChteniyeFayla.

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

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

Здесь стоит отметить, что все исключения должны быть производными от класса Exception.

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


type FaylOtsutstvuyet = class (exception);

     PamyatPolna = class (exception);


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


type TFaylOtsutstvuyetException = class (exception);

     TPamyatPolnaException = class (exception);

     TDrugoyeException = class (exception);


var p:pointer;


procedure ChteniyeFayla(ImyaFayla:string;var p:pointer);

begin

  if not FileExists(ImyaFayla) then

    raise new TFaylOtsutstvuyetException('Файл "'+ImyaFayla+'" не найден!');

// ...................Читаем данные из фала

  if {Память полна} true then

    raise new TPamyatPolnaException('Нехватка памяти.');

// ...................Читаем данные из фала

  raise new TDrugoyeException;

end;


begin

  try

    ChteniyeFayla('Data.dat',p);

  except

    on FaylOtsutstvuyetException:TFaylOtsutstvuyetException do

                            writeln(FaylOtsutstvuyetException.Message);

    on PamyatPolnaException:TPamyatPolnaException do

                            writeln(PamyatPolnaException.Message);

    else writeln('Какое-то другое исключение.');

  end;

end.


Как видите, конструкций, обработчиков исключений, может быть сколько угодно, в зависимости от того, сколько исключений необходимо обработать. Нужный обработчик выбирается автоматически в зависимости от произошедшего исключения. В конце раздела except после всех конструкций on … do … можно поставить слово else и код, который нужно выполнить в случае, если не нашлось ни одного обработчика для случившегося исключения.

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


Файл "Data.dat" не найден!


Затем создайте файл с именем «Data.dat» в том же месте жёсткого диска, где находится сама программа, и запустите программу. Результат будет следующим:


Нехватка памяти.


Затем замените в 12 строчке true на false:


Какое-то другое исключение.


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

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

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


В данном параграфе мы изучили тему исключения.


Задача.

В предыдущем параграфе необходимо было усовершенствовать класс TSpisok, создав на его основе производный TModernSpisok. В данной задаче необходимо во всех методах этого класса, работающих с номерами элементов, «возбудить» исключение в случае, если элемента с таким номером не существует. Переписать программу ModernSpisok таким образом, что бы она демонстрировала работу исключений.


Решение.

program ModernSpisokSIsklucheniyami;

type TElement = record

       Data:integer;

       p:pointer;

     end;


type TSpisok = class

       ..................

     end;


type TNomerMensheKolichestvaElem = class (Exception);


type TModernSpisok = class (TSpisok)

      public

       //Содержит текущий элемент

       property TekElem:integer write write read read;

       //Возвращает количество элементов в списке

       function KolElem:integer;

         begin

           var i:=0;

           Nachalo;

           while not EOS do

             begin

               pEl:=pEl^.p;

               Inc(i);

             end;

           KolElem:=i;

         end;

       //Возвращает значение элемента по номеру 

       function ElemPoNomeru(n:integer):integer;

         begin

           ProverkaNomera(n);

           Poziciya(n);

           ElemPoNomeru:=pEl^.Data;

         end;

       //Меняет значение элемента с номером n 

       procedure PerepisElPoNomeru(n,dat:integer);

         begin

           ProverkaNomera(n);

           Poziciya(n);

           pEl^.Data:=Dat;

         end;

       //Вставляет новый элемент на место старого с номером n

       //при этом весь список сдвигается

       procedure Vstavit(n,dat:integer);

         begin

           ProverkaNomera(n);

           Poziciya(n);

           new(PSled);

           pSled^.p:=pEl^.p;

           pSled^.Data:=pEl^.Data;

           pEl^.Data:=dat;

           pEl^.p:=pSled;

         end;

       //Удаляет из списка элемент с номером n 

       procedure Udalit(n:integer); 

         begin

           if (n>1) and (n<>KolElem) then

             begin//Если элемент где-то в середение

               Poziciya(n);

               pSled:=pEl^.p;

               dispose(pEl);

               Poziciya(n-1);

               pEl^.p:=pSled;

             end

           else

             if n=1 then

               begin//если удаляем первый элемент

                 Poziciya(1);

                 P1:=pEl^.p;

                 dispose(pEl);

               end

              else

               begin//если удаляем последний элемент

                 Poziciya(n);

                 pEl^.p:=nil;

                 Poziciya(n+1);

                 Dispose(pEl)

               end

         end;

       //Очищает весь список 

       procedure Ochistit;

         begin

           Nachalo;

           var pTemp:pointer;

           pTemp:=pEl^.p;

           pEl^.p:=nil;

           pEl:=pTemp;

           while pEl^.p<>nil do

             begin

               pTemp:=pEl^.p;

               dispose(pEl);

               pEl:=pTemp;

             end;

         end;

      protected

        procedure Poziciya(n:integer);//Переход на позицию n

          begin

            ProverkaNomera(n);

            Nachalo;

            var i:integer:=1;

            for i:=1 to n-1 do pEl:=pEl^.p;

          end;

     //Если n больше чем количество элементов, то возбуждается исключение  

        procedure ProverkaNomera(n:integer);

          begin

            if n>KolElem then

              raise new TNomerMensheKolichestvaElem('Элемента с номером '

                          +IntToStr(n)+' нe существует');

          end;

     end;


var Sp:TModernSpisok:=new TModernSpisok;


begin

  writeln('В списке ',Sp.KolElem,' элементов.');

  var max:=random(1,10);

  for var i:=1 to max do

    begin

      var temp:=random(1000);

      Sp.TekElem:=temp;

      writeln(i:2,'   temp = ',temp);

    end

//Выводим список на экран

  Sp.Nachalo;

  while not Sp.EOS do write(Sp.TekElem,'  ');

  writeln;

  writeln('Теперь в списке ',Sp.KolElem,' элементов.');

//Выводим элемент с номером temp

  var temp:=Random(1,max+3);

  try

    Writeln('Элемент с номером ',temp,' равен ',Sp.ElemPoNomeru(temp));

  except

    on Ex1:TNomerMensheKolichestvaElem do

          writeln('При доступе к элементу произошла следующая ошибка: '

                                                          ,Ex1.Message);

  end

//Меняем значение этого элемента

  try

    Sp.PerepisElPoNomeru(temp,Random(1,1000));

  except

    on Ex2:TNomerMensheKolichestvaElem do writeln('При замене элемента ',

              'произошла следующая ошибка:',Ex2.Message);

  end

//Снова выводим этот элемент

  try

    Writeln('После изменения элемент с номером ',temp,' равен '

                                                 ,Sp.ElemPoNomeru(temp));

  except

    on Ex3:TNomerMensheKolichestvaElem do

             writeln('При повторном выводе ',

              'элемента произошла следующая ошибка:',Ex3.Message);

  end

//Вставляем элемент

  try

    Sp.Vstavit(temp,Random(1,1000));

  except

    on Ex4:TNomerMensheKolichestvaElem do

              writeln('Во время вставки элемента',

                   ' произошла следующая ошибка: ',Ex4.Message);

  end

//Выводим новый список 

  writeln('После вставки нового элемента на место ',temp,

      ' список выглядит следующим образом:');

  Sp.Nachalo;

  while not Sp.EOS do write(Sp.TekElem,'  ');

  writeln;

//Удаляем элемент под номером random(1,max+3) и выводим новый список

  temp:=Random(1,max+3);

  try

    Sp.Udalit(temp); 

  except

    on Ex5:TNomerMensheKolichestvaElem do

            writeln('Во время удаления элемента',

                      ' произошла следующая ошибка: ',Ex5.Message);

  end

  writeln('После удаления элемента под номером ',temp,' список выглядит'+

                                                 ' следующим образом:');

  Sp.Nachalo;

  while not Sp.EOS do write(Sp.TekElem,'  ');

  writeln;

//Очищаем список и для проверки вызываем метод Sp.KolElem

  Sp.Ochistit;

  writeln('После очистки списка количество элементов в нём равно - ',

                              IntToStr(Sp.KolElem));

end.

_________________________________________________________________________

В списке 0 элементов.

1   temp = 551

2   temp = 429

3   temp = 804

4   temp = 565

5   temp = 564

6   temp = 184

7   temp = 235

551  429  804  565  564  184  235 

Теперь в списке 7 элементов.

При доступе к элементу произошла следующая ошибка: Элемента с номером 9 нe существует

При замене элемента произошла следующая ошибка: Элемента с номером 9 нe существует

При повторном выводе элемента произошла следующая ошибка: Элемента с номером 9 нe существует

Во время вставки элемента произошла следующая ошибка: Элемента с номером 9 нe существует

После вставки нового элемента на место 9 список выглядит следующим образом:

551  429  804  565  564  184  235 

Во время удаления элемента произошла следующая ошибка: Элемента с номером 10 нe существует

После удаления элемента под номером 10 список выглядит следующим образом:

551  429  804  565  564  184  235 

После очистки списка количество элементов в нём равно - 0


Примечание: класс TModernSpisok остался практически без изменения. Добавился только метод ProverkaNomera, в котором и возбуждается исключение. Этот метод вызывается всеми другими методами, которые обращаются к элементу с определённым номером.



Предыдущий параграф Назад в содержание Следующий параграф