In diesem Kapitel geht es darum, das bereits vorgestellte
Konzept zum Errorhandling in AutoLisp noch ein wenig zu verbessern.
Gleich vorab: Es wird nicht ganz einfach, das neue Konzept
nachzuvollziehen. Wer unsicher ist, sollte vorab vielleicht
noch einmal ein Blick in das Kapitel über Namensräume
werfen. Ebenso wäre es empfehlenswert, sich noch einmal im
Kapitel über das TicTacToe-Spiel ein wenig über das Erzeugen
von Funktionen mit
(setq) anstelle von
(defun)
zu informieren, falls diese Technik völlig fremd erscheint.
Zwei Hauptaufgaben sind hier zu erledigen. Erstens soll auf
globale Variablen verzichtet werden - und auf lokale Variablen
im aufrufenden Programm können wir sowieso verzichten. Fazit:
Alles wird in der Funktion
(*error*) untergebracht,
sowohl die Liste mit den gesicherten Systemvariablen als auch
die vorher vorhandene
(*error*)-Funktion. Das bedeutet
allerdings, dass diese Funktion nicht schon fertig geschrieben
vorliegen kann, sondern erst dann zur Laufzeit geschrieben wird,
wenn das Errorhandling beginnt. Allerdings behalten wir ein
vordefiniertes 'Gerippe', in das dann noch ein paar Zeilen Code
hineinpraktiziert werden.
Zweitens soll die ganze Angelegenheit 'stapelbar' werden. Hier
haben wir allerdings ein kleines Problem: Der Interpreter ruft
im Fehlerfall immer die Funktion
(*error*) auf - der
Name ist festverdrahtet. Es gibt Empfehlungen von AutoLisp-Experten,
die
(*error*)-Funktion lokal zum umgebenden Programm zu
definieren - richtig, dann spart man sich das Sichern der
bisherigen
(*error*)-Funktion, da diese ja unangetastet
bleibt. Auf diese Weise kann man sogar jede Menge
(*error*)-Funktionen erzeugen, die alle in ihrem abgegrenzten
Namensraum liegen und sich nicht gegenseitig stören. Der Benutzer
kann seine Programm-Aufrufe beliebig verschachteln.
Einen kleinen Fehler hat dieses Konzept allerdings schon: Es gibt
definitiv keine Möglichkeit in AutoLisp, dass irgendeine Funktion
eine andere Funktion aufruft, die genauso heisst, aber im
übergeordneten Namensraum zu Hause ist. Einen solchen Aufruf
(in Java z.B. könnte man die beiden namensgleichen Methoden
irgendwas() durch Angabe der Namensräume super.irgendwas() und
this.irgendwas() voneinander abgrenzen) kennt AutoLisp einfach
nicht. Wir können zwar auf diese Weise die bisherige
(*error*)-Funktion sichern und wiederherstellen, aber
aufgerufen wird sie im Fehlerfall doch nicht.
Wenn also Programm A die Systemvariable
cmdecho manipuliert
und Programm B die Variable
filedia, dann wird (wenn der
Fehler in B auftritt) zwar
filedia zurückgesetzt, aber
nicht
cmdecho. Wir müssen also anders vorgehen: Unser
Errorhandler stellt zunächst den Code von
(*error*) sicher
und erzeugt dann eine neue
(*error*)-Funktion, die die
alte als lokale Funktionsdefinition enthält. Wenn der Code unseres
Handlers abgearbeitet ist, wird anschliessend die
eingelagerte Vorgängerfunktion aufgerufen.
Parallel dazu wird
(*error*) selbst wieder auf die
vorhergehenden Inhalte zurückgesetzt, so dass nach einem Abbruch
oder Fehler der alte Zustand wieder hundertprozentig hergestellt
wird, egal wie tief die Verschachtelung von Programmen und
Errorhandlern ausfällt. Damit es in diesem Kapitel nicht zu ständigen
Wiederholungen von leicht modifizierten Codefragmenten kommt, folgt
hier gleich der komplette, fertige Code, und im Anschluss an
den Code gibt es Erläuterungen und Hinweise.
(defun startErrorHandler(name undoMode varsToSave /
ErrorTemplate saveList)
(setq errorTemplate
'( (msg / name undo savedVars previousHandler)
;... Zeile wird eventuell noch eingesetzt
;... Zeile wird noch eingesetzt
;... Zeile wird noch eingesetzt
;... Zeile wird noch eingesetzt
(while(>(getvar "cmdactive")0)(command))
(princ(strcat "\n"name": previous Handler = "))
(princ previousHandler)
(command"_undo""_end")
(if undo(command"_u"))
(foreach pair savedVars
(setvar(car pair)(cdr pair))
(princ
(strcat"\n"name": Setze \""
(car pair)"\" zurück auf "))
(princ(cdr pair))
)
(setq *error* previousHandler)
(if msg
(progn
(princ(strcat"\n" name ": \"" msg "\""))
(if previousHandler(previousHandler msg))
)
)
)
)
;************
(command"_undo""_begin")
(foreach pair varsToSave
(setq saveList
(cons
(cons(car pair)(getvar(car pair)))
saveList
)
)
(setvar(car pair)(cdr pair))
)
(setq *error*
(append
(list(car ErrorTemplate))
(if undomode'((setq undo 'T)))
(list
(list 'setq
'PreviousHandler
(cons'quote(list *error*))
)
)
(list(list 'setq 'name name))
(list
(cons'setq
(cons'savedvars
(list(cons'quote(list savelist)))
)
)
)
(cdr ErrorTemplate)
)
)
)
(defun endErrorHandler( / )
(*error* nil)
)
Die gute Nachricht ist, dass sich in der Funktion
(endErrorHandler) nichts geändert hat: Sie ist genauso
einfach und übersichtlich wie vorher (eigentlich dient sie
ja sowieso nur der Optik). Die schlechte Nachricht allerdings
ist, dass in
(startErrorHandler) am Ende eine ganze
Passage schwerverdaulichen Codes hinzugekommen ist. Diese Passage
wollen wir jetzt untersuchen. Es geht im Prinzip nur darum, vier
Zeilen dynamisch erzeugten Codes in die vordefinierte Schablone
errorTemplate einzufügen. Die Schablone selbst ist in
(startErrorHandler) als lokale Variable integriert, sodass
die Funktion
(myErrorFunction) aus dem vorigen Kapitel
entfällt.
Hinzugekommen sind zwei weitere Argumente für
(startErrorHandling):
name ist ein frei wählbarer Bezeichner, der übergeben wird.
Er dient lediglich der Orientierung des Benutzers, hat aber keinerlei
Programmfunktion. Wenn eine
(*error*)-Funktion etwas auf
dem Bildschirm ausgibt, setzt sie diesen Namen dazu, damit man
unterscheiden kann, was von welcher Instanz des Errhandlers kommt.
undoMode kann
T oder
nil sein und gibt an,
ob im Fehler- bzw. Abbruchsfall gleich der Befehl 'Z' ausgeführt
werden soll, um alle bis dahin vorgenommen Aktionen sofort
rückgängig zu machen.
Werfen wir nun einen Blick auf die Schablone: Es fehlen also die
ersten drei bzw. vier Zeilen, die erst beim Start des Errorhandlers
noch eingesetzt werden. Die lokalen Variablen
name,
undo,
savedVars und
previousHandler werden zwar im Code
verwendet, sind aber nicht initialisiert. Diese Initialisierung
wird noch nachgereicht. Dazu dient das
(append) am Ende von
(startErrorHandler): Hier wird mit
(car) die erste
Zeile der Schablone (die Formale Argumentenliste) und mit
(cdr)
der Rest der Schablone ermittelt. Dazwischen aber die vier
Ausdrücke, die den entsprechenden Code generieren und die in der
Schablone fehlenden Zeilen erzeugen. Der Code von
(*error*) kann,
wenn man ihn aus dem Speicher ausliest, etwa so aussehen:
( (MSG / NAME UNDO SAVEDVARS PREVIOUSHANDLER)
(SETQ PREVIOUSHANDLER
(QUOTE
( (MSG / NAME UNDO SAVEDVARS PREVIOUSHANDLER)
(SETQ PREVIOUSHANDLER
(QUOTE #)
)
(SETQ NAME "Level1")
(SETQ SAVEDVARS
(QUOTE (("blipmode" . 1) ("osmode" . 0)))
)
(WHILE (> (GETVAR "cmdactive") 0) (COMMAND))
(COMMAND "_undo" "_end")
(IF UNDO (COMMAND "_u"))
(FOREACH PAIR SAVEDVARS
(SETVAR (CAR PAIR) (CDR PAIR))
(PRINC
(STRCAT "\n" NAME ": Setze \""
(CAR PAIR)
"\" zurück auf "
)
)
(PRINC (CDR PAIR))
)
(SETQ *ERROR* PREVIOUSHANDLER)
(IF MSG
(PROGN
(PRINT (STRCAT NAME ": \"" MSG "\""))
(IF PREVIOUSHANDLER (PREVIOUSHANDLER MSG))
)
)
)
)
)
(SETQ NAME "Level2")
(SETQ SAVEDVARS
(QUOTE (("filedia" . 0) ("cmdecho" . 1)))
)
(WHILE (> (GETVAR "cmdactive") 0) (COMMAND))
(COMMAND "_undo" "_end")
(IF UNDO (COMMAND "_u"))
(FOREACH PAIR SAVEDVARS
(SETVAR (CAR PAIR) (CDR PAIR))
(PRINC
(STRCAT "\n" NAME ": Setze \""
(CAR PAIR)
"\" zurück auf "
)
)
(PRINC (CDR PAIR))
)
(SETQ *ERROR* PREVIOUSHANDLER)
(IF MSG
(PROGN
(PRINT (STRCAT NAME ": \"" MSG "\""))
(IF PREVIOUSHANDLER (PREVIOUSHANDLER MSG))
)
)
)
Der hier abgebildete Code (natürlich nachträglich schön
eingerückt) stellt also eine
(*error*)-Funktion der
zweiten Instanz dar, das heisst, er wurde innerhalb eines
Programms erzeugt, das schon aus einem anderen Programm mit
Errorhandling heraus aufgerufen wurde. Wichtig ist natürlich
die Anordnung der letzten Zeilen - damit die Error-Funktionen
nacheinander ausgeführt werden und die Reihenfolge aller Dinge
gewahrt bleibt, muss die Zeile mit dem Aufruf des vorigen
Handlers die letzte sein und es darf nichts mehr danach folgen.
Obwohl hier nur eine zweite Instanz vorliegt, sind natürlich
drei Handler beteiligt, am Ende der Kette finden wir die
eingebaute SUBR von AutoLisp, die zwar ganz zum Schluss
aufgerufen wird, aber eigentlich gar nichts mehr zur Sache
beiträgt. Hervorzuheben ist dann auch die Tatsache, dass die
jeweils vorigen Handler natürlich nur dann aufgerufen werden
dürfen, wenn das Argument msg ungleich
nil ist, also wenn
tatsächlich ein Fehler oder Abbruch aufgetreten ist.
Das ist ganz wichtig, denn sonst wäre natürlich eine Behandlung
von ordentlich laufenden Programmen unmöglich. Wenn Programm B
innerhalb von Programm A ausgeführt wird und weder ein Fehler
noch ein Abbruch auftritt, dann muss natürlich nach dem Beenden
von B die
(*error*)-Funktion auf den Handler von A
zurückgesetzt werden, aber dieser darf keinesfalls ausgeführt
werden. Programm A läuft ja noch, und das fehlerfrei! Der Handler
von A darf erst von
(endErrorHandling) am Ende von A
aktiviert werden.
Gehen wir doch nun einmal daran, die Sache ein wenig zu testen.
Wir haben eine Funktion A mit eingebautem Errorhandling:
(defun c:A-Test( / )
(startErrorHandler "A-Handler" 'T
'(("osmode" . 0)("blipmode" . 1))
)
(command "_line" '(0 0)'(100 100)"")
(command "_line" '(100 0)'(0 100)"")
(getint "\nZahl eingeben: ")
(endErrorHandler)
)
Wenn wir bei der Zahlen-Eingabe mit ESC abbrechen, erhalten wir folgende
Meldungen auf dem Bildschirm:
A-Handler: Setze "blipmode" zurück auf 0
A-Handler: Setze "osmode" zurück auf 37
A-Handler: "Funktion abgebrochen"
Natürlich können die Systemvariablen auch andere Werte gehabt
haben, bei mir war's jedenfalls so wie abgebildet. Möglich ist auch,
dass abhängig von
cmdecho das Undo-Handling auf dem Bildschirm
ausgegeben wird. Jetzt definieren wir noch die B-Funktion, die
ihrerseits die A-Funktion aufruft:
(defun c:B-Test( / p1)
(startErrorHandler "B-Handler" 'T
'(("cmdecho" . 0)("filedia" . 0))
)
(command "_line"
(setq p1(getpoint "\nErsten Punkt wählen: "))
(getpoint p1 "\nZweiten Punkt wählen: ")
""
)
(c:A-Test)
(endErrorHandler)
)
Wir rufen den B-Test auf und brechen wieder innerhalb des A-Tests
bei der Eingabe der Integerzahl ab:
A-Handler: Setze "blipmode" zurück auf 0
A-Handler: Setze "osmode" zurück auf 37
A-Handler: "Funktion abgebrochen"
B-Handler: Setze "filedia" zurück auf 1
B-Handler: Setze "cmdecho" zurück auf 0
B-Handler: "Funktion abgebrochen"
Auch das sieht sehr vernünftig aus - Alle Werte werden ordenlich
restauriert. Als kleinen Schönheitsfehler kann man bemängeln, dass
beide unserer Handler die Fehlermeldung ausgeben - das liesse sich
natürlich mit zwei oder drei Zeilen weiteren Codes vermeiden. Ich
möchte mich hier aber auf das Wesentliche beschränken.
Nun ein weiterer Test, um nicht nur auf Abbrüche durch den Benutzer
einzugehen. Dazu verwenden wir die Funktion C-Test, die einen Fehler
auch ohne jede Benutzereingabe erzeugt:
(defun c:C-Test( / )
(startErrorHandler "C-Handler" 'T
'(("attreq" . 0)("attdisp" . 1))
)
(/ 2 0)
(endErrorHandler)
)
C-Handler: Setze "attdia" zurück auf 0
C-Handler: Setze "attreq" zurück auf 0
C-Handler: "Division durch 0"
Und zum Schluss noch einmal unsere B-Funktion, hier allerdings
unter dem Namen D-Test und mit einer kleinen Modifikation, die
einen Fehler verursacht:
(defun c:D-Test( / p1)
(startErrorHandler "D-Handler" 'T
'(("cmdecho" . 0)("filedia" . 0))
)
(command "_line"
(setq p1(getpoint "\nErsten Punkt wählen: "))
(getpoint p1 "\nZweiten Punkt wählen: ")
)
(c:A-Test)
(endErrorHandler)
)
D-Handler: Setze "filedia" zurück auf 1
D-Handler: Setze "cmdecho" zurück auf 1
D-Handler: "Funktion abgebrochen"
Warum wird hier abgebrochen, auch wenn wir die ESC-Taste gar
nicht angefasst haben? Wer genau hinschaut, wird feststellen, dass
der Fehler auf den nicht abgeschlossenen Linie-Befehl zurückgeht -
der Return-Leerstring am Ende fehlt. Beim nachfolgenden Aufruf von
A-Test bricht der A-Handler natürlich den Linie-Befehl ab, da
cmdactive noch einen Wert > 0 hat. Auch hier wird das
Programm noch ordentlich terminiert - ob das allerdings bei allen
Fehlern, die schon im Quellcode vorhanden sind, so funktioniert,
möchte ich eher bezweifeln.