Предотвращение и обработка ошибок. Исключительные ситуации

16.04.2019

Меня учили обходиться без оператора Goto. Использование оператора безусловного перехода считалось моветоном равным по грешности полному отсутствию комментариев в программе.

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

Первый вопрос, который я предвижу от слабо посвященных: "А зачем? "
Действительно, пишешь задачу, как "честная кнопка", пытаешься ничего не упустить, все предусмотреть, собственное творчество, как правило, себе нравится, и не вызывает сомнений правильность его алгоритма... И все же! Если задачка небольшая - все просто и прозрачно. Но, если алгоритм сложен... как говорят "глаз замыливается", перестает находить узкие места. К тому же накладывается человеческий фактор: откуда Вам было знать, что пользователь не введет значение в поле и нажмет "Enter"?.. В этот момент Ваша программа напугает его каким-нибудь англоязычным сообщением да еще и с пиктограммой "Error"... А, я с самого начала просил Вас уважать пользователя и заказчика. Стоит ли доводить ситуацию до общения с плохо вменяемым пиплом?! Если Вы не хотите общаться с человеком, у которого от испуга вытаращены глаза и трясутся руки, нужно научиться предугадывать (перехватывать) ошибки и выдавать на экран сообщения на родном для пользователя языке, желательно с номером Вашего телефона... Это и есть часть понятия "дружественный интерфейс".

Следующий вопрос, к которому я хочу приблизить тебя, дорогой читатель, это вопрос:

где и когда нужен обработчик ошибок?

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

Я же со своей стороны предлагаю почаще спрашивать себя "А вдруг?"
А вдруг сервер не сохранит запись?
А вдруг пользователь не введет допустимое значение и что-нибудь нажмет, закроет окно и т.п.
А вдруг такая запись уже есть в базе данных?
А вдруг...

Чем большим количеством подобных вопросов Вы себя озадачите, тем меньше вероятность того, что Ваша программа в руках незадачливого пользователя перейдет в состояние "Не отвечает".

Наиболее общая схема обработчика ошибок выглядит примерно так:

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

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

Добавьте в объявление переменных в процедуре SetLang() еще парочку:

ErrKod: integer;

ErrMsg: String;

а так же - объявление метки:

Label

ErrLabel;

и стартовые значения:


begin

ErrKod:=0; // Это значение при отсутствии ошибок

ErrMsg:= "Операция завершена успешно";

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

MyMessenger.TitleString:="Ошибка...";
MyMessenger.MessageType:=mtError;
MyMessenger.Buttons:=;

Затем - напишите проверку наличия файла языковой поддержки:

