13.1 Стратегия

Предыдущая  Содержание  Следующая V*D*V

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

 

OOC_Strategy_1

 

Во-первых, устанавливается обработчик ошибок. Либо опасное действие завершается правильно, либо обработчик ошибок получает шанс на очистку до завершения комбинированного действия. Чтобы реализовать эту схему восстановления после ошибки, в ANSI-C используются setjmp() и longjmp():

 

#include <setjmp.h>

 

static jmp_buf onError;

 

static void cause() {

    longjmp(onError, 1);

}

 

action () {

    if (! setjmp(onError))

        опасное действие

    else

        обработчик ошибки

}

 

setjmp() инициализирует onError и возвращает ноль. Если в опасном действии или в функции, вызванной оттуда, что-то идёт не так, мы сигнализируем об ошибке, вызывая cause(). longjmp() в этой функции использует информацию в onError, чтобы выполнить вторичный возврат от вызова до setjmp(), который инициализировал onError. Вторичный возврат поставляет второй параметр longjmp() в виде значения функции; если это значение равно нулю, возвращается единица. Всё идёт ужасно неправильно, если функция, которая вызвала setjmp(), больше не активна.

В терминологии показанной выше картинки ошибка отсылает к вызову setjmp(), чтобы внести информацию для обработки ошибки. опасное действие выполняется, если setjmp() возвращает ноль; или в противном случае выполняется обработчик ошибок. cause() вызывают, чтобы инициировать восстановление после ошибки, передавая управление обработчику ошибок.

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

 

OOC_Strategy_2

 

В этом случае для восстановления необходимо два longjmp() : onError возвращает к основному циклу, а onLoadError используется для очистки после проблемы при операции загрузки:

 

jmp_buf onError, onLoadError;

 

#define cause(x)    longjmp(x, 1)

 

mainLoop () {

    if (! setjmp(onError))

        loadFile();

    else

        какая-то проблема

}

 

loadFile () {

    if (! setjmp(onLoadError))

        работаем с файлом

    else

        закрываем файл

        cause(onError);

}

 

Набросок кода показывает, что cause() так или иначе должна знать, как далеко должно пойти восстановление. Мы можем использовать с этой целью какой-нибудь аргумент или скрытую глобальную структуру.

Если мы даём cause() явный параметр, вероятно, он должен ссылаться на какой-то глобальный символ, чтобы его можно было вызывать из других файлов. Очевидно, чтоб глобальный символ не должен использоваться в момент ошибки. У этого есть дополнительный недостаток, заключающийся в том, что это часть клиентского кода, то есть хотя такой символ значим только для определённого обработчика ошибок, он записан в коде, защищённом обработчиком. Если этот код вызывается из какого-то другого места, мы должны быть уверены, что это случайно не относится к неактивной точке восстановления.

Намного лучшая стратегия — стек значений jmp_buf. Какая-то функция устанавливает обработчик ошибок, продвигая этот стек, а cause() использует самую верхнюю запись. Конечно, обработчик ошибок должен быть вытолкнут из стека до завершения соответствующей функции.

 

Предыдущая  Содержание  Следующая