Ein paar Worte vorabHome   Letzte MeldungenNews   Index der Kapitel und der besprochenen FunktionenIndex   Wer ich bin, warum ich diese Seiten mache, KontaktImpressum   Ich freue mich über jeden Eintrag im Gästebuch!Gästebuch   Einige Links zu anderen AutoLisp-SeitenLinks   Copyrights und DisclaimerRechts
Hier können die kompletten Seiten als ZIP-File heruntergeladen werden!

Funktionen für komfortables Arbeiten mit Zeichenketten String-Tango
Noch mehr Funktionen für komfortableres Arbeiten mit Zeichenketten Kettenhunde
strtok zerlegt Zeichenketten anhand eines Trennzeichens Tock-Tock
Arbeiten mit Datum und Zeit in AutoLisp Zeitlos...
Dotted pairs - wie man den Programmabbruch verhindert Gepunktet?
Neue Funktionen für die Listenbearbeitung Strukturtapete
Weitere neue Funktionen für die Listenbearbeitung Listen to me!
Lambda expressions - dasSalz in der Suppe Lambada
Lambda expressions anhand eines Praxisbeispiels Unter der Erde
Where und whereever erleichtern den Umgang mit Listen Quo vadis?
Rekursion - Funktionen, die sich selbst aufrufen Katzenschwanz
Ein äusserst wichtiger Prototyp für Funktionen Nix passiert
Wo das lineare mapcar am Ende ist Tiefer rein!
Über Effekte und Neben(Seiten-)Effekte von Funktionen Seitensprünge
Die Namensräume (Sichtbarkeit) von Variablen Raumwunder
Let dient zur Schaffung kleinerer Namensräume Lass mal...
Sukzessive Verarbeitung von Listenresten mit mapcdr Der Bruder
Was in AutoLisp einfach nicht machbar ist (Teil I) Beschränkt
Was in AutoLisp einfach nicht machbar ist (Teil II) Limited Edition
Nicht mit Effekten arbeiten, sondern direkter Daten-Änderung Destruktiv
Sequenzielles vs. paralleles Abarbeiten von Argumenten Parallelwelten
Über den Umgang mit Funktionsschablonen Erwachet!
Ein Praxiskapitel über Auswahlsätze, Attribute, wcmatch und mehr Durch die Brust
Die Farben des AutoCAD Color Index und ihre RGB-Werte Alles so schön
Hier laufen die Fäden zusammen: Viele Konzepte vereint Lapsus Lispuli
Ein Spiel als Beispiel für lernfähige Funktionen Zug um Zug
Errorhandling in AutoLisp - Teil 1 Alles valsch!
Errorhandling in AutoLisp - Teil 2 Und foll Feler!


Zum Einsteiger-Tutorial

Zu den ActiveX-Seiten

Meine Private HP mit Fotos, Gedichten, Musik und Postkartenversand

Mein Online-Lexikon der Fotografie

Mein völlig abgedrehtes Reisebüro










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.