If Not FileExists(GetCurrentDir + "\"+Lang)
then
begin
ErrKod:=1;
ErrMsg:="При попытке открытия файла языковой настройки произошла ошибка";
end;

Почему именно здесь нужно предусмотреть какие-то действия? А вдруг в файле настроек указан несуществующий файл? Или его кто-то с диска того... нечаянно стер...

Следом должна идти проверка на наличие ошибок:

If ErrKod>0
then
Goto ErrLabel;


...

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

...


ErrLabel:



MyMessenger.ShowMessage;


end; // Процедуры


Давайте нашалим: переименуем файл Rus.lng, например в Rus1.lng, чтоб программа его уж точно не нашла.

Тогда при старте Rashod.exe будет произведена попытка настроить интерфейс приложения с помощью указанного в файле настроек Rashod.ini, в секции , в переменной LangFileName файла Rus.lng, которого в рабочем каталоге нет. Сработает обработчик ошибки, и на экране будет выведено придуманное нами сообщение:


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

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

не корректное по оформлению и неуместное по сути (если ошибок нет, нужно ли в данном случае вообще выводить какое-то сообщение?!).

Давайте поправим ситуацию:

ErrLabel:

Case ErrKod of

0: // Ошибок нет

Else

Begin

MyMessenger.MessageString:=ErrMsg;

MyMessenger.ShowMessage;

Задание 1: Создайте приложение, которое создает текстовый файл text1.txt и записывает в него текст, введенный пользователем в окно Edit, после чего закрывает файл.

Решение:

Создайте форму и задайте для ее свойства Caption значение «Создание файла и вывод в него текста». Разместите на форме компоненты Edit1, Labbel1, Button1, как показано на рис. 6.1. Задайте значения для свойства Label1. Caption – «Введите текст», Button1. Caption – «Сохранить». Выровняйте компоненты и зафиксируйте их положение на форме.

Сохраните файлы модуля под именем main и проекта под именем TextEditFile в папке Обработка текстовых файлов.

Рис. 6.1 Пример формы проекта

Создайте процедуру обработки события кнопки «Сохранить», введите в окне Редактора кода следующий текст:

f : TextFile ; {описание файловой переменной}

AssignFile ( f , " text 1. txt "); {связь файловой переменной с файлом}

Rewrite ( f ); {создать новый файл}

Writeln (f, Edit1. Text); {записать в файл}

CloseFile(f); end; {закрыть файл}

Запустите приложение и введите в окно Edit следующее предложение – «Мой первый пример текста». Щелкните мышкой на кнопке «Сохранить» и закройте окно приложения.

Откройте окно Проводника Windows папку Обработка текстовых файлов, в которой сохранены файлы проекта. В списке файлов этой папки находится вновь созданный файл text1.txt. Дважды щелкните левой кнопкой мыши на имени файла text1.txt. Убедитесь, что это – тот самый текст, который введен в окне приложения. Откроется окно редактора Блокнот с этим файлом. Закройте окно редактора Блокнот и Проводник.

Задание 2: Создайте приложение, открывающее текстовый файл для чтения и считывающие из него текст в окно Memo. Перед открытием файла следует проверить его наличие, в случае его отсутствия должно выводиться соответствующее сообщение.

Решение:

Создайте новое приложение (проект). Создайте форму «Чтение текста из файла в окно». На форме разместите компоненты Memo1, Label1, Button1, как показано на рис. 6.2. Задайте значения свойств Label1. Caption – «Текст из файла», Button1. Caption – «Прочитать текст из файла».

Для удаления текста Memo1 из компонента выберите в окне Инспектора объектов объект Memo1, затем на странице Свойства выберите свойсво Lines и в поле со значением Strings произведите двойной щелчок. После этого в окне StringListEditor удалите текст и щелкните мышью на кнопке ОК.

Для обеспечения возможности просмотра в окне Memo1 длинных текстов с использованием вертикальной полосы прокрутки в окне Инспектора объектов выберите свойство ScrollBars значение ssVertical. Выровняйте компоненты и зафиксируйте их положение на форме.


Рис. 6.2 Пример формы приложения

Создайте процедуру обработки события кнопки «Прочитать текст из файла», отредактируйте текст процедуры следующим образом:

procedure TForm1. Button1Click (Sender: TObject);

var f: TextFile;

ch: Char;

AssignFile (f, "text1.txt");

Reset(f);

if IOResult=0 then begin

while not Eof(f) do

Read (f, ch);

Memo1. Text:=Memo1. Text+ch;

CloseFile(f);

end else

ShowMessage (" Нет такого файла ");

Сохраните файл модуля под именем main1, а файл проекта – под именем TextMemoFile1 в папке Обработка текстовых файлов. Откомпилируйте и запустите приложение, проверьте его работу.

Задание 3 : Создайте приложение, открывающее текстовый файл для дополнения и затем добавляющее в него введенный текст.

Решение:

Создайте новый проект, задайте название формы «Добавление текста в файл». На форме разместите компонентыButton1,2, Memo1, Label1,2, Edit1 как показано на рис. 6.3. Присвойте значения свойствам Label1. Caption – «Текст из файла», Button1. Capton – «Прочитать текст из файла», Label2. Caption – «Добавляемый текст», Button2. Caption – «Добавить текст в файл». Удалите текст из компонентов Memo1, Edit1. Установите линейку вертикальной прокрутки для обеспечения возможности просмотра длинных текстов в компоненте Memo1. Выровняйте компоненты и зафиксируйте их положение на форме.



Рис. 6.3 Пример формы приложения

Создайте обработчик нажатия кнопки «Прочитать текст из файла» самостоятельно. Для кнопки «Добавить текст в файл» запишите следующий код события:

Procedure TForm1. Button2Click (Sender: TObject);

F: TextFile;

AssingFile (f, ‘text1.txt’);

Append (f);

Writeln (f, Edit1. Text);

CloseFile (f);

End ;

Сохраните файл модуля под именем Main2, а файл проекта – под именем TextMemoFile2 в папке Обработка текстовых файлов.

Запустите и проверьте работу приложения.

Задание 4: Создайте приложение, которое открывает текстовый файл с использованием метода OpenDialog, считывает текст из него в объект Memo, затем сохраняет измененный текст в файл с использованием метода SaveDialog и выводит текст на печать, используя метод PrintDialog.

Решение:

Создайте новый проект и сохраните в папке «Диалоговая панель». На форме разместите компоненты Memo1, Button1,2,3. Кнопки назовите «Сохранить», «Открыть», «Печать» соответственно. Задайте компоненту Memo1 вертикальную полосу прокрутки и удалите текст. Выровняйте и зафиксируйте компоненты на форме (см. рис. 6.4).


Рис. 6.4 Форма проекта «Диалоговая панель»

Выберите в палитре компонентов страницу Dialog и поместите на форму компоненты OpenDialog, SaveDialog, PrintDialog. Так как они не являются визуальными компонентами, то их можно поместить в любое место формы.

Задайте для свойства SaveDialog. Title значение «Сохранить текстовый файл», которое будет отображаться в заголовке диалогового окна сохранения файла. Чтобы при сохранении файла в окне диалога обеспечить выбор типа файла, выберите свойство Filter и произведите двойной щелчок в списке значений. Откроется окно FilterEditor. Задайте фильтры для выбора типа и расширения файла:

И щелкните по кнопке ОК, затем установите расширение *.txt по умолчанию – задайте свойству OpenDialog1. FilterIndex значение 1.

Чтобы в диалоговом окне Печать включить возможность выбора диапазона печатаемых страниц и печати выделенного фрагмента, задайте для свойств PrintDialog1. Options.poPageNums и PrintDialog1. Options.poSelection значение True.

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

Отредактируйте раздел описания переменных:

Form1: TForm1;

FName: string;

F: TextFile;

S : string ;

Создайте процедуры обработки событий трех кнопок, следующим образом:

1) сохранение:

