Обычный результат вызова ошибки — завершение выполняемой команды и немедленный возврат в цикл команд редактора Emacs. Вы можете организовать перехват ошибок, возникающих в части вашей программы, установив обработчик ошибок с помощью специальной формы condition-case. Простой пример выглядит так:
(condition-case nil
(delete-file filename)
(error nil))
Этот код удаляет файл с именем filename, перехватывая любую ошибку и возвращая nil, если ошибка возникает. (Для таких простых случаев можно использовать макрос ignore-errors; см. ниже.)
Конструкция condition-case часто используется для перехвата предсказуемых ошибок, например, невозможности открыть файл при вызове insert-file-contents. Она также используется для перехвата совершенно непредсказуемых ошибок, например, когда программа вычисляет выражение, введённое пользователем.
Второй аргумент condition-case называется защищаемой формой. (В примере выше защищаемой формой является вызов delete-file.) Обработчики ошибок вступают в силу, когда эта форма начинает выполнение, и деактивируются, когда эта форма возвращает результат. Они остаются активными в течение всего промежуточного времени. В частности, они активны во время выполнения функций, вызванных этой формой, в их подпрограммах и так далее. Это хорошо, поскольку, строго говоря, ошибки могут быть вызваны только примитивами Lisp (включая signal и error), вызванными защищаемой формой, а не самой защищаемой формой.
Аргументы после защищаемой формы — это обработчики. Каждый обработчик перечисляет одно или несколько имён условий (которые являются символами), чтобы указать, какие ошибки он будет обрабатывать. Символ ошибки, указанный при вызове ошибки, также определяет список имён условий. Обработчик применяется к ошибке, если у них есть общие имена условий. В примере выше есть один обработчик, и он указывает одно имя условия — error, которое охватывает все ошибки.
Поиск подходящего обработчика проверяет все установленные обработчики, начиная с самого последнего установленного. Таким образом, если две вложенные формы condition-case предлагают обработать одну и ту же ошибку, внутренняя из них получает приоритет.
Если ошибка обрабатывается некоторой формой condition-case, это обычно предотвращает запуск отладчика, даже если debug-on-error указывает, что эта ошибка должна вызывать отладчик.
Если вы хотите иметь возможность отлаживать ошибки, перехватываемые condition-case, установите переменную debug-on-signal в значение, отличное от nil. Вы также можете указать, что конкретный обработчик должен сначала запустить отладчик, записав debug среди условий, например:
(condition-case nil
(delete-file filename)
((debug error) nil))
Эффект debug здесь заключается лишь в том, чтобы не позволить condition-case подавить вызов отладчика. Любая ошибка вызовет отладчик только в том случае, если debug-on-error и другие обычные механизмы фильтрации указывают на это. См. Вход в отладчик при ошибке.
Макрос condition-case-unless-debug предоставляет другой способ обработки отладки таких форм. Он ведёт себя точно так же, как condition-case, если только переменная debug-on-error не имеет значение, отличное от nil, в этом случае он вообще не обрабатывает никакие ошибки.
Как только Emacs решает, что определённый обработчик обрабатывает ошибку, он передаёт управление этому обработчику. Для этого Emacs отменяет все привязки переменных, сделанные конструкциями привязки, из которых происходит выход, и выполняет очистки всех форм unwind-protect, из которых происходит выход. Когда управление достигает обработчика, тело обработчика выполняется обычным образом.
После выполнения тела обработчика выполнение возвращается из формы condition-case. Поскольку защищаемая форма полностью завершается до выполнения обработчика, обработчик не может возобновить выполнение в точке ошибки и не может исследовать привязки переменных, сделанные внутри защищаемой формы. Всё, что он может сделать, — это выполнить очистку и продолжить.
Вызов и обработка ошибок имеют некоторое сходство с throw и catch (см. Явные нелокальные выходы: catch и throw), но это совершенно разные механизмы. Ошибка не может быть перехвачена catch, а throw не может быть обработана обработчиком ошибок (хотя использование throw при отсутствии подходящего catch вызывает ошибку, которая может быть обработана).
Эта специальная форма устанавливает обработчики ошибок обработчики вокруг выполнения защищаемой-формы. Если защищаемая-форма выполняется без ошибок, возвращаемое ею значение становится значением формы condition-case (при отсутствии обработчика успеха; см. ниже). В этом случае condition-case не имеет эффекта. Форма condition-case имеет значение, когда во время защищаемой-формы возникает ошибка.
Каждый из обработчиков представляет собой список вида (условия тело…). Здесь условия — это имя условия ошибки, которое должно обрабатываться, или список имён условий (которые могут включать debug, чтобы позволить отладчику запуститься до обработчика). Имя условия t соответствует любому условию. тело — это одно или несколько выражений Lisp, которые выполняются, когда этот обработчик обрабатывает ошибку. Вот примеры обработчиков:
(error nil) (arith-error (message "Деление на ноль")) ((arith-error file-error) (message "Либо деление на ноль, либо ошибка открытия файла"))
Каждая возникающая ошибка имеет символ ошибки, который описывает, что это за ошибка, и который также описывает список имён условий (см. Символы ошибок и имена условий). Emacs ищет среди всех активных форм condition-case обработчик, который указывает одно или несколько из этих имён условий; самая внутренняя подходящая condition-case обрабатывает ошибку. Внутри этой condition-case первый подходящий обработчик обрабатывает ошибку.
После выполнения тела обработчика condition-case возвращает управление正常льно, используя значение последней формы в теле обработчика в качестве общего результата.
Аргумент var — это переменная. condition-case не привязывает эту переменную при выполнении защищаемой-формы, только когда обрабатывает ошибку. В этот момент он локально привязывает var к описанию ошибки, которое представляет собой список, содержащий подробности ошибки. Описание ошибки имеет вид (символ-ошибки . данные). Обработчик может ссылаться на этот список, чтобы решить, что делать. Например, если ошибка связана с невозможностью открыть файл, имя файла является вторым элементом данных — третьим элементом описания ошибки.
Если var равен nil, это означает, что переменная не привязывается. Тогда символ ошибки и связанные с ней данные недоступны обработчику.
В качестве особого случая один из обработчиков может быть списком вида (:success тело…), где тело выполняется с var (если не nil), привязанной к возвращаемому значению защищаемой-формы, когда это выражение завершается без ошибки.
Иногда необходимо повторно вызвать сигнал, перехваченный condition-case, чтобы его перехватил какой-либо внешний обработчик. Вот как это сделать:
(signal (car err) (cdr err))
где err — это переменная описания ошибки, первый аргумент condition-case, условие ошибки которого вы хотите повторно вызвать. См. Определение signal.
Эта функция возвращает строку сообщения об ошибке для данного описателя ошибки. Она полезна, если вы хотите обработать ошибку, выводя обычное сообщение об ошибке для этой ошибки. См. Определение signal.
Вот пример использования condition-case для обработки ошибки, возникающей при делении на ноль. Обработчик отображает сообщение об ошибке (но без звукового сигнала), а затем возвращает очень большое число.
(defun safe-divide (dividend divisor)
(condition-case err
;; Защищаемая форма.
(/ dividend divisor)
;; Обработчик. (arith-error ; Условие. ;; Вывести обычное сообщение для этой ошибки. (message "%s" (error-message-string err)) 1000000))) ⇒ safe-divide
(safe-divide 5 0)
-| Арифметическая ошибка: (arith-error)
⇒ 1000000
Обработчик указывает имя условия arith-error, поэтому он будет обрабатывать только ошибки деления на ноль. Другие виды ошибок не будут обработаны (этой condition-case). Таким образом:
(safe-divide nil 3)
error→ Неправильный тип аргумента: number-or-marker-p, nil
Вот condition-case, которая перехватывает все виды ошибок, включая ошибки от error:
(setq baz 34)
⇒ 34
(condition-case err
(if (eq baz 35)
t
;; Это вызов функции error.
(error "Крысы! Переменная %s была %s, а не 35" 'baz baz))
;; Это обработчик; это не форма.
(error (princ (format "Ошибка была: %s" err))
2))
-| Ошибка была: (error "Крысы! Переменная baz была 34, а не 35")
⇒ 2
Эта конструкция выполняет тело, игнорируя любые ошибки, возникающие во время его выполнения. Если выполнение проходит без ошибок, ignore-errors возвращает значение последней формы в теле; в противном случае возвращает nil.
Вот пример из начала этого подраздела, переписанный с использованием ignore-errors:
(ignore-errors (delete-file filename))
Этот макрос подобен ignore-errors, но игнорирует только указанное условие ошибки.
(ignore-error end-of-file
(read ""))
условие также может быть списком условий ошибок.
Этот макрос является более мягкой версией ignore-errors. Вместо полного подавления ошибок он преобразует их в сообщения. Он использует строку формат для форматирования сообщения. формат должен содержать одну последовательность с ‘%’; например, "Ошибка: %S". Используйте with-demoted-errors вокруг кода, который не должен вызывать ошибки, но должен быть устойчивым, если ошибка всё же возникнет. Обратите внимание, что этот макрос использует condition-case-unless-debug, а не condition-case.
Иногда мы хотим перехватить некоторые ошибки и записать информацию об условиях, в которых они возникли, например, полный обратный стек или текущий буфер. К сожалению, такая информация недоступна в обработчиках condition-case, потому что стек разворачивается до выполнения этого обработчика, поэтому обработчик выполняется в динамическом контексте condition-case, а не в том месте, где была вызвана ошибка. Для таких случаев вы можете использовать следующую форму:
Эта специальная форма выполняет тело, и если оно выполняется без ошибок, возвращаемое им значение становится значением формы handler-bind. В этом случае handler-bind не имеет эффекта.
обработчики должны быть списком элементов вида (условия обработчик), где условия — это имя условия ошибки, которое должно обрабатываться, или список имён условий, а обработчик должен быть формой, вычисление которой должно возвращать функцию. Как и в condition-case, имена условий являются символами.
Перед выполнением тела handler-bind вычисляет все формы обработчик и устанавливает эти обработчики активными на время вычисления тела. Когда вызывается ошибка, Emacs ищет среди всех активных форм condition-case и handler-bind обработчик, который указывает одно или несколько из этих имён условий. Когда самым внутренним подходящим обработчиком является тот, который установлен handler-bind, функция обработчик вызывается с одним аргументом, содержащим описание ошибки.
В отличие от того, что происходит с condition-case, обработчик вызывается в динамическом контексте, где произошла ошибка. Это означает, что он выполняется без отмены каких-либо привязок переменных или выполнения очисток unwind-protect, так что все эти динамические привязки всё ещё активны. Есть одно исключение: во время выполнения функции обработчик все обработчики ошибок между кодом, вызвавшим ошибку, и handler-bind временно приостанавливаются, что означает, что при вызове ошибки Emacs будет искать только активные формы condition-case и handler-bind, которые находятся внутри функции обработчик или вне текущего handler-bind. Также обратите внимание, что лексически связанные переменные (см. Лексическая привязка) не затрагиваются, поскольку они не имеют динамической протяжённости.
Как и любая обычная функция, обработчик может выйти нелокально, обычно через throw, или может вернуться正常льно. Если обработчик возвращается正常льно, это означает, что обработчик отклонил обработку ошибки, и поиск обработчика ошибок продолжается с того места, где остановился.
Например, если мы хотим вести журнал всех ошибок, возникающих во время выполнения определённого фрагмента кода, вместе с буфером, который активен в момент вызова ошибки, но без изменения поведения этого кода, мы можем сделать это так:
(handler-bind
((error
(lambda (err)
(push (cons err (current-buffer)) my-log-of-errors))))
формы-тела…)
Это зарегистрирует только те ошибки, которые не перехватываются внутри форм-тела…, другими словами, ошибки, которые “выходят” из форм-тела…, и это не помешает этим ошибкам передаваться окружающим обработчикам condition-case (или обработчикам handler-bind), поскольку приведённый выше обработчик возвращается正常льно.
Мы также можем использовать handler-bind для замены одной ошибки на другую, как в коде ниже, который превращает все ошибки типа user-error, возникающие во время выполнения форм-тела…, в обычные error:
(handler-bind
((user-error
(lambda (err)
(signal 'error (cdr err)))))
формы-тела…)
Мы можем получить почти тот же результат с condition-case:
(condition-case err
(progn формы-тела…)
(user-error (signal 'error (cdr err))))
но с той разницей, что когда мы (повторно) вызываем новую ошибку в handler-bind, динамическое окружение исходной ошибки всё ещё активно, что означает, например, что если мы войдём в отладчик в этот момент, он покажет нам полный обратный стек, включая точку, где мы вызвали исходную ошибку:
Отладчик вошёл--Ошибка Lisp: (error "Ой")
signal(error ("Ой"))
#f(lambda (err) [t] (signal 'error (cdr err)))((user-error "Ой"))
user-error("Ой")
…
eval((handler-bind ((user-error (lambda (err) …