制御構造

エラー関数

後述の例外関数 vl-catch-all-apply を使用していない際にエラーが起こった場合は、特定のエラー関数を呼び出してユーザーにメッセージを表示したりして事後処理を行うことができます。特に何も仕掛けていなければ、そこでプログラムの実行はエラー終了します。

エラー関数というのは、ある特定の関数を示しているのではなく、シンボル *error* にエラー処理を目的として代入されている関数を指します。エラー関数をプログラムから呼び出す時には下記のように書きますが、実際には、プログラムにエラーが発生した際にシステムが自動的に呼び出します。

AutoLISP関数
(*error* string)
string : 文字列
ユーザー定義可能なエラー処理関数です。
戻り値:エラー処理関数内で最後に評価された式

ユーザー定義のエラー関数が呼び出されるように指定するためには、*error* という名前の関数を再定義します。このことは、シンボル *error* にユーザーの関数を代入することを意味します。

(defun myError (msg)
    (princ "ユーザー定義のエラー関数::")
    (princ msg)
    (princ "::ERRNO = ")
    (princ (getvar "errno"))
    (princ)
)

(setq *error* myError)

上のコードを AutoCAD にロードすると myError 関数が定義され、シンボル *error* に関数が代入された状態になります。この状態でエラーを発生させてみると、ユーザー定義のエラー関数が呼ばれるようになります。

_$ (abcd) ⏎               ;存在しない関数を呼び出してみる
ユーザー定義のエラー関数::no function definition: ABCD::ERRNO = 2
_1$

エラー関数の中でシステム変数の ERRNO を調べると、エラーの種類を表す数値が得られます。ERRNO の意味については AutoCAD ヘルプからシステム変数「ERRNO」や「エラーコード」を参照してください。

上の例では行っていませんが、グローバル変数の *error* には他で設定したエラー関数が設定されているかもしれませんので、変更する場合は以前の値を保存しておいて、自身のエラー関数が不要になった場合は元に戻しておくことが正しい作法です。

エラー関数内には、一時的に変更したシステム変数をもとに戻したり、AutoCAD の UNDO コマンドでエラー前の状態に復帰したりなどの処理を含めることができます。

次の例は、ClashFunction 関数を実行すると0(ゼロ)による除算エラーが発生します。エラーが発生するとエラー関数に設定した myErrorFunc 関数が呼ばれ、それまで描画した線を消去し、システム変数を元に戻します。ここではローカル変数として *error* を宣言しているので、グローバル変数の *error* には影響を与えず、関数の実行が終わるとローカルな *error* 関数も無効になるようになっています。

(defun myErrorFunc (msg /)                ; ユーザー定義のエラー関数
    (command "._undo" "_e")               ;  1. UNDO コマンドで ClashFunction 関数実行前の
    (command "._U")                       ;     状態に図面データーベースを戻す
    (setvar "CMDECHO" old_CMDECHO)        ;  2. 変更していたシステム変数を元に戻す
    (setvar "BLIPMODE" old_BLIPMODE)
    (setvar "OSMODE" old_OSMODE)
    (prompt msg)                          ;  3. エラーメッセージを表示する
    (terpri)
    (princ)                               ; ここでプログラムは終了
)

(defun ClashFunction (/ old_CMDECHO old_BLIPMODE old_OSMODE *error*)
    (setq old_CMDECHO  (getvar "CMDECHO")
          old_BLIPMODE (getvar "BLIPMODE")
          old_OSMODE   (getvar "OSMODE")
          *error*      myErrorFunc       ; エラー関数をローカル変数にセット
    )
    (setvar "CMDECHO" 0)
    (setvar "BLIPMODE" 0)
    (setvar "OSMODE" 0)
    (command "._undo" "_be")             ; UNDOコマンド上、以降のコマンドをグループ化
    (command "._line" "-100.0,0.0" "100.0,0.0" "") ; 線を描画(図面データベースを変更)
    (command "._line" "0.0,-100.0" "0.0,100.0" "")
    (/ 1 0)                              ; エラーが発生 myErrorFunc 関数へ
    (command "._undo" "_e")              ; ここでは以降の式は評価されない
    (setvar "CMDECHO" old_CMDECHO)
    (setvar "BLIPMODE" old_BLIPMODE)
    (setvar "OSMODE" old_OSMODE)
)