procedure TForm1. Button1Click (Sender: TObject);

fName:="Text1";

SaveDialog1. FileName:=FName;

if SaveDialog1. Execute then begin

fName:=SaveDialog1. FileName;

case SaveDialog1. FilterIndex of

1: fName:=fName+".txt";

2: fName:=fName+".doc";

Memo1. Lines. SaveToFile(fName);

2) открытие:

procedure TForm1. Button2Click (Sender: TObject);

if OpenDialog1. Execute then

AssignFile (f, OpenDialog1. FileName);

fName:=OpenDialog1. FileName;

Reset(F);

Readln (F, s);

Memo1. Text:=s;

CloseFile(F);

3) печать:

procedure TForm1. Button3Click (Sender: TObject);

if PrintDialog1. Execute then

AssignPrn(f);

Rewrite(F);

Writeln (f, Memo1. Text);

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

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

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

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

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

try
основные операторы фрагмента;
except
альтернативный вариант фрагмента;
end;

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

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

try
операторы
finally
заключительные действия
end;
Основные операторы, находящиеся в секции try , могут пройти штатно, или вызвать исключительную ситуацию. Операторы заключительных действий, находящиеся в секции finally , будут выполнены в любом случае.

Есть ещё один способ контроля исключительных ситуаций , касающийся ошибок операций ввода-вывода.
Перед участком программы, где возможны ошибки ввода-вывода (а это, по сути, все операторы ввода-вывода), ставится директива {$I-} , заставляющая компилятор не включать в код автоконтроль ошибок ввода-вывода. Таким образом, в случае ошибки ввода или вывода программа не прерывается. В конце участка с операторами ввода-вывода ставится директива, включающая автоконтроль: {$I+} . Затем анализируется результат вызова функции IOResult . Если функция IOResult (вызывается без параметров) возвращает 0 , значит ошибок ввода-вывода на данном участке не было.

Вот какой пример использования директив {$I} и функции IOResult содержит справка системы Delphi:

var
F: file of Byte;
begin
if OpenDialog1.Execute then
begin
AssignFile(F, OpenDialog1.FileName);
{$I-}
Reset(F);
{$I+}
if IOResult = 0 then
begin
MessageDlg("File size in bytes: " + IntToStr(FileSize(F)), mtInformation, , 0);
CloseFile(F);
end
else
MessageDlg("File access error", mtWarning, , 0);
end ;
end ;

Функция IOResult досталась Delphi в наследство от Turbo Pascal . Тот же самый фрагмент можно составить и с использованием оператора try . На мой взгляд, это удобнее и проще.

При работе программы под управлением Delphi, система будет сама реагировать на исключительные ситуации, мешая работе операторов обработки исключений. Чтобы проверить их действие, можно запускать программу непосредственно, сворачивая Delphi и пользуясь ярлыком, установленном на Рабочем столе. Или можно отключить реакцию системы на исключительные ситуации, тем самым давая возможность отработать специально для этого написанным фрагментам программы - нашим операторам try/except/end. Для этого откроем пункт системного меню Delphi Tools -> Debugger Options... . В появившемся окошке нужно снять галку в чекбоксе Stop on Delphi Exceptions , расположенном на вкладке Language Exceptions . Теперь система Delphi будет предоставлять вашей программе возможность самостоятельно обрабатывать исключительные ситуации, среди которых могут быть и ситуации, возникновение которых прописано специально как удобный инструмент достижения необходимых результатов.

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

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

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

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

try except {Операторы, которые должны быть выполнены в случае ошибки} end ;

Конструкция try...except применяется для перехвата исключительной ситуации и позволяет восстановить работоспособность программы. Секция except может быть разбита на несколько частей on...do для обработки разных классов исключений. После конструкций on...do может быть помещён раздел else, который относится ко всему блоку. По логике работы группа конструкций on...do напоминает оператор case. К исключениям, не имеющим своих локальных обработчиков, применяется механизм глобальной обработки через объект Application.

