Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

19. Обработка исключений изнутри: Условия и Перезапуск

Одной из замечательных особенностей Лиспа является его система условий. Она служит тем же целям, что и системы обработки исключений в Java, Python и C++, но является более гибкой. На самом деле её способности выходят за пределы обработки ошибок – условия являются более всеохватывающими, чем исключения, в том смысле, что условия могут представить любое событие во время выполнения программы, которое может представлять интерес в программировании различных уровней стека вызовов. Например в секции "Другие применения условий" вы увидите, что условия могут быть использованы для выдачи предупреждения без нарушения выполнения кода, который выдаёт предупреждение, в то же время позволяя коду выше на стеке вызовов контролировать, напечатано ли предупреждающее сообщение. Пока, однако, я сосредоточусь на обработке ошибок.

Система условий более гибка, чем система исключений, потому что вместо разделения на две части между кодом, который сигнализирует об ошибке1) и кодом, который обрабатывает её2), система условий разделяет ответственность на три части – сигнализация условия, его обработка и перезапуск. В этой главе я опишу, как можно использовать условия в гипотетическом приложении для анализа журнальных файлов. Вы увидите, как можно использовать систему условий, чтобы дать возможность низкоуровневой функции обнаружить проблему в процессе разбора журнального файла и сигнализировать об ошибке, коду на промежуточном уровне предоставить несколько возможных путей исправления такой ошибки и коду на высшем уровне приложения определить политику для выбора стратегии исправления.

Для начала я введу некоторую терминологию: ошибки, как я буду использовать этот термин, есть последствие закона Мёрфи. Если может случиться что-то плохое, оно случится: файл, который ваша программа должна прочесть, будет потерян, диск, на который вам надо записать, будет полон, сервер, с которым вы общаетесь, упадёт или исчезнет сеть. Если что-то из этих вещей случилось, это может помешать участку кода выполнить то, что вы хотите. Но это не программная ошибка: нет места в коде, которое вы можете исправить, чтобы сделать несуществующий файл существующим или освободить место на диске. Однако если оставшаяся часть программы зависит от действий, которые должны были быть сделаны, то вам лучше как-то разобраться с происшествием, иначе вы ошибку закладываете. Итак, ошибки не всегда бывают программными, но их игнорирование – это всегда ошибка в программе.

Что же означает обработать ошибку? В хорошо написанной программе каждая функция – это чёрный ящик, скрывающий внутреннее содержание своей работы. Программы таким образом строятся из функций разных уровней: функции на высшем уровне построены на функциях более низкого уровня и так далее. Эта иерархия функционирования выражается во время выполнения в форме стека вызовов: когда высший вызывает среднеднего, а он в свою очередь вызывает нижнего, то поток выполнения находится внизу, но он также проходит через средний и высший уровни, поэтому они все находятся в одном стеке вызовов.

Так как каждая функция является чёрным ящиком, связи между функциями – прекрасное место для работы с ошибками. Каждая функция – нижняя для примера – должна делать какую-то работу. Та, которая её непосредственно вызывает – средняя в нашем случае – рассчитывает на неё в своей работе. Однако ошибка, которая мешает ей сделать свою работу, подвергает всех вызывающих риску: средняя вызывала нижнюю, потому что ей нужна работа, которую нижняя делает; если эта работа не сделана, у средней проблемы. Но это означает, что у вызвавшей среднюю, высшей, тоже проблемы – и так далее вверх по стеку вызовов вплоть до верха программы. С другой стороны, так как каждая функция чёрный ящик, если какая-то функция в стеке вызовов может как-то сделать свою работу не смотря на проблемы у тех, кто пониже, то никакой функции повыше не надо знать, что проблемы вообще были – все эти функции заботятся только чтобы функции, которые они вызывают, как-то сделали работу, которую от них ждут.

В большинстве языков ошибки обрабатываются путём возврата из упавшей функции и передачи шанса вызывающей функции или всё исправить или упасть самой. Некоторые языки используют обычный механизм возврата из функций, в то время как языки с исключениями возвращают контроль через выброс или возбуждение исключения. Исключения – это огромное усовершенствование над использованием обычных возвратов функций, но обе схемы страдают от общего порока: пока ищется функция, которая может восстановиться, стек пустеет. Это приводит к тому, что код, который может всё восстановить, должен сделать это без контекста того, что код нижнего уровня пытался сделать, когда случилась ошибка.