エラー関数内では、エラー発生元のローカル変数にも自然にアクセスできます。

AutoCAD 2012 以降のエラー関数

AutoCAD 2012 以降のエラー関数には command 系関数とローカル変数に影響を及ぼす二つのモードがあります。一つはエラー関数内で command 関数系から command-s 関数しか使えないが従前どおりエラー発生元のローカル変数(スタック上のデータ)にアクセスできるもの。もう一つは、command 系関数の使用に制限は無いがグローバル変数を除いてエラー発生元のローカル変数にはアクセスできないものです。二つのモードは、それぞれ *push-error-using-stack* 関数と *push-error-using-command* 関数を使って切り替えますが、特にモードの選択が行われていなければ前者のモードがデフォルトになります。これらの関数を使ってモードを変更すると、以前のモードはシステム内部に保管されます。変更したエラー関数のモードを元に戻すには、*pop-error-mode* 関数を使用します。

  *push-error-using-stack* *push-error-using-command*
command 系関数 command-s 関数のみ 制限なし
グローバル変数 アクセス可 アクセス可
エラー発生元のローカル変数 アクセス可 アクセス不可
  デフォルトの動作モード  

command-s 関数は、エラー関数のモードとともに導入された関数で、従前の command 関数と異なる点は、ユーザーとの対話的な AutoCAD コマンドの処理は行えないが、高速で安定しています。command-s 関数はエラー関数以外の部分でも使用可能です。

AutoLISP関数
(*push-error-using-stack*)
ユーザー設定のエラー関数内でのエラー発生元のローカル変数を参照可能にする宣言をします。
戻り値:nil以外

ユーザー設定のエラー関数内から、エラー発生元のローカル変数を参照可能とする宣言をする関数です。ただし、command-s 関数しかエラー関数内で使用することができません。特にこの関数を使って宣言を行わなくても、デフォルトでこのモードで動作します。

AutoLISP関数
(*push-error-using-command*)
ユーザー設定のエラー関数内でのcommand関数の使用を宣言をします。
戻り値:nil以外

ユーザー設定のエラー関数内で従前の command 関数を使用するという宣言する関数です。ただし、エラー発生元のローカル変数にはアクセスできない制約があります。

AutoLISP関数
(*pop-error-mode*)
*push-error-using-command* または *push-error-using-stack* による宣言を元に戻します。
戻り値:nil以外

*push-error-using-command* 関数、または *push-error-using-stack* 関数による宣言を以前の状態に戻します。

これらの関数は、現在、シンボル *error* に代入されているエラー関数に作用しますので、エラー関数のセットとモードの宣言は次の順になります。どちらのモードを選ぶかによって、シンボル olderrFunc がローカル変数とできるのか、グローバル変数としなければならないのかが変わってきます。

(setq olderrFunc *error*
      *error* myErrorFunc
)
(*push-error-using-stack*) または (*push-error-using-command*)

<実行コード>

(setq *error* olderrFunc)
(*pop-error-mode*)

さて、このようなモードを使い分ける必要があるために、以前の AutoLISP のコードにエラー関数があった場合は書き換える必要があります。

エラー関数内でエラー発生元のローカル変数にアクセスしたいならば、エラー関数内の command 関数を command-s 関数に置き換えなければなりません。そして、以前のモード区別の無い AutoCADの バージョンでも動作するようにするためには、command-s 関数が使用できなければ、従前どおり command 関数を使用するようにしなければなりません。

一方、エラー関数内で command 関数の使用を優先するならば、*push-error-using-command* 関数で明示的にモードを宣言しなければなりません。こちらも AutoCAD のバージョンを考慮すると、わざわざ宣言が必要かどうかを調べるようにしなければなりません。

下は、command-s 関数が可能ならば使用するように書き換えたエラー関数の例です。

(defun myErrorFunc (msg /)
  (if command-s
    (progn (command-s "._undo" "_e") (command-s "._U"))
    (progn (command "._undo" "_e") (command "._U"))
  )
  (setvar "CMDECHO" old_CMDECHO)
  (setvar "BLIPMODE" old_BLIPMODE)
  (setvar "OSMODE" old_OSMODE)
  (prompt msg)
  (terpri)
  (princ)
)

