LISP - 錯誤處理



在 Common LISP 術語中,異常被稱為條件。

事實上,條件比傳統程式語言中的異常更通用,因為條件表示任何可能影響函式呼叫棧各個級別的事件,無論是錯誤還是非錯誤。

LISP 中的條件處理機制以這樣一種方式處理此類情況:條件用於發出警告(例如列印警告),而呼叫棧上的上層程式碼可以繼續其工作。

LISP 中的條件處理系統包含三個部分:

  • 發出條件
  • 處理條件
  • 重新啟動過程

處理條件

讓我們以一個處理因除以零引起的條件的示例來解釋這裡概念。

處理條件需要執行以下步驟:

  • 定義條件 - “條件是一個物件,其類指示條件的總體性質,其例項資料攜帶有關導致發出條件的特定情況的詳細資訊”。

    define-condition 宏用於定義條件,其語法如下:

(define-condition condition-name (error)
   ((text :initarg :text :reader text))
)
  • 新的條件物件使用 MAKE-CONDITION 宏建立,它根據:initargs引數初始化新條件的槽。

在我們的示例中,以下程式碼定義了條件:

(define-condition on-division-by-zero (error)
   ((message :initarg :message :reader message))
)
  • 編寫處理程式 - 條件處理程式是用於處理已發出的條件的程式碼。它通常編寫在調用出錯函式的高階函式之一中。當發出條件時,發出機制會根據條件的類搜尋合適的處理程式。

    每個處理程式包含:

    • 型別說明符,指示它可以處理的條件型別
    • 一個函式,它接受一個引數:條件

    當發出條件時,發出機制會找到與條件型別相容的最近建立的處理程式,並呼叫其函式。

    handler-case建立條件處理程式。handler-case的基本形式:

(handler-case expression error-clause*)

其中,每個錯誤子句都具有以下形式:

condition-type ([var]) code)
  • 重啟階段

    這是實際從錯誤中恢復程式的程式碼,然後條件處理程式可以透過呼叫適當的重啟來處理條件。重啟程式碼通常放置在中級或低階函式中,而條件處理程式則放置在應用程式的上層。

    handler-bind宏允許您提供重啟函式,並允許您在不展開函式呼叫棧的情況下繼續執行低階函式。換句話說,控制流仍然在低階函式中。

    handler-bind的基本形式如下:

(handler-bind (binding*) form*)

其中每個繫結都是以下列表:

  • 條件型別
  • 一個帶有單個引數的處理程式函式

invoke-restart宏查詢並呼叫最近繫結的重啟函式,並使用指定的名稱作為引數。

您可以有多個重啟。

示例

在此示例中,我們透過編寫一個名為 division-function 的函式來演示上述概念,如果除數引數為零,該函式將建立錯誤條件。我們有三個匿名函式,它們提供了三種退出方法:返回一個值 1,傳送一個除數 2 並重新計算,或返回 1。

建立一個名為 main.lisp 的新原始碼檔案,並在其中鍵入以下程式碼。

(define-condition on-division-by-zero (error)
   ((message :initarg :message :reader message))
)
   
(defun handle-infinity ()
   (restart-case
      (let ((result 0))
         (setf result (division-function 10 0))
         (format t "Value: ~a~%" result)
      )
      (just-continue () nil)
   )
)
     
(defun division-function (value1 value2)
   (restart-case
      (if (/= value2 0)
         (/ value1 value2)
         (error 'on-division-by-zero :message "denominator is zero")
      )

      (return-zero () 0)
      (return-value (r) r)
      (recalc-using (d) (division-function value1 d))
   )
)

(defun high-level-code ()
   (handler-bind
      (
         (on-division-by-zero
            #'(lambda (c)
               (format t "error signaled: ~a~%" (message c))
               (invoke-restart 'return-zero)
            )
         )
         (handle-infinity)
      )
   )
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'return-value 1)
         )
      )
   )
   (handle-infinity)
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'recalc-using 2)
         )
      )
   )
   (handle-infinity)
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'just-continue)
         )
      )
   )
   (handle-infinity)
)

(format t "Done."))

執行程式碼時,它將返回以下結果:

error signaled: denominator is zero
Value: 1
error signaled: denominator is zero
Value: 5
error signaled: denominator is zero
Done.

除了上面討論的“條件系統”之外,Common LISP 還提供了各種可用於發出錯誤的函式。但是,發出錯誤後的錯誤處理方式取決於實現。

LISP 中的錯誤發出函式

下表提供了常用函式,用於發出警告、中斷、非致命和致命錯誤。

使用者程式指定錯誤訊息(字串)。這些函式處理此訊息,可能會或可能不會將其顯示給使用者。

錯誤訊息應透過應用format函式構建,開頭和結尾都不應包含換行符,並且不需要指示錯誤,因為 LISP 系統將根據其首選樣式處理這些錯誤。

序號 函式和描述
1

error format-string &rest args

它發出致命錯誤。無法從這種錯誤中繼續;因此,error 永遠不會返回到其呼叫者。

2

cerror continue-format-string error-format-string &rest args

它發出錯誤並進入偵錯程式。但是,它允許程式在解決錯誤後從偵錯程式中繼續執行。

3

warn format-string &rest args

它列印錯誤訊息,但通常不會進入偵錯程式

4

break &optional format-string &rest args

它列印訊息並直接進入偵錯程式,不允許程式錯誤處理功能攔截。

示例

在此示例中,factorial 函式計算數字的階乘;但是,如果引數為負數,則會引發錯誤條件。

建立一個名為 main.lisp 的新原始碼檔案,並在其中鍵入以下程式碼。

(defun factorial (x)
   (cond ((or (not (typep x 'integer)) (minusp x))
      (error "~S is a negative number." x))
      ((zerop x) 1)
      (t (* x (factorial (- x 1))))
   )
)

(write(factorial 5))
(terpri)
(write(factorial -1))

執行程式碼時,它將返回以下結果:

120
*** - -1 is a negative number.
廣告