Рассмотрим гипотетическую цепочку вызовов из высшей, средней, нижней функций. Если нижняя падает, и средняя не может всё поправить, дело передаётся в суд высшей инстанции. Для высшей, чтобы обработать ошибку, надо или сделать свою работу без помощи средней, или как-то всё изменить, чтобы вызов средней работал, и вызвать её снова. Первый вариант теоретически прост, но предполагает кучу лишнего кода – полное воплощение того, что, предполагалось, сделает средняя. И чем дальше стек освобождается, тем больше работы, которую следует переделать. Второй вариант – исправление ситуации, и ещё одна попытка – запутанный: для высшей, чтобы иметь возможность изменить ситуацию в мире так, чтобы второй вызов средней не закончился ошибкой в нижней, необходимо неподобающее ей знание внутреннего устройства обеих функций: средней и нижней, что противоречит идее, что каждая функция является чёрным ящиком.

Путь языка Лисп

Система обработки ошибок в языке Common Lisp даёт вам выход из этого тупика, позволяя вам отделить код, который исправляет ошибки, от кода, который решает, как их исправлять. Таким образом, вы можете поместить код восстановления в функции низкого уровня, на самом деле не обязывая использовать какуй-то конкретную стратегию восстановления, предоставляя решать это коду функций высшего уровня.

Чтобы почувствовать, как это работает, предположим, что вы пишете приложение, которое читает какой-то текстовый файл наподобие журнала Web-сервера. Где-то в вашем приложении будет ваша функция разбора отдельных журнальных записей. Давайте предположим, что вы напишете функцию parse-log-entry, которой будет передаваться строка, содержащая одну журнальную запись, и которая предположительно вернёт объект log-entry, представляющий эту запись. Эта функция будет вызываться из функции parse-log-file, которая читает журнальный файл полностью и возвращает список объектов, представляющий все записи в журнале.

Чтобы всё упростить, от функции parse-log-entry не будет требоваться разбирать неправильно сформированные записи. Однако она будет способна распознать, когда её ввод неправилен. Но что она должна делать, когда она определяет неправильный ввод? В языке C вы бы вернули специальное значение, чтобы обозначить, что была проблема. В языках Java или Python вы бы выбросили или возбудили исключение. В Common Lisp вы сигнализируете условие.

Условия

Условие – это объект, чей класс обозначает общую природу условия, а данные экземпляров класса несут информацию о деталях конкретных обстоятельств, которые приводят к сигнализации об условии.3) В нашей гипотетической программе анализа журнальных файлов вы можете определить класс условия malformed-log-entry-error, с помощью которого parse-log-entry будет сигнализировать, если предоставленные ему данные он не сможет разобрать.

Классы условий определяются макросом DEFINE-CONDITION, который работает точно так же, как DEFCLASS, исключая то, что суперклассом по умолчанию для классов, определённых с DEFINE-CONDITION, является CONDITION, а не STANDARD-OBJECT. Слоты определяются так же, и классы условий могут иметь единственное или множественное наследование от других классов, которые происходят от CONDITION. Однако, по историческим причинам от классов условий не требуется быть экземплярами STANDARD-OBJECT, так что некоторые из функций, которые вы используете, и классы которых созданы через DEFCLASS, не обязательно должны работать с условиями. В частности, слоты условий недоступны для SLOT-VALUE: вы должны задать или опцию :reader, или опцию :accessor для любого слота, чьё значение вы собираетесь использовать. Точно так же новые объекты для условий создаются путём вызова MAKE-CONDITION, а не MAKE-INSTANCE. MAKE-CONDITION инициализирует слоты нового условия, основываясь на полученных :initarg, но способа последующей настройки инициализации условия, аналогичного INITIALIZE-INSTANCE, не существует.4)

При использовании системы условий для обработки ошибок вы должны определять ваши условия как подклассы ERROR, который является в свою очередь подклассом CONDITION. Таким образом, вы можете определить условие malformed-log-entry-error со слотом для хранения аргумента, который был передан в parse-log-entry, вот так:

(define-condition malformed-log-entry-error (error)
  ((text :initarg :text :reader text))
)

Обработчики Условий