例外

エラーの発生は、0 による除算などプログラムに原因があるものの他に、ユーザーが入力をキャンセルした場合、AutoCAD のバージョンによって仕様が異なる場合と、原因はさまざまです。AutoLISP は、エラーが発生したら基本的にプログラムはそこで終了です。ただ、それではあまりにプログラムが脆弱であったり単純なものになりかねないため、エラーを例外としてトラップし、実行を続けることができる関数が用意されています。

AutoLISP の例外の仕組みでは、エラーが発生する可能性のある式を渡して実行し、正常に実行されれば式の結果が、実行過程でエラーが発生すればエラーオブジェクトを返してくる関数を使います。このとき *error* 関数は実行されません。この関数の戻り値を調べることによって、エラー時のプログラムの継続について対応を決めることができます。

例外に関わる関数は次の三つです。vl-catch-all-apply 関数は apply 関数の例外バージョンと捉えることができます。

AutoLISP関数
(vl-catch-all-apply 'function list)
function : 関数
list : リスト
指定された関数を実行し、エラーが発生した場合はエラーオブジェクトを返しますます。正常に実行された場合は、関数の戻り値を返します。
戻り値 : function の結果の値、またはエラーオブジェクト
AutoLISP関数
(vl-catch-all-error-p item)
item : アトム、リスト
引数がエラーオブジェクトかどうかを調べます。
戻り値 : nil、またはnil 以外
AutoLISP関数
(vl-catch-all-error-message error-obj)
error-obj : エラーオブジェクト
エラーオブジェクトから内容を表す文字列を取り出します。
戻り値 : 文字列

以下のプログラムのように、三つの関数を使います。

(setq input 99)
(while (>= input 0)
    (setq input (getint "¥n10.0 を割る値を入力(負数で終了) "))
    (setq result (vl-catch-all-apply '(lambda (x) (/ 10.0 x)) (list input)))
    (if (vl-catch-all-error-p result)                           ;例外が発生したか?
        (progn
            (princ (vl-catch-all-error-message result))         ;例外発生 エラーの表示
            (princ "¥n")
        )
        (progn
            (princ result)                                      ;答えの表示
            (princ "¥n")
        )
    )
)

ユーザーから値を受け取って 10.0 を入力値で割った値を表示するプログラムです。入力の値をきちんとチェックしていないので、ユーザーが 0 を入力したらエラーが発生します。しかし vl-catch-all-apply はエラーオブジェクトを返すことによって、実行の継続に致命的な状況になるのを避けます。(本来は、initget 関数を併用することによって、ユーザーの入力値を制限することができます。)vl-catch-all-apply の戻り値を、vl-catch-all-error-p 関数を使ってエラーオブジェクトであるか検査し、異なれば関数は正常に実行された、エラーオブジェクトであれば何らかのエラーが起こったことが分かります。エラーが起こった場合は、vl-catch-all-error-message 関数でエラーオブジェクトからエラーの内容を表す文字列を取得してユーザーに表示したり、他の必要な処理を行ったりすることができます。

上のプログラムの実行結果は以下のようになります。例外が発生してもプログラムの実行が継続されています。

10.0 を割る値を入力(負数で終了) 2⏎
5.0
10.0 を割る値を入力(負数で終了) 3⏎
3.33333
10.0 を割る値を入力(負数で終了) 0⏎
"0(ゼロ)で除算しました"
10.0 を割る値を入力(負数で終了) -1⏎
-10.0

エラーは、入力待ちの間にユーザーが ESC キーを押した場合にも発生します。単純なコマンドの場合は、エラー関数に事後処理を任せてプログラムを終了させても良いのですが、ある程度複雑になってくると、それでは使い勝手が悪いものになります。その場合も例外を使うことでプログラムの実行を継続させることができます。

次の getKeyWord 関数は、ユーザーにキーワードを入力するように求め、結果を返す関数です。このような入力を求める getkword 関数が AutoLISP には用意されています。そして、initget 関数を使うことにより、ユーザーが入力できる値を制限もしています。しかし、ユーザーが素直に適切なキー、この場合は “OK” か “o” を押す前に、ESC キーを押した場合はエラーが発生します。そこで例外の仕組みを使うことで、正常であればキーワードを、エラーが発生すれば nil を返すようにしています。

(defun getKeyWord (/ result)
    (initget 1 "Ok")
    (setq result
            (vl-catch-all-apply 'getkword (list "キーワードを入力[Ok]"))
    )
    (if (vl-catch-all-error-p result)
        nil
        result
    )
)

関数のテストの様子は以下のとおりになります。

コマンド: (getKeyWord) ⏎
キーワードを入力[Ok]o⏎
"Ok"
コマンド: (getKeyWord) ⏎
キーワードを入力[Ok]*キャンセル*                 ;ESC キーを押した
nil

例外は、AutoCAD のバージョンによる仕様の違いを吸収するためにも使用することができます。例えば、AutoCAD2012 以降は command 関数によく似 たcommand-s 関数が用意されています。次のプログラムは、command-s 関数でまず実行を試み、出来なかった場合は command 関数で実行します。事前に、シンボル command-s の内容が定義されているか調べる実装方法もありますが、あえてここでは例外を用いて書いてみます。

(defun command+s (params / result)
    (setq result (vl-catch-all-apply 'apply (list command-s params)))
    (if (vl-catch-all-error-p result)
        (apply 'command params)
    )
    nil
)

vl-catch-all-apply 関数は、function 引数が nil だった場合などのこの関数自身のエラーに対してはエラーオブジェクトを返せません。そのため、「(vl-catch-all-apply 'command-s params)」で command-s 関数が定義されていなかった場合は、エラーオブジェクトを返さず、エラーが発生します。そのため、apply 関数に仕事をさせ、そのエラーをキャッチするようにしています。

関数の使用例は以下のとおりで、例では【テキストスクリーン】がスクリーンに表示されます。

_$ (command+s ‘("textscr"))⏎
nil

vl-catch-all-apply 関数と apply 関数

vl-catch-all-apply 関数と apply 関数の使用法は同じですが、内部的に完全に同じとは言えないようです。理由はわかりませんが、apply 関数ではエラーは発生しませんが vl-catch-all-apply 関数で command 関数を適用しようとするとエラーが発生します。

_$ (vl-catch-all-apply 'command (list "LINE" PAUSE PAUSE "")) ⏎
; エラー: 並べ替え関数が間違っています: COMMAND

この場合、vl-catch-all-apply 関数では command 関数の代わりに vl-cmdf 関数を使用するとエラーは発生しません。

例外の一般化

例外関数を使用した部分のプログラムの構造は共通しているので、一般化した関数を書いてみます。

(defun exception (func args onException / $error)
  (if func
    (if (vl-catch-all-error-p (setq $error (vl-catch-all-apply func args)))
      (eval onException)
      $error
    )
    (exit)
  )
)

func 引数と args 引数は、vl-catch-all-apply 関数の二つの引数と使い方は同じです。onException 引数は、例外が発生した場合に評価され、そこで最後に評価された式が exception 関数の戻り値になります。つまり、アトムは評価されても同じ値ですからそのまま書けますが、実行コードは quote したリストの形で渡します。ここの実行コードでは、シンボル $error に例外オブジェクトが代入されており利用できます。あらかじめわかっている一定のリストを戻り値としたい場合は、onException 引数に評価されるとリストとなるように「(quote '(0 1))」や「''(0 1)」を引数として渡します。

先ほどの0で除算の例外を検出するコードは、次のように書き直しできます。

(setq input 99)
(while (>= input 0)
  (setq input (getint "\n10.0 を割る値を入力(負数で終了) "))
  (setq result (exception (function (lambda (num) (/ 10.0 num)))
                          (list input)
                          '(progn
                            (prompt "\nERROR : ") ;エラーの表示
                            (prompt (vl-catch-all-error-message $error))
                            nil
                           )
               )
  )
  (if result
    (progn (prompt "\n")
           (prompt (rtos result))       ;答えの表示
    )
  )
)

ユーザー入力の例外を扱う例は、次のように書き直せます。

(defun getKeyWord (/ result)
  (initget 1 "Ok")
  (exception 'getkword (list "キーワードを入力[Ok]") nil)
)

command-s 関数が定義されていない場合、command 関数を使う例は次のようになります。

(defun command+s (params / result)
  (exception 'apply (list command-s params) '(apply 'command params))
  nil
)