Dies ist eine Übersetzung von drei Artikeln, die Mark-Jason Dominus im Usenet gepostet hat. Die Originale finden sich unter http://perl.plover.com/varvarname.html, http://perl.plover.com/varvarname2.html und http://perl.plover.com/varvarname3.html . Ich habe mir die Freiheit genommen, auch die Header und Codebeispiele zu übersetzen. Mögen die Autoren mir verzeihen. -- HaraldBongartz
Warum es dumm ist, eine Variable als Variablenname zu benutzen
Betreff: Warum es dumm ist, "eine Variable als Variablenname zu benutzen"
Von: mjd@op.net (Mark-Jason Dominus)
Datum: 1998/06/10
Message-ID: <6lnb70$lct$1@monet.op.net>
Newsgroups: comp.lang.perl.misc,comp.programming
Immer wieder tauchen Leute in comp.lang.perl.misc auf und fragen, wie man den Inhalt einer Variable als Namen für eine andere Variable benutzen kann. Zum Beispiel haben sie
$foo='snonk' und sie wollen etwas mit dem Inhalte der Variablen
$snonk machen.
Das lässt sich in Perl sehr einfach erreichen, also bringen sie meist einige Leute dazu, es ihnen zu zeigen. Und sie veranlassen andere Leute dazu, sie zu fragen, warum sie stattdessen keinen Hash benutzen. Manchmal gehöre ich zu den Leuten, die vorschlagen, einen Hash zu benutzen, und manchmal gehöre ich zu den Leuten, die einfach die Frage beantworten, auch wenn ich der Meinung bin, dass sie stattdessen einen Hash benutzen sollten.
Jedenfalls rief mich vor ein paar Wochen einer meiner Kunden an, weil ein Programm falsche Berichte erzeugte. Sie mussten das Problem bis zum nächsten Tag gelöst haben. Das Programm hat Datensätze wie die folgenden
dies rot irgendwas
das grün irgendwas anderes
anderes rot mehr
dies blau noch mehr
aus einer Datenbank eingelesen und daraus einen Bericht erstellt, wie oft jeder Wert an jeder Position der Datensätze vorkommt.
Es zeigte sich, dass die Deppen, die dieses Programm geschrieben hatten, es so ähnlich gemacht hatten:
while (<DATENSAETZE>) {
chomp;
@werte = split /\t/, $_;
foreach $w (@werte) {
$$w++;
}
}
print <<EOT;
Frage 1:
$dies Benutzer sagten 'dies'. $das Benutzer sagten 'das'.
Frage 2:
$rot Benutzer sagten, ihre Lieblingsfarbe sei rot.
... (und so weiter) ...
EOT
Natürlich war der echte Code viel länger und wesentlich verwirrender.
Nun, kurz gesagt, es stellte sich heraus, dass das Problem daran lag, dass es eine bestimmt Antwort gab, nennen wir sie mal "foo" (tatsächlich war es "digoxin" - kaum zu glauben), die eine gültige Antwort auf zwei vollkommen unabhängige Fragen war, sagen wir mal Nummer 7a und Nummer 11. Somit wurde jeder, der auf Frage 7a mit "foo" antwortete, so gezählt, als hätte er auch Frage 11 mit "foo" beantwortet, und umgekehrt. Dann traten die Summen im Bericht an zwei verschiedenen Stellen auf und die Berichte waren nicht richtig.
Diese kaputte Logik zog sich so durch das Programm, dass ich keine einfache Methode finden konnte, um es in Ordnung zu bringen. Wenn die ursprünglichen Programmierer eine Reihe von Hashes benutzt hätten, statt alles in einen Stapel von globalen Variablen zu stopfen, wäre das nie passiert oder schlimmstenfalls wäre es einfach zu reparieren gewesen. Es endete damit, dass ich das Programm grundlegend überarbeitet habe, um das Problem zu beseitigen. Die Hauptschleife sah schließlich ungefähr so aus:
while (<DATENSAETZE>) {
chomp;
@werte = split /\t/, $_;
for ($i=1; $i <= $ANZAHLFRAGEN; $i++)
my $v = shift @werte;
$anzahl[$i]{$v}++
}
}
Natürlich war der echte Code viel länger and wesentlich verwirrender, aber er war weder so lang noch so verwirrend wie zu dem Zeitpunkt, als ich mich daran gesetzt habe.
Es läuft mir kalt den Rücken herunter, wenn ich daran denke, was wohl mit dem Programm passiert wäre, wenn eine der Antworten 'i' oder 'v' oder '3' oder so gewesen wäre. Man kann sich sogar vorstellen, dass das diesen Deppen ab und zu passiert, und dass sie daraufhin den Namen der betroffenen Variablen ändern, anstatt aus dieser Warnung etwas zu lernen.
Naja, den Namen einer Variablen aus einem Eingabewert abzuleiten, hat sich in diesem Fall als eine ziemlich dumme Entscheidung herausgestellt, und dazu noch eine, die meinem Kunden einige Tausend Dollar kostete.
Wenn jemand nach comp.lang.perl.misc kommt und dort fragt, wie man etwas Dummes machen kann, dann weiß ich nie genau, wie ich reagieren soll. Ich kann die Frage beantworten, wie sie gestellt wurde und mir sagen, dass es nicht meine Aufgabe ist, anderen zu sagen, dass sie etwas Dummes vorhaben. Das ist in meinem eigenen Interesse, denn auf diese Weise kostet mich die Anwort weniger Zeit, und vielleicht bezahlt mich später mal jemand dafür, dass ich hinterher die Dummheiten aufräume, wie in diesem Fall. Aber wenn ich so reagiere, werde ich vielleicht von anderen angemotzt, weil ich so auf Schlauberger mache, was schon ab und zu passiert ist. ("Nun mach schon, hilf dem armen Kerl doch; wenn du weißt, was er wirklich braucht, warum sagst du es ihm nicht?")
Andererseits könnte ich auf einem anderen Level antworten, eine bessere Lösung vorstellen und ihnen vielleicht etwas beibringen. Das ist nett, wenn es funktioniert. Aber wenn nicht, dann ist es wirklich frustrierend zu sehen, wie eine gute Lösung und guter Rat ignoriert werden. Auch werde ich dann von anderen angemacht, weil ich die Frage nicht beanworte. ("Wer bist du eigentlich, dass du dem Typ sagst, was er machen soll? Beantworte einfach die Frage.")
Ich denke beide Antwortmöglichkeiten sind angebracht. Oder vielleicht auch für keine von beiden.
Was auch immer. Offensichtlich bin ich etwas vom Thema abgeschweift. Das eigentliche Problem mit diesem Code: Er ist zerbrechlich. Man vermischt unterschiedliche Dinge miteinander. Und wenn zwei dieser unterschiedlichen Dinge zufällig den gleichen Namen haben, gibt es einen Konflikt zwischen ihnen und man erhält die falsche Antwort. Schließlich erhält man eine lange Liste von Namen, bei denen man aufpassen muss, sie nicht nochmals zu benutzen, und wenn man dabei etwas falsch macht, erhält man einen wirklich merkwürdigen Fehler. Eben um solche Probleme zu lösen, wurden Namensräume erfunden, und genau das ist ein Hash: Ein portabler Namensraum.
In diesem Artikel ging es hauptsächlich darum, einen echtes Beispiel dafür zu bringen, wo die Benutzung einer Variablen für einen Variablennamen eine ziemlich dumme Idee war. Es scheint, dass die meisten Leute, die dazu in comp.lang.perl.misc schreiben, die gleiche dumme Idee auf die gleiche dumme Art umsetzen wollen, und daher dachte ich mir, es wäre gut, das mal zu erwähnen und vielleicht kann ich erreichen, dass den Leuten das Problem bewusster wird.
Eine genauere Erklärung des Problems
Betreff: Re: Kritik/Kommentare zu einem Webboard?
Von: mjd@op.net (Mark-Jason Dominus)
Datum: Don, 16 Sep 1999 21:46:20 GMT
Message-ID: <7rroeb$s1n$1@monet.op.net>
Newsgroups: comp.lang.perl.misc
Im Artikel <37e05478_1@news2.one.net> schrieb David Wall <darkon@one.net>:
>
> Im Artikel <7rnome$ho2@dfw-ixnews19.ix.netcom.com> schrieb ebohlman@netcom.com
> (Eric Bohlman):
>> 2) Du benutzt symbolische Referenzen nur um weniger Tipparbeit zu haben.
>> Da sparst du an der falschen Stelle. Benutze einen Hash stattdessen, und
>> benutze 'strict'.
>
> Ich bin mir nicht sicher, ob ich jetzt ohne weiteres verstehe, was du hier meinst.
> Ich denke mal, ich sollte sowas hier nicht machen?
>
> # Nachrichtenfelder herausholen und in eigene Variablen legen,
> # damit ich nicht so viel zu tippen habe, wenn ich den Code warte
> foreach $feld ( keys %{$Nachrichten{$id}} ) {
> $$feld = $Nachrichten{$id}{$feld};
> }
Genau.
> Was ist denn so schlecht an symbolischen Referenzen? [Zeit vergeht, während ich
> das im CamelBook nachschlage] Hm, ich nehme an, ich könnte versehentlich eine
> echte und eine symbolische Referenz verwechseln und es ist dann verdammt
> schwierig, den Fehler herauszufinden, hm?
Nein. Das eigentliche Problem liegt darin, dass, wenn dein String etwas Unerwartetes enthält, eine vollkommen andere Stelle des Programms dadurch sabotiert wird, und es ist dann verdammt schwierig, den Fehler herauszufinden.
Stell dir vor, etwas geht schief und einer der "Feldnamen" in einer der Nachrichten wird verstümmelt. Vielleicht gabe es eine Fehlermeldung, die du nicht gemerkt hast, und daraufhin ist einer der Schlüssel
0, oder vielleicht wurde eine Funktion im falschen Kontext aufgerufen und der Feldname ist auf einmal
4, oder es wird etwas gequotet, was nicht sein sollte und ein Feldname ist plötzlich
*.
Dann mach deine Schleife fröhlich voran und versucht, die Variablen
$0 oder
$4 oder
$* zu verändern, die alle reserviert sind und jeweils eine besondere Bedeutung haben, und schließlich passiert etwas merkwürdiges und hässliches -- was genau, hängt vom Variablennamen ab. Wenn du zum Beispiel versehentlich
$* änderst, könnten einige deiner Regex plötzlich matchen, wo sie nicht sollte. Versuch mal, das zu debuggen.
Wenn der Feldname
/ enthält, wirst du
$/ ändern, und plötzlich geht das Einlesen von jedem Filehandle im Rest des Programms komplett schief. Diese Einlesevorgänge sind vielleicht an einer ganz anderen Stelle des Programms als der Bug und du brauchst möglicherweise Tage um das herauszufinden - oder du schaffst es nie.
Wenn es eine Feld gibt, dessen Name zufälligerweise
i ist, dann fummelst du an
$i herum, und das Problem fällt dir nicht mal auf, bis du eines Tages so etwas hinzufügst
for $i (...) {
...
}
und deine
$$felder Zuweisung deine Schleifenvariable platt macht, das Programm in einer Endlosschleife landet und alles sehr merkwürdig wird.
Das eigentliche Problem ist, dass
$$fields = ... keine Begrenzung hat, und wenn etwas schief geht, kann es eine Variable irgendwo im gesamten Programm betreffen, was bizarre Auswirkungen irgendwo weit weg hat.
Diese Art von Problem sollen Namensräume für Variablen verhinden. Code A soll nicht mit Variablen von Code B herummachen können, der sich ganz woanders befindet. Ein Hash ist ein portabler Namensraum, und wenn du hier einen Hash benutzt, hast du kein Problem mit diesen Merkwürdigkeiten.
# Nachrichtenfelder herausholen und in eigene Variablen legen,
# damit ich nicht so viel zu tippen habe, wenn ich den Code warte
my %f;
foreach $feldname ( keys %{$Nachrichten{$id}} ) {
$f{$feldname} = $Nachrichten{$id}{$feldname};
}
Jetzt musst du
$f{BETREFF} oder so schreiben. Das sind nur drei Zeichen mehr, aber du läufst nicht mehr Gefahr, irgendwelche weit entfernten Variablen plattzumachen.
Wenn du es so schreibst, wird auch sofort deutlich, dass sich das gleichzeitig kürzer und einfacher schreiben lässt:
# Nachrichtenfelder herausholen und in eigene Variablen legen,
# damit ich nicht so viel zu tippen habe, wenn ich den Code warte
my %f = %{$Nachrichten{$id}};
Das ist kürzer, einfacher, sicherer und effizienter. Ein Stapel von großen Vorteilen und ein mittelgroßer Vorteil, und im Gegenzug musst du nur
$f{BETREFF}
statt
$BETREFF
schreiben.
Wie üblich gilt es also abzuwägen, aber in diesem schlägt die Waage ganz eindeutig zu einer Seite aus. Die Methode der Soft References bereitet große Probleme und triviale Vorzüge.
> Also soll ich stattdessen hingehen und $Nachrichten{$id}{$feld} benutzen?
> Hey, was ist aus der Faulheit geworden?
Ich bin immer für Faulheit.
1 Ich bin zu faul, um mein halbes Leben lang Monster-Bugs zu suchen, die durch eine versehentlich veränderte globale Variable verursacht wurden. Das ist der Grund, warum wir lokale Variablen haben: Um genau diesen Fehler zu vermeiden.
Ich hoffe, das macht das potenzielle Problem deutlicher.
Was ist, wenn ich wirklich vorsichtig bin?
Betreff: Re: Ist $$variable erlaubt wie in PHP?
Von: mjd@op.net (Mark-Jason Dominus)
Datum: Mit, 17 Nov 1999 23:26:01 GMT
Message-ID: <80vdh5$f79$1@monet.op.net>
Newsgroups: comp.lang.perl.misc
Im Artikel <FDn*U3ado@news.chiark.greenend.org.uk> schrieb Ben Evans <bene@chiark.greenend.org.uk>:
> Also sind die grundlegenden Probleme:
> * dass es globale Variablen sind
>
> * dass, wenn man sich schlafen legt und dann vergisst, was
> man gerade gemacht hat, man versehentlich etwas Dummes
> macht, d. h. etwas löschen, was man eigentlich haben wollte,
> oder die falsche Subroutine aufrufen oder einen Laufzeitfehler
> erhalten.
>
> Also, wenn ich schnell mal eben ein Skript von ein paar hundert
> Zeilen schreibe, bei dem ich ziemlich genau weiss, was ich mache
> und nur ganz genau einschränkt von Benutzereingaben abhänge,
> dann gibt es auch kein Problem mit Soft References?
Ja, klar, es ist auch kein Problem, im Bett zu rauchen, solange man nicht einschläft und das Haus abfackelt.
Ich glaube du hast hast den entscheidenden Punkt übersehen. Globale Variablen haben ein Problem, richtig, das hast du verstanden. Wenn du symbolische Variablen benutzt, dann benutzt du globale Variablen ohne jede Beschränkung; das scheinst du nicht erfasst zu haben. Hier ein Beispiel: Vor in diesem Thread haben wir von jemandem gehört, dessen CGI-Programm das Folgende macht:
ReadParse();
while (($name, $wert) = each %in) { $$name = $wert }
Er "weiß", dass das sicher ist, denn er "weiß", dass die Namen in seinem Formular nicht mit seinen anderen Variablen in Konflikt geraten. Das ist genauso wie jemand, der sagt, es wäre in Ordnung, im Bett zu rauchen, weil er "weiß", dass er nicht einschläft.
In Wahrheit muss natürlich jeder, der das Formular editiert, im Programm gegenprüfen, um sicherzustellen, dass die Namen, die er benutzen möchte, noch nicht reserviert sind, und umgekehrt.
Die ganze fünfzigjährige Geschichte der Programmiersprachen war nicht mehr als ein gewaltiger Trend weg von dieser Art von Gegenprüfung (
cross-checking). Alles: Hochsprachen, Subroutinen, strukturierte Programmierung, Module, OOP und all der andere Kram dient dazu, neue und bessere Wege zu entdecken, wie man zwei Komponenten, wie ein Formular und das verarbeitende Programm, auf sichere Weise zusammenfügen kann, ohne dass man sie gegenprüfen muss.
Es ist unmöglich all das aufzuzählen, dass hier schiefgehen kann. Jemand könnte das Prüfen vergessen. Jemand könnte beschließen, nicht zu prüfen, weil er "weiß", dass der Name sicher ist. Jemand könnte beim Prüfen einen Fehler machen. Jemand könnte richtig prüfen, aber beim Eingeben des Namens einen Tippfehler machen. Jemand könnte aus Versehen einen Variablennamen in Perl benutzen, der eine Bedeutung hat, aber nicht explizit im Programm auftaucht, wie Exporter::VERBOSE, und so der Prüfung entgeht. Jemand könnte einen Namen wie Exporter::VERBOSE benutzen, und erst Monate später taucht plötzlich ein Problem auf, weil das Programm vorher das Modul nicht benutzt hat. Jemand könnte vorsätzlich gefälschte Daten schicken, einschließlich gefälschter Feldnamen wie
0,
/,
\, "" oder
*, um damit im Programm gezielt ein nicht vorhergesehenes Verhalten zu provozieren. Dies sind nur ein paar der vielen möglichen Problemfälle.
Man kann sich nicht einfach entschließen, vorsichtig zu sein, denn der Problembereich ist einfach zu umfangreich, um alle möglichen Dinge zu überblicken, die schief gehen könnten. Deshalb haben wir fünfzig Jahre mit dem Versuch verbracht, das gar nicht versuchen zu müssen. Man kann kein fünfzig Jahre bestehendes Forschungsproblem dadurch umgehen, dass man einfach vorsichtig ist; man muss auch methodisch vorgehen. Eine von möglichen zwölftausend Fallen zu übersehen heißt nicht "etwas Dummes zu machen", jedenfalls nicht mehr, als das Haus in Brand zu setzen, wenn man im Bett raucht; die Dummheit besteht darin, überhaupt erst im Bett zu rauchen.
Ich denke du hast da auch übersehen, dass eine Menge dieser Fallen unmöglich zu diagnostizieren sind. Ich weiß nicht, wie es bei dir aussieht, aber ich baue nicht absichtlich Fehler in meine Programme ein. Sie kommen da zufällig hinein. Obwohl ich vorsichtig bin. Wenn du hingehst und ein fehlerfreies Programm postulierst, das symbolische Referenzen benutzt, gut, offensichtlich gibt es in diesem Fall keine Probleme mit den symbolischen Referenzen; aber das ist ein Zirkelschluss - und dazu noch ein trügerischer. Rauchen im Bett ist gefährlich, egal ob man es schafft, dabei wach zu bleiben. Klar, wenn man ein perfekter Programmierer ist, muss man symbolische Referenzen nicht meiden. Man braucht auch selbstmodifizierenden Assemblercode nicht zu meiden. Und man kann auch gleich in Assembler programmieren, denn es ist deutlich effizienter als Perl zu benutzen. Aber einige von uns einfachen Sterblichen müssen uns mit den Auswirkungen von Bugs beschäftigen.
Der andere Aspekt dieses fünfzigjährigen Trends fort von globalen Variablen führt dazu, dass wir Programme in einzelne Komponenten aufteilen, die über umschriebene Wege miteinander kommunizieren. Warum machen wir das? Es dient dazu, dass wir herausfinden, wo das Problem liegt, wenn (nicht falls, wenn) wir Mist bauen. Wenn das CGI-Programm mit einem Fehler abbricht, braucht man sich keine Gedanken darüber zu machen, dass das Problem vielleicht bei einem der 1500 anderen Programme in /usr/local/bin liegt, denn das sind vollkommen getrennte Stücke Software, die nichts mit dem Programm zu tun haben, dass man gerade debuggen möchte. Wenn Subroutine X nicht funktioniert, kann man bei Subroutine X mit der Suche anfangen und die Suche von da aus erweitern; man muss nicht von Anfang an jede der 700 anderen Zeilen Code im Programm in Verdacht haben. Zumindest ist das die normale Annahme. Wenn du symbolische Referenzen benutzt, sind alle diese Annahmen, und ich meine wirklich alle, auf einmal wertlos. Wenn du
my $x = 1 schreibst, sind die Auswirkungen auf den aktuellen Block begrenzt. Wenn du
$main::x = 1 schreibst, kannst du die Auswirkungen abschätzen, indem du im Programm nachschaust, wer sich mit
$main::x beschäftigt. In dem Moment, wo du
$$name = 1 schreibst, weißt du nicht mehr, was das für Auswirkungen haben wird oder wo sie sich zeigen werden. Es könnte überall sein. Du hast soeben ein 25-Zeilen-Debugging-Problem in ein potenzielles 700-Zeilen-Debugging-Problem verwandelt.
Hier also eine kurze Zusammenfassung:
Eines der größten Probleme in der ganzen Computerprogrammierung ist die Verwaltung von Namensräumen und die Datenkapselung. Wenn man eine symbolische Referenz benutzt, wirft man damit vierzig Jahre schwer erworbenen Wissens weg.
Vorsichtig zu sein bedeutet nicht, dass man sich die möglichen Konsequenzen überlegt, während man gefährlich handelt; es bedeutet, gefährliche Handlungen von vornherein zu vermeiden. Sag nicht "ich weiß, dass es gefährlich ist, also bin ich wirklich vorsichtig". Sag "ich weiß, dass es gefährlich ist, also mache ich es nicht".
Bereite dich auf Fehlschläge vor, nicht auf den Erfolg, denn der Erfolg sorgt für sich selber. Du brauchst keinen Plan für den Fall, dass du nichts falsch machst, also gewöhne dir Programmiertechniken an, die dich beim Debugging unterstützen und es nicht vereiteln.
Entschuldigung, dass der Text so lang geworden ist, aber mir wurde bewusst, dass vieles hiervon nicht offensichtlich ist und es vielleicht noch niemand bisher ausdrücklich gesagt hat. Meine beiden Webseiten hierzu gehen in diese Richtung, aber ich glaube, ich war noch nicht deutlich genug.
Ich hoffe das hilft.
1 Larry Wall schreibt im
CamelBook:
[...] die drei großen Tugenden eines Programmierers: Faulheit, Ungeduld und Überheblichkeit. (Amn. d. Übers.)
--
HaraldBongartz - 12 Apr 2003