В parse-log-entry вы будете сигнализировать malformed-log-entry-error, если вы не сможете разобрать журнальную запись. Вы сигнализируете об ошибках функцией ERROR, которая вызывает низкоуровневую функцию SIGNAL и попадает в отладчик, если условие не обрабатывается. Можно вызвать ERROR двумя путями: послать ей уже сформированный объект условия или послать ей имя класса условия и любые аргументы инициализации, необходимые для построения нового условия, и она создаст его для вас. Первое иногда полезно для повторной сигнализации уже существующего объекта условия, а второе более лаконично. Итак, вы можете записать parse-log-entry в таком виде, опуская детали собственно разбора журнальной записи:

(defun parse-log-entry (text)
  (if (well-formed-log-entry-p text)
    (make-instance 'log-entry ...)
    (error 'malformed-log-entry-error :text text)
)
)

То, что происходит, когда сигнализируется ошибка, зависит от кода в стеке вызовов, стоящего выше parse-log-entry. Чтобы избежать попадания в отладчик, вы должны установить обработчик условия в одну из функций, предшествующих вызову parse-log-entry. Когда условие сигнализируется, механизм сигнализирования просматривает список активных обработчиков условий, ищя по классу условия обработчик, способный обработать сигнализируемое условие. Каждый обработчик условия состоит из спецификатора типа, обозначающего, какие типы условий он может обработать, и функции, которая получает единственный аргумент – условие. В каждый момент времени может быть несколько активных обработчиков этого условия на различных уровнях стека вызовов. Когда условие сигнализируется, механизм сигнализирования находит последний установленный обработчик, чей спецификатор типа совместим с сигнализируемым условием и вызывает его функцию, передавая ей объект условия.

Функция обработки может выбрать, обрабатывать ли ей условие. Функция может отказаться обрабатывать условие, просто нормально завершившись, в этом случае управление возвращается функции SIGNAL, которая будет искать следующий установленный обработчик с подходящим спецификатором типа. Для обработки условия функция обязана передать управление минуя функцию SIGNAL посредством нелокального выхода. В следующей секции вы увидите, как обработчик может выбрать, куда передать контроль. Однако многие обработчики условий просто хотят опустошить стек до того места, где они были установлены, и затем запустить некоторый код. Макрос HANDLER-CASE как раз устанавливает такой тип обработчика. Базовая форма HANDLER-CASE такова:

(handler-case expression
  error-clause*
)

где каждая конструкция error-clause представляет собой следующую форму:

(condition-type ([var]) code)

Если выражение expression завершается нормально, тогда его значение возвращается из конструкции HANDLER-CASE. Тело HANDLER-CASE должно быть одним выражением, но вы можете использовать PROGN для объединения нескольких выражений в одну форму. Если всё же выражение сигнализирует условие, которое является одним из типов condition-type, указанных в одной из секций error-clause, то код из соответствующей секции будет выполнен, и его значение будет возвращено из HANDLER-CASE. Если указана переменная var, она будет именем переменной, содержащей объект условия при выполнении кода обработчика. Если код не нуждается в доступе к объекту условия, вы можете опустить имя переменной.

Например, одним из способов обработки условия malformed-log-entry-error, сигнализируемого из функции parse-log-entry в вызывающую её функцию parse-log-file, был бы пропуск неправильной записи. В следующей функции выражение HANDLER-CASE вернёт либо имя, возвращаемое parse-log-entry, либо NIL, если malformed-log-entry-error сигнализируется. Слово it во фразе collect it цикла LOOP является ещё одним ключевым словом LOOP, которое ссылается на значение последнего выполненного условия, в данном случае – на переменную entry).

(defun parse-log-file (file)
  (with-open-file (in file :direction :input)
    (loop for text = (read-line in nil nil) while text
       for entry = (handler-case (parse-log-entry text)
                     (malformed-log-entry-error () nil))

       when entry collect it
)
)
)

Когда функция parse-log-entry завершается нормально, её значение присваивается переменной entry и затем накапливается циклом LOOP. Но если функция parse-log-entry сигнализирует ошибку malformed-log-entry-error, то обработчик ошибки вернёт NIL, который не будет сохранён.

Обработка исключений в стиле языка Java

|HANDLER-CASE в языке Common Lisp является ближайшим аналогом стиля обработки исключений в языках Java и Python. То, что вы можете записать на языке Java как

try {
 doStuff();
 doMoreStuff();
} catch (SomeException se) {
 recover(se);
}

или на языке Python как

try:
 doStuff()
 doMoreStuff()
except SomeException, se:
 recover(se)

в языке Common Lisp вы пишите так:

(handler-case
    (progn
      (do-stuff)
      (do-more-stuff)
)

  (some-exception (se) (recover se))
)

|

Эта версия parse-log-file имеет один серьёзный недостаток: она делает слишком много всего. Как предполагает её имя, работа parse-log-file заключается в разборе файла и выдаче списка объектов log-entry; если это невозможно, не её дело решать что сделать взамен. Что если вы захотите использовать parse-log-file в приложении, которое захочет сказать пользователю, что журнальный файл повреждён или в таком, которое захочет восстановить неправильную запись путём исправления её и разбора снова? Или приложению достаточно пропускать их до тех пор, пока не накопится определённое количество повреждённых записей.

Вы можете попытаться решить проблему, переместив HANDLER-CASE в функцию высшего уровня. Однако тогда у вас не будет способа воплотить текущую политику пропуска отдельных записей – когда ошибка будет просигнализированна, стек будет опустошён вплоть до функции высшего уровня, разбор журнального файла будет вообще покинут. Что вам надо, так это способ предоставить текущую стратегию восстановления без требования всегда её использовать.

Перезапуск

Система условий позволяет вам это делать через разделение кода обработки ошибок на две части. Вы помещаете код, который собственно исправляет ошибки, в перезапуски (restarts), и обработчик условия может затем обработать условие, запустив подходящий вариант. Код перезапуска можно положить в средне- или низко-уровневую функцию, такую parse-log-file или parse-log-entry, переместив обработчик условия на высший уровень в приложении.

Для изменения parse-log-file, чтобы она устанавливала перезапуск вместо обработчика условия, вы можете сменить HANDLER-CASE на RESTART-CASE. Форма RESTART-CASE довольно похожа на HANDLER-CASE за исключением того, что имена перезапусков, это просто имена, не обязательно имена типов условия. В общем, имя перезапуска должно описывать действие, которое перезапуск совершает. В parse-log-file вы можете вызвать перезапуск skip-log-entry так как он делает именно "пропустить-журнальную-запись". Новая версия будет выглядеть так:

(defun parse-log-file (file)
  (with-open-file (in file :direction :input)
    (loop for text = (read-line in nil nil) while text
       for entry = (restart-case (parse-log-entry text)
                     (skip-log-entry () nil))

       when entry collect it
)
)
)

Если вы вызовете эту версию parse-log-file на журнальном файле, содержащем повреждённую запись, она не обработает ошибку напрямую; вы попадёте в отладчик. Однако там, среди различных представленных перезапусков от отладчика будет один, называемый skip-log-entry, который, если вызовете его, продолжит выполнение parse-log-file дальше в прежнем режиме. Для избежания попадания в отладчик, вы можете установить обработчик условия, который вызовет перезапуск skip-log-entry автоматически.

Преимущество установки перезапуска вместо обработки ошибки напрямую в parse-log-file в том, что это даёт возможность использовать parse-log-file в большем количестве ситуаций. Код высшего уровня, который вызывает parse-log-file, не обязан вызывать перезапуск skip-log-entry. Он может выбрать обработку ошибки на высшем уровне. Или, как я покажу в следующей секции, вы можете добавить перезапуск в parse-log-entry, чтобы предоставить другую стратегию исправления и затем обработчик условия может выбрать какую стратегию он хочет использовать.

Но перед этим вам надо увидеть как установить обработчик условия, который будет вызывать skip-log-entry перезапуск. Вы можете установить обработчик где угодно в цепочке вызовов ведущей к parse-log-file. Это может быть довольно высокий уровень в вашем приложении, не обязательно в функции, непосредственно вызывающей parse-log-file. Например, предположим, что главная точка входа в ваше приложение, это функция log-analyzer, которая ищет стопку журналов и анализирует их функцией analyze-log, которая в итоге приводит к вызову parse-log-file. Без какой-либо обработки ошибок, это может выглядеть так:

(defun log-analyzer ()
  (dolist (log (find-all-logs))
    (analyze-log log)
)
)

Работа analyze-log – это вызвать прямо или непрямо parse-log-file и затем сделать что-то с возвращённым списком журнальных записей. Сверхпростая версия может выглядеть так:

(defun analyze-log (log)
  (dolist (entry (parse-log-file log))
    (analyze-entry entry)
)
)

где функция analyze-entry предположительно ответственна за извлечение заботящей вас информации про каждую журнальную запись и припрятывание её куда-нибудь.

Таким образом, путь от функции высшего уровня log-analyzer к parse-log-entry, которая собственно сигнализирует про ошибку, следующий:

Изображение

Предполагая, что вы всегда хотите пропускать неправильно сформированные записи, вы можете изменить эту функцию для установки обработчика условия, который вызывает перезапуск skip-log-entry для вас. Однако вы не можете использовать HANDLER-CASE для установки обработчика условия, потмоу что тогда стек будет опустошён до функции, где HANDLER-CASE появляется. Вместо этого, вам надо использовать макрос нижнего уровня HANDLER-BIND. Основная форма HANDLER-BIND следующая:

(handler-bind (binding*) form*)

где каждая привязка (binding) представляет собой список из типа услоия и обрабатывающей функции с одним аргументом. После провязок с обработчиками, тело HANDLER-BIND может содержать произвольное число форм. В отличие от кода обработчика в HANDLER-CASE, код обработчика должен быть FIXME функцией-объектом и должен принимать единственный аргумент. Более важным различием между HANDLER-BIND и HANDLER-CASE является то, что функция обработки привязанная через HANDLER-BIND будет запущена без опустошения стека – поток контроля будет всё ещё в вызове parse-log-entry если эта функция вызвана. Вызов INVOKE-RESTART найдёт и вызовет последний связанный перезапуск с данным именем. Таким образом вы можете добавить обработчик к log-analyzer который будет вызывать skip-log-entry перезапуск, установленный в parse-log-file таким образом:5)

