Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования. Концепции и принципы

.pdf
Скачиваний:
2
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
Исключения
Обратите внимание, объявляются исключения статически, подобно перемен
ным и процедурам, а реакции на них выбираются динамически из статически оп ределенного множества возможных реакций.
Если разрешить вводить новые имена исключений динамически, то следовало бы
создавать динамически и реакции на них, то есть динамически создавать програм
му. Такого рода возможности противоречат концепции статического контроля и
в современных языках индустриального программирования практически не встре
чаются.
Вообще, в современных ЯП поведение исполнителя в режиме обработки исключе
ний довольно жестко регламентировано. Нет прямых аналогов, например такой
житейской возможности, как сообщить «начальству» и ждать распоряжений «на
месте происшествия». Или позвонить сразу и в милицию, и в скорую помощь,
и в пожарную охрану, одновременно принимая собственные меры. Конечно, всякое
поведение можно моделировать, но, например, несколько исключений в одном ме
сте возникнуть не могут.
191
8.3.3. Реакция на исключение – принципы пластыря и катапульты
Принятая в ЯП стратегия обработки исключений прямо связана со взглядом на сущность исключений. Этот взгляд, в свою очередь, зависит от важнейших требо ваний, определивших авторскую позицию при создании ЯП. Хотя в итоге разли чия поведения могут показаться не такими уж значительными, рассмотреть их обоснование и интересно, и поучительно. Выберем для определенности два ЯП: ПЛ/1 и Аду. Скажем заранее, что различия касаются лишь продолжения работы после реакции на исключение.
Принцип пластыря. Сначала о ПЛ/1. Несколько упрощая, можно сказать, что
один из основных принципов конструирования языка ПЛ/1 – «предпочитать такие истолкования конструкций, которые позволяют оправдать их дальнейшее исполне ние». В соответствии с этим принципом введены многочисленные правила подразу меваемых преобразований данных, допустимы различного рода сокращения и т. п.
В согласии с этим принципом исключение трактуется как относительно ред
кое, но в целом естественное для выполняемого конструкта событие. При его об работке следует направить усилия на скорейшее возобновление прерванного про цесса. Эти усилия можно наглядно представить себе как наложение пластыря на «рану». Естественная модель поведения – прервать исполняемый процесс, выз вать «врачебную бригаду», после окончания «лечения» продолжить прерванный процесс. Обратите внимание на три составляющих поведения – прервать, вызвать (это значит, понять, кого вызвать, – врачебная бригада подбирается динамиче ски) и продолжить.
Подобный взгляд полезен, например, при нехватке памяти – нужно вызвать
подпрограмму сборки мусора или подпрограмму динамического выделения памя ти, а затем попытаться продолжить работу. Описанное отношение к сущности ис ключения можно назвать принципом пластыря.
192
Однако где гарантии, что «заклеенный» процесс сможет нормально работать? Если исключение связано с окончанием файла или нарушением диапазона, то бес смысленно продолжать работу прерванного процесса. В ПЛ/1 в таких случаях в реакции на исключение (после «лечения», если оно требуется) применяют пере дачу управления туда, откуда признано разумным продолжать работу. Например:
ON ENDFILE GOTO Ml;
По многим причинам это далеко не лучшее решение. Одна из основных при чин – в том, что динамическая структура программы оказывается слабо связан ной с ее статической структурой. Чтобы разобраться в программе, приходится «прокручивать» каждый оператор. Короче говоря, решение с передачей управле ния в общем случае затрудняет чтение и отладку программ. По «неструктуриро ванности» это решение можно сравнить с выходом из подпрограммы не по воз врату, а по передаче управления. Что при этом происходит с динамической цепочкой вызовов? Остается только гадать или определять, руководствуясь «тон кими» правилами!
Итак, будем считать обоснованным, что решение с передачей управления проти воречит концепции «структурного программирования». В Аде стараются обойтись без goto, тем более что таким способом «далеко не уйдешь», – в этом ЯП самая вло женная последовательность операторов, окружающая объявление метки, должна окружать и оператор перехода на эту метку. А без передачи управления принцип пластыря не позволяет адекватно обрабатывать многие виды исключений.
Упражнение. Приведите соответствующие примеры.
Современное состояние языков программирования
Принцип катапульты. Одно из ключевых требований к языку Ада – способ ствовать надежному программированию. Другими словами, следует стремиться к минимуму отказов изза ошибок в программе и в данных. Когда же отказ неизбе жен, то следует обеспечить по меньшей мере осмысленную диагностику.
Требование надежности оправдывает трактовку исключения как свидетель ства полной непригодности «аварийного процесса» (процесса, где возникло ис ключение) к нормальной работе в создавшихся условиях. Стремясь к минимуму отказов, следует не «лечить» аварийный процесс, а нацелить обработку исключе ния на локализацию последствий «аварии», на создание возможности продол жать работу тех (связанных с аварийным) процессов, которых авария пока не кос нулась.
Саму обработку исключения в «аварийном» процессе обычно разумно рас сматривать скорее не как «лечение», а как «посмертную выдачу» – попытку со хранить как можно больше сведений для анализа ситуации на уровне иерархии, принимающем решения о последующих действиях.
Именно такой принцип действует в Аде (ведь надежность – одна из основных целей этого ЯП), а также в ЯП Эль76 для машин серии Эльбрус.
При такой цели естественная стратегия – последовательно признавать аварий ными вложенные процессы (начиная с самого внутреннего) до тех пор, пока среди них не найдется процесс, в котором приготовлена реакция на возникшее исключе ние. При этом аварийные процессы, в которых нет нужной реакции, последова
Исключения
тельно завершаются аварийным способом («катапультированием»). Найденная в конечном итоге реакция на исключение призвана обеспечить нормальное продол жение работы уцелевших процессов (и, возможно, выдачу сообщения об ошибке).
Итак, никакого возврата к аварийному процессу при такой стратегии нет,
а значит, нет и опасности вызвать «лавину» исключений и сообщений об авариях. Если ведущий процесс сочтет возможным, он может снова запустить (в новых условиях) и бывший аварийный процесс. Но решение об этом не встроено в се мантику ЯП, а программируется на уровне иерархии, высшем по отношению к аварийному процессу.
Назовем описанный принцип обработки исключений принципом катапульты.
Название связано с тем, что исключение заставляет управление немедленно поки нуть признанный аварийным процесс, приняв меры к спасению самой ценной ин формации (вполне аналогично тому, как катапультируется с аварийного самолета летчик, спасая самое ценное – человеческую жизнь).
Именно этот принцип поведения исполнителя в исключительных ситуациях
воплощен в Аде (как целиком отвечающий требованиям к этому ЯП и, в частно сти, способствующий надежному и структурному программированию). Пример на стр. 188 показывает, как можно ликвидировать аварию и продолжить работу (после реакции на исключение управление покидает блок В).
193
8.3.4. Ловушка исключений
Итак, в зависимости от ЯП реакция на исключение может быть и «пластырем», и «катапультой». Осталось объяснить, как она устроена.
В общем случае тела подпрограмм, тела пакетов, тела задач, а также блоки со
держат в конце обычной последовательности операторов еще часть, отделенную ключевым словом exception. Это и есть ловушка исключений. Она устроена ана логично оператору выбора, но вместо значений перечисляемого типа после клю чевого слова when фигурируют имена исключений.
Например:
begin
... -- последовательность операторов
exception -- ловушка исключений
when плохо_обусловленная | численная ошибка =>
PUT("матрица плохо обусловлена");
when others =>
PUT("фатальная ошибка"); raise ошибка;
end;
Альтернатива others, как обычно, выбирается в том случае, когда не выбраны
остальные. В нашем примере при возникновении исключения плохо_обусловлен ная или численная_ошибка (первое – объявляемое, второе – предопределенное) печатается «плохо обусловленная матрица» и обработка исключения завершается (затем продолжает нормально работать динамически объемлющий процесс). Лю
194
бые другие исключения будут пойманы альтернативой others, будет напечатано «фатальная ошибка», и возникнет новое исключение «ошибка» как результат ра боты оператора исключения (raise).
Это исключение будет распространяться в динамически объемлющих процес сах, пока не попадет в ловушку (для предопределенных исключений ловушки предусмотрены в предопределенном пакете «система»). Если бы второй альтер нативы не было, то любое исключение, отличное от двух указанных в первой аль тернативе нашей ловушки, распространялось бы по динамически объемлющим процессам до «своей» ловушки.
Современное состояние языков программирования
8.4. Дополнительные особенности обработки исключений
Исключения в объявлениях. Когда исключение возникает в объявлениях некото рого конструкта, то оно немедленно распространяется на динамически объемлю щие конструкты. Собственная ловушка конструкта рассчитана только на исклю чения, возникшие среди операторов конструкта. Это правильно, так как в ловушке могут использоваться объекты, которые изза «недообработки» объявле ний (ведь в них возникла авария) окажутся еще не существующими.
Приведенный выше пример показывает, что есть и еще одна причина – можно попасть в бесконечный цикл, если при возникновении исключения «ошибка» ис кать реакцию в той же ловушке. Напомним, что саму ловушку естественно счи тать объявлением, а именно объявлением «тел» исключений.
Исключения в асинхронных процессах. Интересно рассмотреть особенности распространения исключений на взаимодействующие задачи. У тела задачи нет динамически объемлющего процесса – соответствующий телу задачи процесс ра ботает асинхронно, с ним могут быть динамически связаны посредством рандеву много других «равноправных» процессовпартнеров. С другой стороны, каждый асинхронный процесс запускается при обработке некоторого фиксированного объявления в определенном «родительском» конструкте.
Поэтому когда исключение возникает среди объявлений задачи, то эта задача аварийно завершается и на месте запустившего ее объявления (задачи) в роди тельском процессе возникает предопределенное исключение ошибка_взаимодей ствия. Другими словами, о возникшей чрезвычайной ситуации в задаче немедлен но «узнает» запускающий ее процесс (например, для того, чтобы можно было заменить незапущенную задачу). Заменить можно потому, что запускаемая зада ча еще наверняка не успела вступить во взаимодействие со своими асинхронными партнерами (почему?) и их можно пока не предупреждать об аварии. Не успела она и запустить дочерние процессыпотомки (потому что они считаются запущен ными только после нормального завершения обработки раздела объявлений в ро дительском процессе).
С другой стороны, если потенциальный клиент так и не запущенной (аварий ной) задачи попытается обратиться к ее входу, то в этом клиенте на месте вызова
Исключения
195
входа возникнет исключение ошибка_взаимодействия (такое исключение всегда возникает, если мастер, к которому пытаются обратиться, оказывается неработа ющим). Так что исключение в случае аварии в одном из асинхронных процессов распространяется не только «линейно» вверх по цепочке от потомков родителям, но и «веером» ко всем клиентам.
Когда же исключение возникает не в объявлениях, а в теле задачи и в своем
распространении доходит до самой внешней в этом теле ловушки, то задача завер шается аварийно (независимо от того, «поймано» ли исключение), но в запустив шем ее процессеродителе исключение не возникает. Повидимому, потому, что в общем случае ничего разумного он сделать не сможет – перезапускать аварийную задачу считается опасным: она уже успела поработать с партнерами, причем не только с клиентами, но и мастерами, а также запустить своих потомков.
Упражнение. Предложите версии ответа на вопрос, почему не возникает исклю
чение в мастерах, с которыми могла взаимодействовать аварийная задача, или в ее
потомках.
Подсказка. Возможно, авария не имеет к ним никакого отношения.
Заметим, что исключения, возникающие при рандеву, считаются возникшими
в обоих партнерах и распространяются в них независимо и асинхронно.
Уточнение к принципу динамической ловушки. Поиск ловушки происходит
с учетом динамической цепочки вызовов любых блоков, не обязательно процедур, пока она есть. Выше – с учетом статической вложенности объявлений, затем, воз можно, снова динамической цепочки вызовов и т. д.
Универсальная ловушка. Описанные ловушки требуют знать и учитывать все
имена потенциальных исключений – это не всегда удобно и даже возможно. Иногда нужно единообразно реагировать на любое исключение, то есть иметь средство абстракции от характера исключения, средство реагировать на сам факт его возникновения (на сам факт аварии – не важно, какой именно). Обычно это, по сути, потребность принять лишь меры общего характера, но отложить приня тие конкретных решений до достижения «достаточно компетентных» уровней иерархии.
Такая потребность возникает, вопервых, на уровнях программной иерархии,
которые просто некомпетентны содержательно реагировать на содержательные аварии, и, вовторых, в таких структурах, где возникшее исключение статически невидимо (и потому они формально не могут содержать ловушку с именем такого исключения).
Упражнение. Придумайте пример такой структуры.
Пример типичной программной иерархии:
package обслуживание is
нет_исполнителей, нет_ресурсов, нет_заказов : exception; -- содержательные
-- исключения
procedure распределить_работу is
...
196
raise нет_исполнителей; -- содержательное исключение, но что делать при его
... -- возникновении, здесь не ясно.
end распределить_работу; -- Поэтому ловушки нет.
procedure проверить_исполнение is
...
raise нет_заказов; -- по той же причине ловушки нет
...
end проверить_исполнение;
procedure выполнить is
...
raise нет_ресурсов; -- по той же причине ловушки нет
...
end выполнить;
procedure обслужить_категорию_А(f : файл) is
... -- «некомпетентный» уровень
begin
открыть(f); распределить_работу; ... выполнить; ... проверить_исполнение; ... закрыть(f);
Современное состояние языков программирования
-- Никакой реакции!
exception -- универсальная ловушка (для любых исключений)
when others => закрыть (f); raise;
-- что бы ни произошло, нужно
-- закрыть файл, а с остальным
-- пусть разбираются выше
end обслужить_ категорию_А;
...
exception -- это "компетентный" уровень
when нет_исполнителей =>
PUT(‘Bac много, а я одна!);
when нет_заказов =>
PUT(‘Íåò заказов, нужна реклама!);
end обслуживание;
Ловушка с альтернативой «when others» – это и есть ловушка, пригодная для любых исключений. Оператор raise перевозбуждает последнее возбужденное ис ключение, которое продолжает распространяться обычным методом. Все проис ходит почти так же, как если бы ловушки вообще не было. Но это «почти» – пред варительная реакция соответствующих уровней программной иерархии.
Исключения
197
Выход за границы видимости. Исключения нет_ресурсов (и др.) можно
объявить и на нижних уровнях иерархии, но обрабатывать (без учета их особенно стей) на верхних уровнях в универсальных ловушках. Здесь в Аде нет статическо го контроля. Повидимому, это считается более надежным, чем провоцировать от сутствие исключений изза опасений, что не удастся придумать адекватную реакцию (а при отсутствии таковой пришлось бы вставлять чисто формально ре акцию raise;). В других ЯП (с параметрамипроцедурами) подобные случаи были бы вообще статически неконтролируемыми (
package Ð is
procedure f; procedure Pr(f : proc) is -- параметр-процедура
f; -- здесь e не видно, но может распространяться end Pr; -- нет оснований контролировать наличие ловушки
-- исключения е – ведь возможны различные параметры-процедуры end Р; package body Р is
å : exception; procedure f is
raise e;
end f; -- ловушки нет end P; -- ловушки также нет with P; use P;
begin
Pr(f); -- здесь e видно
end; exception
when t => S; -- ловушки для e не оказалось end;
почему?). Например:
Можно и так запрограммировать Рr:
procedure Pr(f; proc) is
begin
f;
exception -- универсальная ловушка;
-- e невидимо, но обрабатывается
when others =-> ÷òî-òî; raise;
-- и распространяется дальше
end Ðr;
Различия между исключениями в объявлениях и операторах. Суть разли чий состоит в том, что ловушка блока не обслуживает исключений, возникших при обработке объявлений этого блока. Ловушка только для операторов.
Например:
<< Â >> declare
а : integer := f(22); – при вычислении f возможны исключения х : real;
begin
198
...
exception
when числ_ош => PUT(‘ОШ в блоке В’);
PUT(a); PUT(x);
end Â;
Современное состояние языков программирования
Исключение, возникшее при вычислении f, немедленно распространяется в точку, указанную стрелкой. (Почему так?) Ведь если исключение возникло сре ди объявлений, то нет гарантий, что нормально введены объекты, используемые в ловушке, а задача ловушки – сохранить объекты (для анализа) присваиванием глобальным переменным или выводом. Если в такой ситуации не обойти ло вушку, то ее исполнение может нанести глобальным объектам дополнительный ущерб (а также привести к новым исключениям).
Напомним, что при этом сама ловушка тоже считается объявлением, а именно «телом исключения».
Можно считать, что здесь проявляется еще один полезный принцип – принцип минимизации каскадов (взаимозависимых) исключений. С другой стороны, его можно непосредственно вывести из принципа минимальных повреждений.
Упражнение. Объясните смысл и обоснуйте принцип минимальных каскадов.
Подавление исключений (оптимизирующие указания). Для полноты пред ставления об обработке исключений осталось добавить, что в случае, когда неко торые проверки ограничений, потенциально приводящие к возникновению ис ключений, считаются дорогими (неприемлемо ресурсоемкими), их можно отменить с помощью так называемых прагм (оптимизирующих указаний) вида
pragma подавить(проверка_индексов, на => таблица);
Если реализация способна реагировать на такие указания (они не обязательны для исполнения), то в нашем случае проверка индексов будет отменена для всех объектов типа «таблица». Можно управлять исключениями и с точностью до отдель ных объектов. Конечно, подобными указаниями следует пользоваться очень осто рожно. В Аде есть и другие оптимизирующие указания. Все они не обязательны.
Выводы. Итак, на примере аппарата исключений в Аде показаны в действии все три основных принципа стр. 185. В частности, этот аппарат позволяет отде лить программирование содержательной функции программного сегмента от программирования его взаимодействия с другими сегментами в чрезвычайных (аварийных) обстоятельствах.
Программируя содержательную функцию, можно абстрагироваться от не обычных ситуаций, а программируя взаимодействие в необычных ситуациях, в значительной степени абстрагироваться от содержательной функции сегмента, опираясь на априорные правила поведения Адаисполнителя. Для особо важных чрезвычайных ситуаций можно заранее заготовить названия и ловушки в библио течных модулях, а также программировать ловушки в пользовательских модулях, конкретизируя необходимую реакцию. Таким образом, концепция исключения – одна из компонент общего аппарата абстракцииконкретизации в ЯП. Ее можно
Исключения
было бы довести и до уровня модульности, физически отделив ловушки от тел сегментов. Повидимому, такая возможность появится в ЯП будущего. (Точнее, уже появилась в языке SDL/PLUS [15] и в последних версиях языка Модула2).
Стоит заметить, что исключения – весьма специфический аспект ЯП, очевид
ным образом нуждающийся в развитии и определении более четкого места в структуре ЯП в целом. Ведь исключения – это не сообщения, но похожи; не пре рывания, но похожи; не goto, но похожи; не процедуры, но похожи; не типы, но похожи.
Действительно, это не обычные сообщения, потому что нет явного отправите
ля и адресата, нет явной структуры сообщения, но вместе с тем функция исключе ния – сообщить одному или нескольким процессам (содержащим подходящие ловушки) о чрезвычайных обстоятельствах. Это не обычные прерывания, потому что не предполагают обязательного участия «средств низкого уровня» – аппарату ры или ОС для своей обработки. Хотя исключения было бы правильно назвать «прерываниями на уровне исходного ЯП». Это не обычные процедуры, хотя ис ключениям соответствуют последовательности действий, вызываемых по именам (но тела связываются с именами не статически, а динамически, недопустимы па раметры, необязателен вызов и т. д.). Это не обычный тип, хотя можно объявлять соответствующие «объекты», потому что нельзя передавать их какимлибо опре деляемым пользователем операциям (нельзя передавать как параметры).
И вся эта увлекательная специфика исключений объясняется их особой ролью –
обслуживанием потребности в разделении нормального и «чрезвычайного» ре жимов работы программы с учетом принципов минимальных возмущений и ми нимальных повреждений.
Упражнение. Дополните анализ аппарата исключений в Аде с точки зрения связей
с другими языковыми конструктами. Проделайте то же для других известных вам
ЯП (например, Эль76 или ПЛ/1).
199
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]