Заметьте, после двух попыток (в общем случае n попыток) предложение rescue достигает конца, не вызывая retry, следовательно, приводит к отказу.
Давайте рассмотрим более тщательно, что случается, когда включается исключение во время выполнения r. Нормальное выполнение (тела) останавливается; вместо этого начинает выполняться предложение rescue. После чего могут встретиться два случая:
[x]. Предложение rescue выполнит в конечном итоге retry. В этом случае начнется повторное выполнение тела программы. Эта новая попытка может быть успешной, тогда программа нормально завершится и управление вернется к клиенту. Вызов успешен, контракт выполнен. За исключением того, что вызов мог занять больше времени, никакого другого влияния появление исключения не оказывает. Если, однако, повторная попытка снова приводит к исключению, то вновь начнет работать предложение rescue.
[x]. Если предложение rescue не выполняет retry, оно завершится естественным образом, достигнув end. (В последнем примере это происходит, когда attempts >=2.) В этом случае программа завершается отказом; она возвращает управление клиенту, сигнализируя о неудаче выбрасыванием исключения. Поскольку клиент должен обработать возникшее исключение, то снова возникают два рассмотренных случая, теперь уже на уровне клиента.
Этот механизм строго соответствует принципу Дисциплинированной Обработки Исключения. Программа завершается либо успехом, либо отказом. В случае успеха ее тело выполняется до конца и гарантирует выполнение постусловия и инварианта. Когда выполнение прерывается исключением, то можно либо уведомить об отказе, либо попытаться повторно выполнить нормальное тело. Но нет никакой возможности выхода из предложения rescue, уведомив клиента, что все завершилось нормально.
Задача предложения rescue
Последний комментарий позволяет нам продвинуться в лучшем понимании механизма исключений, обосновав теоретическую роль предложения rescue. Формальные рассуждения помогут получить полную картину.
Корректность предложения rescue
Формальное определение корректности класса выдвигает два требования к компонентам класса. Первое (1) требует, чтобы процедуры создания гарантировали корректную инициализацию - выполнение инварианта класса. Второе (2) напрямую относится к нашему обсуждению, требуя от каждой программы, запущенной при условии выполнения предусловия и инварианта класса, выполнения в завершающем состоянии постусловия и инварианта класса. Диаграмма, описывающая жизненный цикл объекта, отражает эти требования:
Рис. 12.3. Жизнь объекта
Формально правило (2) говорит:
3.
Для каждой экспортируемой программы r и любого множества правильных аргументов xr
{prer (xr) and INV} Bodyr {postr (xr) and INV}
Для простоты позвольте в дальнейшем рассмотрении игнорировать аргументы xr.
Пусть Rescuer обозначает ту часть предложения rescue, в которой игнорируются все ветви, ведущие к retry, другими словами в этой части сохраняются все ветви, доходящие до конца предложения rescue. Правило (2) задает спецификацию для программ тела - Bodyr. Можно ли получить такую же спецификацию для Rescuer? Она должна иметь вид:
{ ? } Rescuer { ? }
с заменой знаков вопроса соответствующими утверждениями. (Полезно, перед дальнейшим чтением постараться самостоятельно задать эти утверждения.)
Рассмотрим, прежде всего, предусловие для Rescuer. Любая попытка написать нечто не тривиальное будет ошибкой! Напомним, чем сильнее предусловие, тем проще работа программы. Любое предусловие для Rescuer ограничит число случаев, которыми должна управлять эта программа. Но она должна работать во всех ситуациях! Когда возникает исключение, ничего нельзя предполагать, - такова природа исключения. Нам не дано предугадать, когда компьютер даст сбой, или пользователю вздумается нажать клавишу "break".
Поэтому остается единственная возможность - предусловие для Rescuer равно True. Это самое слабое предусловие, удовлетворяющее всем состояниям и означающее, что Rescuer должна работать во всех ситуациях.
Для ленивого создателя Rescuer это "плохая новость", - тот случай, когда "заказчик всегда прав"!
Что можно сказать о постусловии Rescuer? Напомню, эта часть предложения rescue ведет к отказу, но, прежде чем передать управление клиенту, необходимо восстановить стабильное состояние. Это означает необходимость восстановления инварианта класса.
Отсюда следует правило, в котором уже больше нет знаков вопросов:
Правило корректности для включающего отказ предложения rescue
4.
{True} Rescuer {INV}
Похожие рассуждения дают правило для Retryr - части предложения rescue, включающей ветви, приводящие к инструкции retry:
Правило корректности для включающего повтор предложения rescue
5.
{True} Retryr {INV and prer }
Четкое разделение ролей
Интересно сравнить формальные роли тела и предложения Rescuer:
{prer and INV} Bodyr {postr (xr) INV}
{True} Rescuer {INV}
Входное утверждение сильнее для Bodyr - в то время, когда Rescuer не накладывает никаких требований, перед началом выполнение тела программы (предложения do) должно выполняться предусловие и инвариант. Это упрощает работу Bodyr.
Выходное утверждение также сильнее для Bodyr - в то время, когда Rescuer обязана восстановить только инвариант класса, Bodyr обязана сыграть свою роль и обеспечить истинность выполнения постусловия. Это делает ее работу более трудной.
Эти правила отражают разделение ролей между предложением do и предложением rescue. Задача тела обеспечить выполнение контракта программы, не управляя непосредственно исключениями. Задача rescue - управлять обработкой исключениями, возвращая управление либо телу программы, либо вызывающей программе. Но в обязанности rescue не входит обеспечение контракта.