Как происходит отслеживание свободного экстента? |
Структуры управления
Повторение
После возникновения исключения можно ли в процессе его обработки все восстановить? Можно, например, применить альтернативную стратегию, когда исходная стратегия потерпела неудачу.
Иногда полезно повторно применить ту же самую стратегию (пренебрегая обвинениями в безумности, приписываемыми Эйнштейну, - "безумно повторять ту же самую вещь и ожидать других результатов"). Безумно или нет, этот метод может работать, если причиной исключений, например, являются так называемые самоустраняемые сбои аппаратуры.
Для иллюстрации идеи позвольте показать, как работает в Eiffel механизм исключений, базирующийся на двух ключевых словах — rescue и Retry ("спаси" и "повтори"). Предложение rescue, введенное в подпрограмму, выполняется, когда программа становится получателем исключения. Retry является предопределенной булевской переменной, которая, если она принимает значение истина в конце rescue, становится причиной того, что тело выполнится снова; если же Retry ложно, то выполнение подпрограммы приводит к отказу. Вот пример:
transmit (m: MESSAGE) — Передать m, если возможно. local i: INTEGER do send_to_transmitter (m, i) rescue i:= i + 1 Retry:= (i <= count) end
Предполагается, что вызов send_to_transmitter (m, i) пытается послать m, используя i-й передатчик. Мы располагаем передатчиками по нашему выбору; те, у кого меньшие номера, работают быстрее, но имеют большую вероятность отказа. По этой причине мы вначале пытаемся использовать быстрые передатчики, но в случае их отказа переходим к более надежным.
Подобно любым другим локальным переменным типа INTEGER, переменная i инициализируется нулем на входе процедуры. Если встретится исключение, как результат отказа вызова send_to_transmitter, начнет выполняться предложение rescue, увеличив i на единицу. Если есть доступные передатчики, Retry получит значение true, что приведет к повторному выполнению тела (предложения do), передавая сообщение новым передатчиком, который может успешно работать. Если же в rescue Retry станет ложным, то все закончится отказом, исключение будет передано вверх по цепочке вызовов.
Этот механизм исключения явным образом разделяет две роли.
Е1 Нормальное тело подпрограммы — предложение do — не занимается непосредственно обработкой исключения, его задачей является выполнение контракта.
Е2 Обработчик исключения — предложение rescue — не пытается выполнить контракт, его задача обработать исключение. Подобно сотруднику спасательной службы, он расчищает завалы и создает условия для нормального продолжения программы, если это возможно.
Отказ в подпрограмме сигнализирует, что она не способна выполнить свой контракт. Если это не корневая процедура (верхний уровень выполнения), то это еще не означает, что все выполнение дает отказ, так как подпрограмма передает исключение вверх по цепочке вызовов, и в цепочке может найтись программа, исправляющая ситуацию, благодаря механизму Retry, и восстанавливающая нормальный процесс выполнения всей программы.
Как следствие, выполнение может откатиться назад, а потом снова вернуться к вызову x.r (...), бывшему причиной отказа, к объекту, присоединенному к x. Новый вызов на этом объекте может корректно работать, только если объект находится в согласованном состоянии — состоянии, удовлетворяющем инварианту класса. Правило, сопровождающее исключение, говорит, что в предложении rescue в случае, когда Retry становится ложным, предварительно следует восстановить инвариант класса. Это и означает "чистку завалов" в случае Е2. Это правило отражено в следующем принципе.
Почувствуй методологию
Исключение подпрограммы, приведшее к отказу, должно восстановить инвариант класса, а затем стать причиной исключения у вызвавшей подпрограммы.
Принцип позволяет определить, что случится, если получатель исключения не включил rescue-предложение. Это случай, характерный для большинства подпрограмм. Обычно программа включает небольшое число предложений rescue в наиболее важных критических точках, позволяющих либо восстановить нормальное выполнение, либо достойно завершиться в случае непредвиденных событий. Если исключение возникло в подпрограмме без rescue, то эта подпрограмма приведет к отказу, передавая исключение вызывающей подпрограмме. Это соответствует случаю, когда подпрограмма имеет предложение rescue в следующей форме:
rescue default_rescue
Здесь default_rescue является библиотечной подпрограммой, которая по умолчанию ничего не делает. Хорошая практика состоит в том, чтобы для каждого класса подготовить новую версию default_rescue с простой реализацией, восстанавливающей инвариант класса любым доступным образом.
default_rescue наследуется от класса верхнего уровня ANY, так что любой класс может переопределить эту реализацию. Эти концепции будут частью изучения наследования.
Детали исключения
В примере с передатчиком мог появляться только один вид исключения. Иногда может потребоваться рассматривать разные виды исключений, предусматривая для них различные способы обработки. Все механизмы обработки исключений позволяют получить доступ к деталям последнего исключения, таким как точный тип исключения. В ОО-языках, таких как Eiffel, Java, C# и C++, эта информация доступна через объект exception, создаваемый автоматически, когда появляется исключение.
В Eiffel этот объект доступен через запрос last_exception: его тип является потомком библиотечного класса EXCEPTION. Исключения, тип которых определен программистом, создаются явно, — для этого используется специальный оператор raise; говорят, что он "выбрасывает" исключение, создавая соответствующий объект.
Стиль try-catch обработки исключений
Вместо стиля "rescue-retry", основанного на Принципе Проектирования по Контракту, С++ использует стиль "try-catch", который воспроизведен и в языках Java и C# с небольшими вариациями. Основная идея (детали можно увидеть в приложениях, посвященных этим языкам программирования) состоит в том, чтобы писать код, в котором предусматривается возможность появления исключений, в охраняемых try операторах и обрабатывать любые возникшие исключения в одном из его catch-предложений.
try ... Обработка общего случая, возможно, создающего исключение ... catch (e: T1, T2) ... Обработка возникшего исключения типа T1 или T2 ... catch (e: T3, T4, T5) ... Обработка возникшего исключения типа T3, T4 или T5 ... ... end
Для каждого ожидаемого типа предусматривается свой процесс обработки. Оператор throw является аналогом raise, позволяя программно создавать исключение любого типа. В отличие от rescue, предложение catch не только "расчищает завалы", но и полностью обрабатывает ситуацию. Этот механизм не поддерживает в явной форме возможность возврата — retry, но возврат можно смоделировать, заключив try блок в цикл.
Две точки зрения на исключения
Как механизмы языка, оба стиля "try-catch" и "rescue-retry" эквивалентны: все, что выразимо в одном стиле, может быть выражено и в другом стиле. Однако их дух отражает две различные точки зрения на исключения. Согласно одной точке зрения, исключения представляют собой еще одну структуру управления, некий аналог "обобщенного goto", который обрабатывает случаи, отличающиеся от основного, общего случая. Согласно другой точке зрения, исключения — это только адреса вариантов, в частности, системных ошибок, но не способы их обработки. Возможны решения, находящиеся между этими граничными точками зрения, так что при написании сложного ПО вы сумеете выработать подходящий для вас стиль.
7.11. Приложение: Пример удаления GOTO
Этот последний раздел дает возможность попрактиковаться в удалении goto на специальном примере, довольно простом, но не тривиальном. Это дополнительный материал, который можно пропустить при первом чтении, хотя раздел (также дополнительный) в лекции по рекурсии основывается на нем.
Он использует концепции переменной и присваивания, которые формально мы еще не проходили, так что, если это ваша первая книга по программированию, вам следует вернуться сюда после чтения соответствующей лекции.
Если же вы не любите ждать, то вот что нужно понимать: переменная является программной сущностью, которая может принимать различные значения во время выполнения. Атрибуты класса являются переменными, но программа использует и локальные переменные, имеющие смысл только во время выполнения. Оператор присваивания x:= e позволяет дать переменной x новое значение, заданное выражением e.
Наш пример по удалению goto является не искусственным примером, а схемой, с которой мы встретимся при обсуждении задачи "Ханойская башня". Он включает совокупность операторов if ... then ...else ... end, которые могли бы быть представлены в терминах test ... goto... операторов. Для настоящего обсуждения не имеет значения смысл базисных операций — операторов I0, I1, I2, I3 и условий CO, C1, C2, — достаточно знать, что сами они не содержат ветвлений.
Стрелки справа показывают структуру управления с пересекающимися циклами, что выглядит достаточно запутанно — еще одно блюдо спагетти, — и трудно представить ее в виде структуры без goto.
Заметьте, что эта "перепутанность" возникла из-за порядка следования операторов (идущего от оригинального рекурсивного кода, который будет приведен в более поздних обсуждениях). От "перепутанности" можно легко избавиться, так как блок after_1 достижим только через goto (оператор, предшествующий ему, сам представляет goto, передавая управление по другому адресу). Мы переместим его в другое место, вне других блоков, например, в самый конец.
Спагетти распутаны! Мы видим три цикла с нормальной вложенностью. Остается еще переход goto after_1, но так как эта ветвь не используется другими операторами, то весь after_1 блок можно включить в предложение else. Так что нетрудно переписать всю структуру без всяких меток, используя вместо этого два вложенных цикла:
from I0 until over loop — Прежде позиция "start" from until not C0 loop I1 end from stop:= not C1 until stop loop — Прежде позиция "after_2" I3 stop:= ((not C1) or (not C2)) end over:= ((not C1) and C2) if not over then I2 end — Прежде позиция "after_1" end
Так как у двух циклов условия выхода не элементарны (второму внутреннему циклу перед первой итерацией необходимо выполнение not C1, а затем not C1 and not C2), в программе используются булевские переменные over и stop для представления этих условий. Пример демонстрирует стандартные приемы исключения goto.
7.12. Дальнейшее чтение
George Polya: How to Solve It, 2nd edition; Princeton University Press, 1957.
На русском языке: Пойа Д. Как решать задачу. Учпедгиз, 1959.
Не обращайте внимания на дату публикации George Polya книга по-прежнему остается бестселлером по этой теме.
Edsger W. Dijkstra: Goto Statement Considered Harmful, Letter to the Editor, in Communications of the ACM, Vol. 11, No. 3, March 1968, pp. 147-148. Доступна в Интернете: http://www.acm.org/clas-sics/oct95/.
Известная короткая статья "О вреде оператора goto" ознаменовала революцию в методологии и привела к структурному программированию. Объяснила, почему goto неприемлем для хорошего программирования и, что более важно, высветила процесс конструирования программ, лаконично и эффективно. Несмотря на прошедшие десятилетия, ее следует прочитать.
Ole-Johan Dahl, Edsger W Dijkstra, C.A.R Hoare: Structured Programming, Academic Press, 1972.
На русском языке: У.Дал, Э. Дейкстра, К. Хоор Структурное программирование. Мир 1975
Классика. Содержит три монографии, первая из которых, "Заметки по структурному программированию" Э. Дейкстры, наиболее известна, но две другие также интересны. Убедительная работа Хоора "О структурной организации данных" дополняет предыдущую работу. Совместная статья Хоора и Дала "Иерархические структуры программ" представляет презентацию языка Симула 67 и излагает концепции, теперь известные как ОО-программирование. Немногие книги оказали такое влияние на развитие программирования.
C.A.R. Hoare and D.C.S Allison: Incomputability, in ACM Computing Surveys, vol. 4, no. 3, September 1972, pages 169-178. Доступна по подписке: portal.acm.org/citation.cfm?id=356606.
Краткое, простое, ясное объяснение, почему решение некоторых проблем, таких как проблема остановки, не может быть запрограммировано на компьютере.
7.13. Ключевые концепции, изучаемые в этой лекции
- Структуры управления определяют последовательность действий при выполнении программы.
- Структуры управления могут рассматриваться как приемы решения задач, сводя исходную, возможно, сложную задачу к множеству более простых задач.
- Базисными структурами управления являются: составной оператор, задающий последовательное выполнение списка действий; условный оператор, задающий в зависимости от некоторых условий выполнение одного действия из заданного списка; цикл, задающий повторное выполнение действия.
- При использовании структур управления следует руководствоваться соображениями корректности. Цикл характеризуется инвариантом, задающим условие, которое поддерживается на всем протяжении цикла, и вариантом, положительным целочисленным выражением, уменьшающимся на каждой итерации цикла. Инвариант позволяет доказать корректность цикла, вариант — его завершаемость.
- Структуры низкого уровня, такие как goto, важны на машинном уровне, но с презрением отвергаются языками высокого уровня. Любая программа, использующая их, имеет эквивалент, выраженный в терминах стандартных структур управления.
Новый словарь
7-У. Упражнения
7-У.1. Словарь
Дайте точные определения всем терминам словаря.
7-У.2. Карта концепций
Добавьте новые термины в карту концепций, построенную в предыдущих лекциях.
7-У.3. Циклы в машинных языках/
Рассмотрите цикл в форме:
from Compound_1 until i = n loop Compound_2 end
Напишите код на машинном языке, используя команды BR и BEQ, рассмотренные при обсуждении переходов.
7-У.4. Блок-схема условного оператора
Следуя соглашениям для блок-схем, введенным для циклов, нарисуйте блок-схему условного оператора if Condition then Compound_1 else Compound_2 end.
7-У.5. Бём - Джакопини на практике
Рассмотрите следующий фрагмент программы с goto, применяющей условные goto-операторы перехода:
- Нарисуйте соответствующую блок-схему.
- Предложите фрагмент программы с тем же эффектом, но без goto, в котором используются базисные структуры управления: цикл, составной и условный операторы.
7-У.6. Формы цикла
Рассмотрите варианты базисной формы цикла и покажите, как выразить:
- repeat ... until ... через while ...
- while ... через repeat ... until.
- Базисную форму Eiffel (from ... until ...) через while ....
- Базисную форму Eiffel (from ... until ...) через repeat ... until ....
7-У.7. Эмуляция (моделирование) варианта
Вариант цикла обеспечивает доказательство завершаемости. Предположите, что синтаксис не включает variant-предложения, но для него по-прежнему задан инвариант. На примере вычисления максимума множества целых докажите завершаемость цикла, перестроив прежнее доказательство с вариантом (подсказка: введите переменную, сохраняющую предыдущее значение выражения варианта, используйте ее для построения инварианта).
7-У.8. Эмуляция retry в языке с try-catch
Рассмотрите схему для обработки исключений rescue-retry, такую как в примере transmit с передатчиком сообщения, которая может стать причиной нескольких выполнений главного алгоритма (от нуля до count раз). Покажите, как запрограммировать ее в языке программирования, предлагающего обработку исключений в стиле try-catch.
Можно использовать механизмы любого языка: Java, C# или C++, рассматриваемые в приложениях.
7-У.9. Эмуляция try-catch в языке с rescue-retry
Рассмотрите стиль обработки исключений try-catch, кратко описанный в этой лекции. Покажите, как эмулировать (моделировать) его или один из вариантов (такой как в Java с предложением finally), используя механизмы rescue-retry языка Eiffel.
Для определения типа последнего исключения используйте last_exception.type. Нотация {T} обозначает объект, представляющий тип T, который может быть типом исключения.