try {Операторы, выполнение которых может вызвать ошибку} except {ераторы, которые должны быть выполнены в случае ошибки} Оп on Exception1 do ...; on Exception2 do ...; ... else .end; ..

Рассмотрим следующий пример. В поля Edit1 и Edit2 записываются целые числа. При щелчке по кнопке Button1 выполняется перевод введённых строк в числовой формат, первое число делится на второе и результат выводится в Edit3. Затем в Memo1 записываются исходные строки, сумма чисел и частное от деления первого числа на второе.

Внимание! При тестировании приложений желательно пользоваться созданным exe-файлом. Запускать приложения из Delphi можно, но при этом надо учитывать, что при возникновании исключительной ситуации прежде всего сработает система защиты Delphi. При появлении системного сообщения его надо прочитать, окно сообщения закрыть и выполнить команду Run для продолжения работы.

procedure TForm1.Button1Click(Sender: TObject); Var a,b:integer; rez:extended; begin a:=strtoint(Edit1.Text); b:=strtoint(Edit2.Text); rez:=a/b; Edit3.Text:=floattostr(rez); Memo1.Lines.Add(Edit1.Text); Memo1.Lines.Add(Edit2.Text); Memo1.Lines.Add(inttostr(a+b)); Mend; emo1.Lines.Add(floattostr(rez));

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

Введём локальную обработку исключительных ситуаций. Для этого сформируем защищённый блок. Анализировать ошибки не будем.

procedure TForm1.Button1Click(Sender: TObject); Var a,b:integer; rbegin ez:extended; try a:=strtoint(Edit1.Text); b:=strtoint(Edit2.Text); rez:=a/b; Edit3.Text:=floattostr(rez); except ShowMessage("Ошибка!" ); end ; Memo1.Lines.Add(Edit1.Text); Memo1.Lines.Add(Edit2.Text); Memo1.Lines.Add(inttostr(a+b)); Memo1.Lines.Add(floattostr(rez)); end ;

В этом случае при возникновении любого исключения будет прерываться выполнение операторов защищённого блока, в Edit3 результат не появится. На экран будет выведено окно с сообщением «Ошибка!». Операторы, расположенные после защищённого блока, будут выполняться, то есть в Memo1 появятся записи.

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

"Попытка деления на ноль!" ); Edit2.SetFocus; end ;

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

Добавим локальный обработчик для контроля за преобразованием вводимых данных. При этом глобальная обработка исключений будет задействована только для нулевых введённых значений (0/0).

except on EZeroDivide do begin ShowMessage("Попытка деления на ноль!" "Ошибка преобразования!" );

Если ввести секцию else, то все исключения будут обработаны локально.

except on EZeroDivide do begin ShowMessage("Попытка деления на ноль!" ); Edit2.SetFocus; end ; on EConvertError do ShowMessage("Ошибка преобразования!" ) else ShowMessage("Ошибка в защищённом блоке!" );

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

try ...{Операторы, выполнение которых может вызвать ошибку} finally {Операторы, которые должны быть выполнены даже в случае ошибки} end ;

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

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

  • EConvertError – ошибка преобразования типов, может возникнуть при выполнении функций StrToInt и StrToFloat.
  • EInOutError – ошибка ввода/вывода при включенной директиве {$I+}.
  • EDivByZero – деление целого на ноль.
  • EIntOverflow – переполнение в операции с целыми переменными.
  • ERangeError – присвоение значения, выходящего за пределы допустимого диапазона. Например, при попытке обращения к элементам массива по индексу, выходящему за пределы массива.
  • EInvalidGraphic – попытка загрузки методом LoadFromFile файла, несовместимого графического формата.
  • EInvalidPointer – некорректная операция с указателем.
  • EFCreateError – ошибка создания файла
  • EFOpenError – ошибка открытия файла
  • EListError, EStringListError – ошибка при работе со списками.
  • EMathError – предок исключений, возникающих при выполнении операций с плавающей точкой.
  • EInvalidOp – попытка передачи математическому сопроцессору ошибочной инструкции.
  • EOverflow –переполнение при слишком больших величинах.
  • EUnderflow – потеря значимости при операции с плавающей точкой (слишком малая величина). Результат получает нулевое значение.
  • EZeroDivide – попытка деления на ноль.
  • EMenuError –ошибка при работе с пунктами меню для компонент TMenu, TMenuItem, TPopupMenu и их наследников.
  • EOutOfMemory – вызов методов New, GetMem или конструкторов классов при невозможности распределения памяти.
  • EOutOfResources – ошибка при выполнении запроса на выделение или заполнение Windows-ресурсов (например, обработчика handles).

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