М. Бен-Ари Языки программирования. Практический сравнительный анализ. Предисловие
Вид материала | Документы |
СодержаниеИсключительные ситуации 11.2. Исключения в PL/I 11.3. Исключения в Ada 11.4. Исключения в C++ 11.5. Обработка ошибок в языке Eiffei Типы перечисления Проектирование по контракту |
- Рабочей программы учебной дисциплины языки программирования Уровень основной образовательной, 47.91kb.
- Существуют различные классификации языков программирования, 174.02kb.
- Лекция 3 Инструментальное по. Классификация языков программирования, 90.16kb.
- Аннотация рабочей программы учебной дисциплины языки программирования Направление подготовки, 135.09kb.
- Лекция Языки и системы программирования. Структура данных, 436.98kb.
- Государственное Образовательное Учреждение высшего профессионального образования Московский, 1556.11kb.
- Программа дисциплины Языки и технологии программирования Семестры, 20.19kb.
- Календарный план учебных занятий по дисциплине «Языки и технология программирования», 43.35kb.
- Пояснительная записка Ккурсовой работе по дисциплине "Алгоритмические языки и программирование", 121.92kb.
- Утверждены Методическим Советом иэупс, протокол №8 от 24. 04. 2008г. Языки программирования, 320.93kb.
Исключительные ситуации
11.1. Требования обработки исключительных ситуаций
Ошибка во время выполнения программы называется исключительной ситуацией или просто исключением (exception). Когда программы исполнялись не интерактивно (offline), соответствующая реакция на исключительную ситуацию состояла в том, чтобы просто напечатать сообщение об ошибке и завершить выполнение программы. Однако реакция на исключение в интерактивной среде не может быть ограничена сообщением, а должна также включать восстановление, например возврат к той точке, с которой пользователь может повторить вычисление или, по крайней мере, выбрать другой вариант. Программное обеспечение, которое используется в таких встроенных системах, как системы управления летательными аппаратами, должно выполнять восстановление при ошибках без вмешательства человека. Обработка исключений до недавнего времени, как правило, не поддерживалась в языках программирования; использовались только средства операционной системы. В этом разделе будут описаны некоторые механизмы обработки исключений, которые существуют в современных языках программирования.
Восстановление при ошибках не дается даром. Всегда есть затраты на дополнительные структуры данных и алгоритмы, необходимые для идентификации и обработки исключений. Кроме того, часто господствует точка зрения, что код обработки исключений сам является компонентом программы и может содержать ошибки, вызывающие более серьезные проблемы, чем первоначальное исключение! К тому же чрезвычайно трудно идентифицировать ситуации, приводящие к ошибке, и тестировать код обработки исключений, потому что сложно, а иногда и невозможно, создать ситуации, приводящие к ошибке.
Какие свойства делают средства обработки исключений хорошими?
• В случае отсутствия исключения издержки должны быть очень небольшими.
• Обработка исключения должна быть легкой в использовании и безопасной.
Первое требование важнее, чем это может показаться. Поскольку мы предполагаем, что исключительные ситуации, как правило, не возникают, издержки для прикладной программы должны быть минимальны. При наступлении исключительной ситуации издержки на ее обработку обычно не считаются существенными. Суть второго требования в том, что, поскольку исключения происходят нечасто, программирование реакции на них не должно требовать больших усилий; само собой разумеется, что обработчик исключения не должен использовать конструкции, которые могут привести к ошибке.
Одно предупреждение для программиста: обработчик исключений не является заменой условного оператора. Если ситуация может возникать, это не является ошибкой и должно быть явно запрограммировано. Например, вероятность того, что такие структуры данных, как список или дерево, окажутся пустыми, весьма велика, и эту ситуацию необходимо явно проверять, используя условный оператор:
Ada |
С другой стороны, переполнение стека или потеря значимости в операциях с плавающей точкой встречается очень редко и почти наверняка указывает на ошибку в вычислениях.
В качестве элементарной обработки исключений в некоторых языках пользователю дана возможность определять блок кода, который будет выполнен перед завершением программы. Это полезно для наведения порядка (закрытия файлов и т.д.) перед выходом из программы. В языке С средство setjmp/longjmp позволяет пользователю задать дополнительные точки внутри программы, в которые обработчик исключений может возвращаться. Этого типа обработки исключений достаточно, чтобы гарантировать, что программа «изящно» завершится или перезапустится, но он недостаточно гибок для детализированной обработки исключений.
Обратите внимание, что, согласно нашему определению исключения как непредвиденной ошибки на этапе выполнения, в языке С «исключений» меньше, чем в таком языке, как Ada. Во-первых, такие ошибки, как выход за границы массива, не определены в языке С; они просто являются ошибками программиста, которые не могут быть «обработаны». Во-вторых, поскольку в С нет гибкого средства обработки исключений, каждая возможность языка, которая запрашивается через подпрограмму, возвращает код, указывающий, был запрос успешным или нет. Таким образом, в языке Ada распределение памяти с помощью new может вызвать исключительную ситуацию, если нет достаточного объема памяти, тогда как в С malloc возвращает код, который должен быть проверен явно. Выводы для стиля программирования следующие: в Ada можно использовать new обычным порядком, а обработку исключений проектировать независимо, в то время как в С полезно написать подпрограм-му-оболочку для malloc так, чтобы реакцию на исключительные ситуации можно было разработать и запрограммировать централизованно, вместо того чтобы разрешать каждому члену группы тестировать (или забывать тестировать) нехватку памяти:
void* get_memory(int n)
C |
void* p = malloc(n);
if (p == 0) /* Выделение памяти потерпело неудачу */
/* Сделайте что-нибудь или корректно
завершите работу */
return р;
}
11.2. Исключения в PL/I
PL/1 был первым языком, который содержал средство для обработки исклю-чительных ситуаций в самом языке — блок «при наступлении события» или, |коротко, «при» (on-unit). Он является блоком кода, который выполняется, ког-да возникает исключительная ситуация; после его завершения вычисление продолжается. Проблема в PL/1, связанная с блоком «при», состоит в том, что он влияет на обычные вычисления. Предположим, что активизирован блок, Срабатывающий при потере значимости с плавающей точкой. Тогда потенци-ально возможно воздействие на каждое выражение с плавающей точкой; дру-гими словами, каждое выражение с плавающей точкой содержит неявный вы-|зов блока и возврат из него. Это затрудняет выполнение оптимизации сохра-нения значений в регистрах или вынесения общих подвыражений.
11.3. Исключения в Ada
В языке Ada определен очень простой механизм обработки исключений, ко-
торый послужил моделью для других языков.
В Ada есть четыре предопределенных исключения:
Constraint_Error (ошибка ограничения). Нарушение ограничивающих ус-ловий, например, когда индексация массива выходит за границы или вы- бор вариантного поля не соответствует дискриминанту.
Storage_Error (ошибка памяти). Недостаточно памяти.
Program_Error (программная ошибка). Нарушение правил языка, напри-
мер выход из функции без выполнения оператора return.
Tasking_Error (ошибка задачи). Ошибки, возникающие при взаимодейст- вии задач
(см. гл. 12).
Конечно, Constraint_Error — наиболее часто встречающееся исключение, связанное со строгим контролем соответствия типов в языке Ada. Кроме того,
программист может объявлять исключения, которые обрабатываются точно
так же, как и предопределенные исключения.
Когда исключительная ситуация наступает, в терминологии языка Ada —возбуждается (raised), вызывается блок кода, называемый обработчиком исключения (exeption handler). В отличие от PL/I вызов обработчика завершает включающую процедуру. Так как обработчик не возвращается к нормальному вычислению, нет никаких помех для оптимизации. В отличие от обработчиков глобальных ошибок в С, обработка исключительных ситуаций в Ada чрезвычайно гибкая, потому что обработчики исключений могут быть привязаны к любой подпрограмме:
procedure Main is
procedure Proc is
P: Node_Ptr;
begin
P := new Node; -- Может возбуждаться исключение
Statement_1; -- Пропускается, если возбуждено исключение
exception
when Storage_Error =>
-- Обработчик исключения
end Proc; begin Proc; Statement_2; — Пропускается, если исключение распространилось
из Proc
exception
when Storage_Error =>
-- Обработчик исключения
end Main;
После последнего исполняемого оператора подпрограммы ключевое слово exception вводит последовательность обработчиков исключений — по одному для каждого вида исключений. Когда возбуждается исключение, процедура покидается, и вместо нее выполняется обработчик исключения. Когда обработчик завершает свою работу, выполняется нормальное завершение процедуры. В приведенном примере программа выделения памяти может породить исключительную ситуацию Storage_Error, в этом случае Statement_1 пропускается, и выполняется обработчик исключения. После завершения обработчика и нормального завершения процедуры главная программа продолжается с оператора Statement_2.
Семантика обработки исключений предоставляет программисту большую гибкость в управлении обработкой исключительных ситуаций:
• Если исключительная ситуация не обработана внутри процедуры, попытка ее выполнения оставляется, и исключительная ситуация возбуждается снова в точке вызова. При отсутствии обработчика в Proc исключение повторно было бы возбуждено в Main, оператор Statement_2 был бы пропущен и выполнен обработчик в Main.
• Если исключительная ситуация возбуждается во время выполнения обработчика, обработчик оставляется, и исключение возбуждается снова в точке вызова.
• У программиста есть выбор: возбудить то же самое или другое исключение в точке вызова. Например, мы могли бы перевести предопределенное исключение типа Storage_Error в исключение, определенное в прикладной программе. Это делается с помощью оператора rais в обработчике:
exception
when Storage_Error =>
… -- Обрабатывается исключение, затем
raise Overflow; --Возбуждается исключение Overflow в точке вызова
Обработчик для others может использоваться, чтобы обработать все исключения, которые не упомянуты в предыдущих обработчиках.
Если даже в главной программе нет обработчика для исключения, оно обрабатывается системой поддержки выполнения, которая обычно прерывает выполнение программы и выдает сообщение. Хорошим стилем программирования можно считать такой, при котором все исключения гарантированно обрабатываются хотя бы на уровне главной программы.
Определение исключений в языке Ada 83 не позволяло обработчику иметь доступ к информации о ситуации. Если более одной исключительной ситуации обрабатываются одинаково, никаким способом нельзя было узнать, что же именно произошло:
exception
when Ех_1 | Ех_2 | Ех_3 =>
--Какое именно исключение произошло?
Язык Ada 95 позволяет обработчику исключительной ситуации иметь параметр:
exception
when Ex: others =>
Всякий раз при возбуждении исключения параметр Ех будет содержать информацию, идентифицирующую исключение, а предопределенные процедуры позволят программисту отыскать информацию относительно исключения. Эта информация также может быть определена программистом (см. справочное руководство по языку, раздел 11.4.1).
Реализация
Реализуются обработчики исключений очень эффективно. Процедура, которая содержит обработчики исключений, имеет дополнительное поле в записи активации с указателем на обработчики (см. рис. 11.1). Требуется только одна команда при вызове процедуры, чтобы установить это поле, вот и все издержки при отсутствии исключений.
Если исключение возбуждается, то, чтобы найти обработчик, может потребоваться большой объем вычислений для поиска по динамической цепочке, но, поскольку исключения происходят редко, это не представляет проблемы. Вспомните наш совет не использовать обработчик исключений как замену для гораздо более эффективного условного оператора.
11.4. Исключения в C++
Обработка исключений в C++ во многом сходна с той, которая применяется в языке Ada, а именно, исключение можно явно возбудить, обработать соответствующим обработчиком (если он есть), после чего блок (подпрограмма) окажется завершенным. Отличия в следующем:
• Вместо приписывания обработчика исключения к подпрограмме используется специальный синтаксис для указания группы операторов, к которым применяется обработчик.
• Исключения идентифицируются типом параметра, а не именем. Имеется специальный эквивалент синтаксиса others для обработки исключений, не упомянутых явно.
• Можно создавать семейства исключений, используя наследование (см. гл. 14).
• Если в языке Ada для исключения в программе не предусмотрен обработчик, то вызывается системный обработчик. В C++ программист может определить функцию terminate(), которая вызывается, когда исключение не обработано.
В следующем примере блок try идентифицирует область действия последовательности операторов, для которых обработчики исключений (обозначенные как catch-блоки) являются активными. Throw-оператор приводит к возбуждению исключений; в этом случае оно будет обработано вторым catch-блоком, так как строковый параметр throw-оператора соответствует параметру char* второго catch-блока:
void proc()
{
… // Исключения здесь не обрабатываются
try {
…
throw "Invalid data"; // Возбудить исключение
}
catch (int i) {
… // Обработчик исключения
}
catch (char *s) {
… // Обработчик исключения
}
catch (...) { // Прочие исключения
…. // Обработчик исключений
}
}
Как в Ada, так и в C++ допускается, чтобы обработчик вызывался для исключения, которое он не может видеть, потому что оно объявлено в теле пакета (Ada), или тип объявлен как private в классе (C++). Если исключение не обработано и в others (или ...), то оно будет неоднократно повторно возбуждаться до тех пор, пока, наконец, с ним не обойдутся как с необработанным исключением. В C++ есть способ предотвратить такую неопределенность поведения с помощью точного объявления в подпрограмме, какие исключительные ситуации она готова обрабатывать:
void proc() throw (t1 , t2, t3);
Такая спецификация исключений (exception specifications) означает, что отсутствующие в списке исключения, которые, возможно, будут возбуждаться, но не будут обрабатываться в ргос (или любой подпрограмме, вызванной из ргос), немедленно вызывут глобально определенную функцию unex-pectedQ вместо того, чтобы продолжать поиск обработчика. В больших системах эта конструкция полезна для документирования полного интерфейса подпрограмм, включая исключения, которые будут распространяться.
11.5. Обработка ошибок в языке Eiffei
Утверждения
В языке Eiffei подход к обработке исключений основан на концепции, что, прежде всего, ошибок быть не должно. Конечно, все программисты борются за это, и особенность языка Eiffei состоит в том, что в него включена поддержка определения правильности программы. Она основана на понятии утверждений (assertions), которые являются логическими формулами и обычно используются для формализации программы, но не являются непосредственно частью ее (см. раздел 2.2). Каждая подпрограмма, называемая рутиной (routine) в Eiffei, может иметь связанные с ней утверждения. Например, подпрограмма для вычисления результата (result) и остатка (remainder) целочисленного деления делимого (dividend) на делитель (divisor) была бы написана следующим образом:
integer_division(dividend, divisor, result, remainder: INTEGER) is
require
divisor > 0
do
from
result = 0; remainder = dividend;
invariant
dividend = result*divisor + remainder
variant
remainder
until
remainder < divisor
loop
remainder := remainder - divisor;
result := result + 1 ;
end
ensure
dividend = result*divisor + remainder;
remainder < divisor
end
Конструкция require (требуется) называется предусловием и специфицирует, какие выходные данные подпрограмма считает правильными. Конструкция do (выполнить) содержит выполняемые операторы, собственно и составляющие программу. Конструкция ensure (гарантируется) называется постусловием и содержит. условия, истинность которых подпрограмма обещает обеспечить, если будет выполнена конструкция do над данными, удовлетворяющими предусловию. В данном случае справедливость постусловия является достаточно тривиальным следствием инварианта (см. 6.6) и условия until.
На большем масштабе вы можете присоединить инвариант к классу (см. раздел 15.4). Например, класс, реализующий стек с помощью массива, включал бы инвариант такого вида:
invariant
top >= 0;
top < max;
Все подпрограммы класса должны гарантировать, что инвариант истинен, когда объект класса создан, и что каждая подпрограмма сохраняет истиность инварианта. Например, подпрограмма pop имела бы предусловие top> 0, в противном случае выполнение оператора:
top := top - 1
могло бы нарушить инвариант.
Типы перечисления
Инварианты применяются также, чтобы гарантировать безопасность типа, которая достигается в других языках использованием типов перечисления. Следующие объявления в языке Ada:
Ada |
Dial: Heat;
были бы записаны на языке Eiffel как обычные целые переменные с именованными константами:
Dial: Integer;
Off: Integer is 0;
Low: Integer is 1;
Medium: Integer is 2;
High: Integer is 3;
Инвариант гарантирует, что бессмысленные присваивания не выполнятся:
invariant
Off <= Dial <= High
Последняя версия языка Eiffel включает уникальные константы (unigue constants), похожие на имена перечисления в том отношении, что их фактические значения присваиваются компилятором. Однако они по-прежнему остаются целыми числами, поэтому безопасность типа должна по-прежнему обеспечиваться с помощью утверждений: постусловие должно присоединяться к любой подпрограмме, которая изменяет переменные, чьи значения должны быть ограничены этими константами.
Проектирование по контракту
Утверждения — базовая компонента того, что язык Eiffel называет проектированием по контракту, в том смысле, что проектировщик подпрограммы заключает неявный контракт с пользователем подпрограммы: если вы обеспечите состояние, которое удовлетворяет предусловию, то я обещаю преобразовать состояние так, чтобы оно удовлетворяло постусловию. Точно так же класс поддерживает истинность своих инвариантов. Если контракты используются в системе повсеместно, то ничто никогда не может идти неправильно.
На практике, конечно, разработчик может потерпеть неудачу, пытаясь создать соответствующую контракту подпрограмму (либо потому, что операторы не удовлетворяют утверждениям, либо потому, что были выбраны неправильные утверждения). Для отладки и тестирования в реализации языка Eiffel для пользователя предусмотрена возможность запросить проверку утверждений при входе в подпрограмму и выходе из нее так, чтобы можно было остановить выполнение, если утверждение неверно.
Исключения
Подпрограммы Eiffel могут содержать обработчики исключений:
proc is
do
… -- Может возбуждаться исключение
rescue
… -- Обработчик исключения
end;
Когда возбуждается исключение, считается, что подпрограмма потерпела неудачу, и выполняются операторы после rescue. В отличие от языка Ada, после завершения обработчика исключение порождается снова в вызывающей программе. Это эквивалентно завершению в Ada обработчика исключения raise-оператором, который повторно порождает в вызывающей подпрограмме то же самое исключение, которое заставило вызвать обработчик.
Мотивировка такого решения в предположении, что постусловие подпрограммы (и/или инвариант класса) удовлетворяются.
Если это не так, то вам, возможно, захочется получить уведомление, но уж наверняка вы не можете удовлетворить постусловие, т. е. потерпели неудачу при выполнении работы, которую заказала вам вызывающая подпрограмма. Другими словами, если.вам известно, как справиться с проблемой и удовлетворить постусловие, то предусмотрите это в подпрограмме. Это аналогично нашему совету не пользоваться исключениями вместо операторов if.
Обработчик исключения для помощи в решении возникших проблем может вносить некоторые изменения и запрашивать повторное выполнение подпрограммы с самого начала, если в него включено ключевое слово retry в качестве последнего оператора. Новая попытка может привести или не привести к успеху. Принципиально здесь то, что успешное выполнение — это нормальное завершение подпрограммы с выполненным постусловием. В противном случае ее выполнение терпит неудачу.
Обработчик исключений в языке Ada можно смоделировать в Eiffel следующим образом, хотя это идет вразрез с философией языка:
proc is
local
tried: Boolean; -- Инициализировано как false;
do
if not tried then
-- Обычная обработка
-- Порождает исключения
else
-- «Обработчик исключения»
end
rescue
if not tried then
tried := true; -- Чтобы не было двойного повтора
retry
end
end;
11.6. Упражнения
1. Пакет языка Ada. Исключения в Ada 95 определяют типы и подпрограммы для сопоставления информации с исключениями. Сравните эти конструкции с конструкциями throw и catch в C++.
2. Покажите, что исключение в языке Ada может быть порождено вне области действия исключения. (Подсказка: см. гл. 13.) Как можно обработать исключение, объявление которого не находится в области действия?
3. Покажите, как описание исключений на языке C++: void proc() throw(t1, t2, t3); может быть смоделировано с помощью многократных catch-блоков.
4. Изучите класс EXCEPTION в языке Eiffel и сравните его с обработчиком исключения в языке Ada.