(defun log-analyzer ()
  (handler-bind ((malformed-log-entry-error
                  #'(lambda (c)
                      (invoke-restart 'skip-log-entry)
)
)
)

    (dolist (log (find-all-logs))
      (analyze-log log)
)
)
)

В этом HANDLER-BIND обработчик является безымянной функцией, которая вызывает перезапуск skip-log-entry. Вы также могли бы определить функцию с именем, которая делала бы тоже самое и связать всё с ней. Фактически это обычная практика, когда определение перезапуска является опрделением функции с тем же именем и получающую единственный аргумент, условие, которая вызывает одноимённый перезапуск. Такие функции называются функциями перезапуска. Можно определить функцию перезапуска skip-log-entry так:

(defun skip-log-entry (c)
  (invoke-restart 'skip-log-entry)
)

Затем вы могли бы изменить определение log-analyzer на такое:

(defun log-analyzer ()
  (handler-bind ((malformed-log-entry-error #'skip-log-entry))
    (dolist (log (find-all-logs))
      (analyze-log log)
)
)
)

Как уже было сказано, функция перезапуска skip-log-entry полагает, что перезапуск skip-log-entry уже был установлен. Если malformed-log-entry-error просигнализирован кодом, вызванным из log-analyzer без уже установленного skip-log-entry, вызов INVOKE-RESTART будет сигнализировать CONTROL-ERROR, когда не сможет обнаружить перезапуск skip-log-entry. Если вы хотите допустить возможность того, чтобы malformed-log-entry-error могло быть просигнализировано из кода в котором перезапуск skip-log-entry не установлен, то вы можете изменить функцию skip-log-entry таким образом:

(defun skip-log-entry (c)
  (let ((restart (find-restart 'skip-log-entry)))
    (when restart (invoke-restart restart))
)
)

FIND-RESTART ищет перезапуск с данным именем и возвращает объект, представляющий перезапуск, если перезапуск найден или NIL если нет. Вы можете вызвать перезапуск путём посылки перезапуск-объекта к INVOKE-RESTART. Таким образом, когда skip-log-entry привязывается внутри HANDLER-BIND, она будет обрабатывать условие путём вызова перезапуска skip-log-entry, если тот доступен или, в противном случае, нормально завершится, давая другим обработчикам условия, привязанным выше по стеку, шанс таки обработать условие.

Предоставление множественных перезапусков

Так как перезапуски должны быть прямо вызваны, чтобы был какой-то эффект, вы можете определить несколько перезапусков, предоставляющих различную стратегию исправления. Как я упоминал ранее, не все приложения для разбора журналов будут обязательно хотеть пропускать неправильные записи. Некоторые приложения могут захотеть, чтобы parse-log-file включала специальный тип объекта для представления неправильной записи в списке log-entry объектов; другие приложения могут иметь несколько путей для восстановления неправильной записи и могут хотеть иметь способ отправки исправленной записи назад в parse-log-entry.

Чтобы позволить существовать более сложным протоколам исправления, перезапуски могут получать произвольные аргументы, которые передаются в вызове INVOKE-RESTART. Вы можете предоставить поддержку для обеих стратегий исправления, которые я упомянул, путём добавления двух перезапусков к parse-log-entry, каждый из которых получает единственный аргумент. Первый просто возвращает значение, которое получил, как значение всего parse-log-entry, в то время как другой пытается разобрать свой аргумент на месте оригинальной журнальной записи.

(defun parse-log-entry (text)
  (if (well-formed-log-entry-p text)
    (make-instance 'log-entry ...)
    (restart-case (error 'malformed-log-entry-error :text text)
      (use-value (value) value)
      (reparse-entry (fixed-text) (parse-log-entry fixed-text))
)
)
)

Имя USE-VALUE является стандартным именем для такого типа перезапуска. Common Lisp определяет функцию для USE-VALUE аналогично skip-log-entry функции, только что вами определённой. Таким образом, если вы хотели изменить политику по отношению к неправильным записям на ту, которая создана экземпляром malformed-log-entry, вы могли бы сменить log-analyzer на такую функцию (предполагая существование malformed-log-entry класса с инициирующим аргументом :text):

(defun log-analyzer ()
  (handler-bind ((malformed-log-entry-error
                  #'(lambda (c)
                      (use-value
                       (make-instance 'malformed-log-entry :text (text c))
)
)
)
)

    (dolist (log (find-all-logs))
      (analyze-log log)
)
)
)

Вы могли бы точно также поместить эти новые перезапуски в parse-log-file вместо parse-log-entry. Хотя вы, вообще-то, хотите поместить перезапуски в код по-возможности самого нижнего уровня. Не было бы, однако, правильным перемещать перезапуск skip-log-entry в parse-log-entry, так как это может привести к возвращению иногда 'NIL от parse-log-entry в качестве результата нормального завершения, а вы начинали всё с тем, чтобы таких вещей избежать. И это была бы одинаково плохая идея убрать перезапуск skip-log-entry исходя из теории, что обработчик условия смог бы достичь того же эффекта через вызов перезапуск use-value с NIL в качестве аргумента; это потребовало бы от обработчика условия знания внутренней работы parse-log-file. Таким образом skip-log-entry – правильно абстрагированная часть API журнального разбора.

Другие применения условий

В то время, как условия в основном используются для обработки ошибок, они также могут быть использованы для других целей – вы можете применить условия, обработчики условий и перезапуски для конструирования различных протоколов между низко и высокоуровневым кодом. Ключом для понимания возможностей условий является понимание того, что просто сигнализация условия не влияет на поток контроля.

Примитивная сигнальная функция SIGNAL воплощает механизм поиска применимого обработчика условия и вызывает его обрабатывающую функцию. Причина, почему обработчик может отказаться обрабатывать условие и нормально завершиться, это потому что вызов функции обработчика, это обычный вызов функции – когда обработчик завершается, контроль передаётся обратно к SIGNAL, которая затем ищет другой, ранее привязанный обработчик, который может обработать условие. Если SIGNAL исчерпает обработчики условия до того, как условие будет обработано, она также завершается нормально.

Ранее использованная вами функция ERROR вызывает SIGNAL. Если ошибка обрабатывается обработчиком условия, который передаёт управление через HANDLER-CASE или через вызов перезапуска, тогда вызов SIGNAL никогда не завершится. Но если SIGNAL завершается, ERROR вызывает отладчик путём вызова функции, сохранённой в *DEBUGGER-HOOK*. Таким образом вызов ERROR никогда не сможет завершиться нормально; условие должно быть обработано или обработчиком условия или в отладчике.

Другая сигнализирующая условие функция WARN, представляет пример протокола ещё одного типа, построенный на системе условий. Подобно ERROR, WARN вызывает SIGNAL для сигнализации условия. Но, если SIGNAL завершается, WARN не вызывает отладчик – она печатает условие в *ERROR-OUTPUT* и возвращает NIL, позволяя своему вызывающему продолжать работу. WARN также устанавливает перезапуск MUFFLE-WARNING, FIXME обёртку вызова SIGNAL, которая может быть использована обработчиком условия, чтобы сделать возврат из WARN без печатания чего либо. Функция MUFFLE-WARNING перезапуска, ищет и вызывает одноимённый перезапуск, сигнализируя CONTROL-ERROR, если такого перезапуска нет. Конечно условие, сигнализируемое WARN, также может быть обработано другим путём – обработчик условия может

"поспособствовать"

, чтобы предупреждение об ошибке обрабатывалось, как если бы ошибка действительно была.

Например в приложении для разбора журналов, если журнальная запись немного неправильна, но всё же поддаётся разбору, вы могли бы указать parse-log-entry разобрать немного повреждённую запись, но сигнализировать WARN, когда она будет делать это. Затем большее приложение может выбрать либо позволить предупреждению быть напечатанным, либо скрыть предупреждение, либо рассматривать предупреждение как ошибку, исправляя ситуацию как это делалось при malformed-log-entry-error.

Третья сигнализирующая ошибки функция, CERROR, представляет ещё один протокол. Подобно ERROR, CERROR сбросит вас в отладчик, если условие, которая она сигнализирует, не будет обработано. Однако как и WARN, она устанавливает перезапуск перед сигнализацией условия. Перезапуск, CONTINUE, приведёт к нормальному завершению CERROR – если перезапуск был вызван обработчиком условия, он вообще сохранит вас от попадания в отладчик. FIXME В противном случае, вы можете использовать перезапуск, как только окажетесь в отладчике для продолжения вычислений сразу после вызова CERROR. Функция CONTINUE находит и вызывает CONTINUE перезапуск, если он доступен, и возвращает NIL в противном случае.

Вы также можете конструировать ваши собственные протоколы поверх SIGNAL – везде, где низкоуровневый код нуждается в передаче информации назад по стеку вызовов в высокоуровневый код, механизм условий является подходящим механизмом для использования. Но для большинства целей один из стандартных протоколов ошибок или предупреждений должен подойти.

Вы будете использовать систему условий в будущих практических главах, как в виде обычной обработки ошибок, так и, в 25-й главе, для помощи в обработке мудрёного особого случая при разборе ID3 файлов. К сожалению, это судьба обработки ошибок, всегда идти мелким шрифтом в программных текстах – надлежащая обработка ошибок или отсутствие таковой, является часто наибольшим отличием иллюстративного кода и утяжелённого кода промышленного качества. Хитрость написания последнего относится больше к принятию особого строгого образа мышления о программном обеспечении, чем к деталями конструкции языка. Так что если вашей целью является написание такого типа программ, то вы найдёте систему условий в Common Lisp отличным инструментом для написания надёжного кода и прекрасно подходящей для стиля последовательных улучшений (incremental development style).

Написание надёжных программ
Для информации по написанию надёжных программ нет ничего лучше, чем начать с поиска книги Гленфорда Меерса "Надёжность программного обеспечания" Software Reliability Glenford J. Meyers (John Wiley & Sons, 1976). Написанное Bertrand Meyer про Design By Contract также показывает полезный путь в размышлениях про правильность программ. Например, смотрите главы 11 и 12 его Object-Oriented Software Construction (Prentice Hall, 1997). Запомните, однако, что Bertrand Meyer является изобретателем Eiffel, статически типизированного крепостнического и дисциплинарного языка из школы Algol/Ada. Хотя у него есть много умных мыслей про объектную ориентированность и программную надёжность, всё же существует довольно большой разрыв между его видением программирования и путём Лиспа. Наконец, для прекрасного обзора большинства проблем, окружающих построение отказоустойчивых систем, смотрите главу 3 из классической Transaction Processing: Concepts and Techniques (Morgan Kaufmann, 1993) от Jim Gray и Andreas Reuter.

В следующей главе я дам быстрый обзор некоторых из 25 специальных операторов, которых у вас пока не было шанса использовать, по крайней мере прямо.

1)выбрасывает (throw) или вызывает (raise) исключение в терминах Java/Python.
2)ловит (catch) исключение в терминах Java/Python
3)В этом аспекте условия очень похожи на исключения в Java или Python, разве что не все условия представляют ошибку или исключительную ситуацию.
4)В некоторых реализациях языка Common Lisp условия определены как подкласс от STANDARD-OBJECT, в этом случае SLOT-VALUE, MAKE-INSTANCE, и INITIALIZE-INSTANCE будут работать, но на это нельзя полагаться из-за непереносимости.
5)Компилятор может возмутиться если параметр нигде не используется. Вы можете подавить это предупреждение, добавив объявление (declare (ignore c)) как первое выражение в тело LAMBDA.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru