In den Lisp-Handbüchern von AutoCAD steht, dass Variablen, die
hinter dem Schrägstrich in der Argumentenliste von
(defun ...)
deklariert werden,
lokal sind. Genau genommen stimmt das
nicht ganz: Der Namensraum lokaler Variablen ist eigentlich (d.h.
in anderen Programmiersprachen) exakt auf die Funktion begrenzt,
in der sie deklariert werden.
In AutoLisp sind Variablen, die in einer Funktion deklariert
werden, nicht nur innerhalb dieser sichtbar, sondern auch
innerhalb aller aus dieser Funktion heraus aufgerufenen weiteren
Funktionen! Dies gilt natürlich nur, solange in den Unterfunktionen
nicht weitere namensgleiche Variablen existieren, die die weiter oben
in der Hierarchie angesiedelten Variablen überdecken.
Um diese Zusammenhänge zu verdeutlichen, hier ein paar (inhaltsleere)
Beispiel-Funktionen mit verschiedenen Variablen:
(defun funktion1( / a b c)
(setq a 11 b 12 c 13)
(funktion2)
(list a b c)
)
(defun funktion2( / b c)
(setq a 21 b 22 c 23)
(funktion 3)
(list a b c)
)
(defun funktion3( / c)
(setq a 31 b 32 c 33)
(list a b c)
)
Der Namensraum von
a erstreckt sich eindeutig über
funktion1
einschliesslich der Aufrufe von
funktion2 und
funktion3,
d.h., die Variable ist in allen drei Funktionen sichtbar und ihr Wert
kann überall verändert werden! Unter dem Namen
b existieren aber
schon zwei Variablen: Die eine wird in
funktion1 deklariert, ist
aber in
funktion2 nicht mehr sichtbar, da sie dort von einer
anderen, jedoch namensgleichen Variablen verdeckt wird.
Die Variable
c schliesslich ist dreifach vorhanden, in jeder der drei
Funktionen existiert eine! Betrachten wir das Ganze nach Funktionen: In
funktion1 sind
b und
c also lokale Variablen,
a
ist nicht wirklich lokal, da der Namensraum größer ist. In
funktion2
ist
c lokal, aber der Namensraum von
b deckt auch
funktion3
mit ab. Was wird also die Rückgabe von
funktion1 sein? Richtig:
(31 12 13)!
Betrachten wir nun folgende Funktion:
(defun foo (a b / c d e)
(setq c (* a b)
d (/ a b)
e (+ a b)
f (- a b)
)
(list c d e f)
)
Richtig, es fällt auf, dass f hier nicht als 'lokale' Variable
deklariert wurde! Aber es ist ein Fehler, nun den Schluss daraus zu ziehen,
f sein eine globale Variable! Alles, was wir aufgrund dieser Funktion
sagen können, ist, dass f nicht zu dieser Funktion lokal ist.
Schlimmer noch! Wir wissen ja bisher gar nicht, wer diese Funktion aufruft!
Nehmen wir an, wir suchen ein wenig im Code und finden diese beiden hier:
(defun bar( / f)
(foo 33 44 55 66)
...
)
(defun baz( / )
(foo 33 44 55 66)
...
)
Wird
foo von
bar aufgerufen, dann ändert die aufgerufene
Funktion den Wert von
f in
bar, da die Variable ja auch
in
bar sichtbar ist!
baz hingegen würde durch
(setq f ...)
stillschweigend eine globale Variable erzeugen ... oder auch nicht!
Schliesslich kann es ja sein, dass um unsere Anordnung von
bar, baz
und
foo herum noch weitere Funktionen liegen, in denen eine Variable
f deklariert ist. Diese würde dann (evtl. unabsichtlich und
unbemerkt) verändert, aber es enstünde keine globale Variable!
Es wird jetzt klar, dass hier ein ungeheures Fehlerpotenzial liegt!
Wir können ja nicht einmal in einem Programm nachverfolgen, von welchen
anderen Funktionen eine ganz bestimmte Funktion aufgerufen wird.
Natürlich kann man den Quellcode eines Programms mit dem Texteditor
danach durchsuchen, aber würde man dann z.B. auch diese Funktion
finden ??? :
(defun heimlich( / q r)
(setq q'(0 7))
(setq r(str-list "fehlerpotenzial"))
(apply
(read
(apply'strcat
(mapcar
'(lambda(w / )(nth w r))
(append q(list(last q)))
)
)
)
q
)
)
Jawohl, auch diese Funktion ruft (ganz heimlich)
foo auf, wenn es
auch nicht auf Anhieb sichtbar ist! Kein Texteditor wird jemals in der
Lage sein herauszukriegen, dass hier
foo aufgerufen wird! Folgende
Fehler sind in unserem Szenario vorstellbar:
-
In einer Funktion wird vergessen, eine lokale Variable
in die Argumentenliste einzutragen. Im günstigeren Falle
entsteht eine globale Variable, im ungüstigeren Fall
manipulieren wir eine namensgleiche Variable in einer
übergeordneten Funktion.
-
Ein Programm verlässt sich auf die Manipulation einer
weiter oben deklarierten Variablen in tifer angesiedelten
Unterfunktionen. Bei einer Änderung des Programms wird
irgendwo in der Hierarchie der Funktionsaufrufe eine
neue Variable eingeführt, die zufällig den gleichen
Namen hat. Plötzlich funktioniert nichts mehr!
-
Ein Programm arbeitet mit globalen Variablen... Der
Rest verhält sich genauso wie im vorigen Beispiel.
Hier noch ein Code-Beispiel, wie durch einen kleinen
Flüchtigkeitsfehler eine ganz ordentliche Schleife zur
Endlosschleife mutieren kann:
(defun f1( / i)
(setq i 0)
(while(< i 10)
(f2)
)
)
(defun f2( / j)
(setq i 0)
(while(< i 5)
...
)
)
Dadurch, dass hier beim Eintrag in die Argumentenliste statt
einem
i ein
j eingetippt wurde, greift
f2
nun auf die Variable
i aus
f1 zu. Diese wird
immer wieder auf 0 gesetzt, sodass die Schleife in
f1
weiterläuft, bis sie vom Benutzer endlich abgebrochen wird.
Was kann man denn nun tun, um sich vor solchen Fehlern zu schützen?
Nun, es ist nicht leicht, und einen vollständigen Schutz gibt es
ganz sich nicht. Aber man kann sich an ein paar Regeln halten:
-
Die allererste und wichtigste Massnahme ist natürlich,
Variablen immer schön lokal zu deklarieren und mehr
als einmal nachzusehen, ob man auch keine vergessen hat.
-
Gewollte und notwendige globale Variablen als solche
kennzeichnen! In Lisp hat sich eingebürgert, dass der
Name einer globalen Variablen mit Sternchen eingefasst
wird: *my_global* oder *config*. Auf
unnötige globale Variablen sollte man verzichten!
-
Man sollte möglichst wenig Gebrauch von der Tatsache
machen, dass Variablen auch in Unterfunktionen sichtbar
sind. Allerdings ist man manchmal auf diesen Mechanismus
angewiesen, z.B. bei Rekursionen und auch bei
wechselrekursiven Funktionen, die gemeinsam auf eine
Variable zugreifen. In diesen Fällen kann man das Problem
meist umgehen, indem man auch die entsprechenden Funktionen
lokal macht (verschachtelte Funktionsdefinitionen).
-
Möchte man wirklich mit einem Namensraum über mehrere
Funktionsebenen arbeiten, dann empfiehlt es sich, die
Variablen entsprechend zu bezeichnen (ich halte es so,
dass der Variablenname den Namen der Funktion enthält,
in der die Variable definiert wurde) und in den
Kommentaren der Unterfunktion zu erwähnen, dass hier
auf Variablen aus übergeordneten Funktionen zugegriffen
wird.