Keywords: Common, Beginner, Pitfall, Problem, Mistake
Fehler passieren allen. Die häufigsten sind hier aufgelistet. Ein weit größeres Problem sind die Fallen der Programmiersprachen. Häufig handelt es sich dabei um Abkürzungen, die dem Programmierer viel Tipparbeit abnehmen wollen und dann vor allem die Anfänger in große Schwierigkeiten bringen.
Der Artikel richtet sich an Trainer, die rasch die Fehler der Schüler entdecken müssen oder an Personen im Selbststudium, die nach einigen Progrämmchen die Problemquellen von vornherein ausschließen wollen.
Dieser Artikel behandelt vorwiegend C-ähnliche Programmiersprachen (Java, JavaScript, ...). Der generelle Teil am Anfang kann jedoch für alle Programmiersprachen angewendet werden. Java-Programmierer lassen den JavaScript-Teil weg; JavaScript-Programmierer lassen den Java-Teil weg.
Ein häufiger "Fehler" ist die Tatsache, dass sich Anfänger
zunächst fragen, welche Sprache sich zum Erlernen von
Programmierfähigkeit eignet. Ist jedoch ein Ziel vor Augen
(Web-Applikation, iPhone-App, Android-App, ...), so ist die Sprache
in der Regel vorgegeben.
Will man einfach nur Programmieren lernen, so bieten sich diverse
Sprachen an. Zu den meisten gibt es gute Lehrbücher oder Tutorials
im Internet.
Tipps fürs Selbststudium
In diesem Fall ist es jedoch wichtiger, als "die richtige" Sprache zu finden, die richtigen Konzepte zu erlernen. Viele Trainingsaufgaben zu den folgenden Konzepten finden sich in meinem Buch (Programmieren lernen, Philipp Gressly Freimann | Martin Guggisberg, 2011 Orell Füssli Verlag) oder auf der dazu gehörigen Webseite.
Die folgenden Grundkonzepte sollten bekannt sein:
Datentyp | Werte werden im Computer aus Nullern (0) und Einsen (1)
zusammengesetzt:
Boolean: wahr = 1; falsch = 0 Zweiersystem: 0, 1, 10, 11, 100, 101, 110, 111, 1000, 1001, ... Buchstabentabellen: 'a' = 97 = 1100001 (z. B. ASCII) Gebrochene Zahlen: 3.5 = 01000000011000000000000000000000 (ieee 754 Notation) Zeichenketten (Strings) "Hallo" = 'h', 'a', 'l', 'l', 'o' |
Variable | Jede Variable ist entweder nicht initialisiert oder sie enthält ein Bitmuster, das einem Wert eines Datentypen entspricht. |
Ausdrücke (Term) | Ausdrücke werden meist in einer algebraischen Form
dargestellt: 3+9, 2*durchmesser, ... Ein Ausdruck ist etwas, das einen Wert aufweist; also etwas, das einer Variable zugewiesen werden kann, oder das in einem größeren Ausdruck verwendet werden kann. Ausdrücke sind:
|
Anweisung (Statement) | Die typischen drei Anweisungen sind:
|
Kontrollfluss | Sequenz : a(); b(); c(); ... Selektion: if (...) {...} Iteration: while(...) {...} |
Subroutinen | Unterprogramme selbst schreiben:
«function» ausgabe(wert) {
print("Das Resultat ist: ", wert);
} |
Felder (Arrays) | Anstatt Variable a1, a2, a3, ..., a20 zu deklarieren, deklarieren wir a[1..20]. Das hat den Vorteil, die einzelnen Variable mit einer Indexvariablen anzusteuern: a[index] |
«Programmieren lernt man, indem man die ca. 20 wichtigsten Grundkonzepte kennenlernt.»
Falsch: Es ist zwar richtig, dass die Programmierung wie die Kunstmalerei, die Geometrie oder das Go-Spiel aus wenigen Grundkonzepten und wenigen wichtigen Regeln aufgebaut sind. Aber Malen lerne ich auch nicht mit dem Kennenlernen der Theorie zu Pinsel, Bleistift, Farben und Leinwand. Hier gilt: üben, üben und nochmals üben. Nochmals: programmieraufgaben lösen!
Die Grundlegenden debug-Möglichkeiten sind:
print: printf(), alert(), System.out.println(),
... kurz, im Folgenden print()
genannt. Damit kann an beliebiger Stelle zwischen den Zeilen gelesen werden.
system.log: Irgendwelche Loging-Funktionen,
wenn ein print() nicht ausgeführt
werden kann. Beispiel: Fehlerkonsole bei Javascript.
Debugger: Verwenden eines Debuggers
(Zeilenweise, bzw. Anweisung für Anweisung). Die Unterscheidung: step-into
und step-over sollten bekannt sein.
Die Fehlersuche wächst exponentiell zur Anzahl der programmierten Zeilen. Dabei bietet es sich gerade für Anfänger an, jeden programmierten Schritt mittels Ausgabe 'print()' oder Debugger sofort zu überprüfen. Komplizierte Algorithmen sollten zunächst von Hand (Wertetabelle, Flussdiagramm, Objekt-Theater, ...) mit Bleistift und Papier vor dem Programmieren einmal durchgespielt werden.
1.
2.
3.
Oft werden Fehlermeldungen nicht gelesen, oder nur flüchtig und dann nicht verstanden. Meist sind sie jedoch ganz nützlich. So heißt z. B. 'missing semicolon' nichts anderes als "Fehlender Strichpunkt(;)".
Aufruf von doSomething(); mit gegebener Funktion
Offensichtlich wurde beim Aufruf das 's' groß geschrieben. Abhilfe: längere Namen sollten kopiert, statt getippt werden.
Auch wenn die Anweisungsreihenfolge innerhalb einer Funktion korrekt ist, läuft das Programm nicht:
Wird nun haupt(); nie aufgerufen, so kann es noch so korrekt sein. Dies kann einfach mit einem print(); auf der ersten Zeile gemerkt werden.
Gerade Anfänger haben das Verständnisproblem, dass die Reihenfolge des Funktionsaufrufs wichtig ist, und nicht die Reihenfolge der Funktionsdefinitionen. So ist es unnötig, die Reihenfolge wie folgt vorzugeben:
Die Reihenfolge obiger Funktionen kann komplett umgestellt werden. Wichtig ist, dass der Aufruf im Hauptprogramm korrekt ist:
Fehlermeldung: x ist nicht initialisiert.
Abhilfe:
Der folgende Code produziert eine Endlosschleife:
Abhilfe: Ein einfaches print(); am Anfang der Schleife wird dies zum Ausdruck bringen.
Oft werden die Symbole < und > beziehungsweise die Funktionen min() und max() vertauscht.
Lösung < statt >.
Der Debugger kann diesen Fehler leicht finden. Warum ist 0 > 101 denn falsch?
Die Eingabe von Zahlen in ein Programm geschieht häufig via Zeichenketten (Strings). Diese müssen zunächst in Zahlen umgewandelt werden.
Lösung:
Speziell bei der Division ist es wichtig, sich zu überlegen, ob man mit ganzen oder mit gebrochenen Zahlen (Dezimalbrüchen) rechnen muss:
Nur wenige Programmiersprachen (z. B. PL/1) übergeben
standardmäßig die Variable an Unterprogramme.
Die meisten modernen Sprachen übergeben die Werte - m. a. W. sie
betrachten die Variable als Ausdrücke und werten diese vor der
Übergabe an das Unterprogramm aus. Beispiel:
Programmieren für Maschinen kann jeder. Wir schreiben Programme für Menschen; das ist die hohe Kunst. Gehen Sie davon aus, dass Ihr Programmcode nicht einfach funktionieren muss, sondern dass er auch unterhalten werden will. Jemand muss Ihren Code verstehen (vielleicht Sie selbst?)!
Verwenden Sie immer sprechende Namen:
Was denn ???:
Nonsense !!! :
Schon besser:
Noch besser:
State of the Art:
So nicht!
Aber so
Die folgenden Fallen in C-Sprachen betreffen nicht nur Anfänger in der Computerprogrammierung. Auch alte Hasen, die eine andere Sprache gekannt hatten, oder die schon länger keine C-Sprache mehr programmiert haben, werden getäuscht. Es handelt sich i. d. R. um abkürzende Schreibweisen, die Weniger Tipparbeit, aber massiv mehr Fehlerquellen bedeuten.
Folgendes sieht man leider zu oft:
In obigem Code auf Zeile 3, 5 und 7 stehen keine Anweisungen, sondern Ausdrücke! Einige Programmiersprachen erlauben diese Syntax, führen dann aber nichts aus!
Was gemeint war ist etwas in der Art:
Hierbei handelt es sich um eine der perfidesten Fallen der C-Sprachen. Es handelt sich nicht um einen Anfängerfehler sondern um eine Unzulänglichkeit oder eben eine "Falle" der Programmiersprache. C-Sprachen unterscheiden beim Aufruf nicht, ob ein Unterprogramm einen Wert zurückgibt oder nicht. Somit funktioniert der folgende Code ohne Compilerfehler:
Der korrekte Aufruf müsste lauten:
Andere Programmiersprachen (wie z. B. PL/1) sind in diesem Bereich
hoch zu loben!
Am besten gewöhnt man sich an, beim Aufruf von (nicht void)
Funktionen, dies zu kommentieren, falls das Funktionsresultat
vernichtet werden soll:
Eine weitere Falle bietet die Möglichkeit, bei Selektionen oder Iterationen, die nur aus einer einzigen Anweisung bestehen, die geschweiften Klammern wegzulassen.
Die generelle Syntax der while-Schleife (Iteration) ist wie folgt (abgesehen vom else ist das if analog):
Folgender abkürzende Code (ohne {}) wurde zwar korrekt geschrieben:
Wird dieser Code mit einer zweiten Anweisung ergänzt, so geschieht das unerwartete:
Obige zweite Anweisung (anweisung2();) wird erst nach der Schleife ausgeführt, dafür in jedem Fall
Ein weiteres Problem ergibt sich durch die Strichpunkte:
Was aufs das selbe herauskommt wie
Die Lösung des Problems liegt darin immer die geschweiften Klammern zu verwenden und die Strichpunkte immer erst nach einer Anweisung zu setzen:
Wir haben gelernt, dass es robuster ist, die geschweiften Klammern bei while und if immer anzufügen.
Die klassische if/else-Konstruktion sieht wie folgt aus:
Hier gibt es jedoch eine
Wenn wir eine Mehrfach-Selektion mit mindestens drei (3) Varianten haben, so wird die Anweisung rasch unübersichtlich:
Lassen wir hier beim ersten else-Block die oben generell vorgeschlagenen geschweiften Klammern weg...
... und rücken neu ein (so, bei einer oder zwei Anweisungen pro Block) ...
... oder so (bei mehreren Anweisungen) ...
... so verletzen wir zwar die "Immer geschweifte Klammern"-Regel, doch es wird viel lesbarer und wartbarer.
Wenn es auch Tipparbeit spart, so sollten abkürzende Operatoren nur sparsam und gezielt eingesetzt werden.
So ist die folgende Anweisung kompletter Nonsense; und das nur, weil meine Schüler denken, sie haben mit dem ++-Operator etwas schlaues gelernt:
i = i++; anstelle von i++;.
Damit solche Probleme gar nicht erst auftreten, verwende ich als Trainer in den ersten Semestern den ++-Operator überhaupt nicht und schreibe statt dessen konsequent:
Dies funktioniert in den meisten Programmiersprachen. Eine Adaption ist daher einfach. Natürlich darf man den gewitzten Schülern den ++-Operator zeigen, muss sie dann aber auf deren Gefahren hinweisen. Ebenso ist es nützlich (gerade im Zusammenhang mit Feldern (= Arrays)) die for-Schleife als zusammenhängendes Konstrukt einzuführen:
Hier «wird es einfach so gemacht». Damit geht auch die Schleifenvariable nicht vergessen.
Sind wir dann endlich gewohnt, so schreiben wir später sehr wohl:
Genau genommen kann die folgende Abkürzung effizient und elegant sein, wenn sie korrekt angewendet wird. Sagen wir, wir wollen in einem Array (Feld) einen Wert an variabler Stelle (nennen wir sie mal einfach index) um eine Konstante (sagen wir 3) vergrößern:
Gefährlich wirds bei einer einzigen Variable:
Schreibt jemand fällschlicherweise nun
so ists um ihn geschehen (ich habe dies selbst bei erfahrenen Programmierern erlebt). Es wird immer einfach die Zahl (+)1 in die Variable i geschrieben.
Um nicht in die Falle zu tappen, bleiben wir doch besser bei
Wohlweislich haben wir aber gerade bei komplexen Indizes damit ein weiteres Problem: Die Arrayposition muss doppelt berechnet werden (Performanz) und beim doppelten Schreiben der Indizes, können Fehler passieren. Also auch so nicht:
Tippfehler sind so schnell passiert. Hier dann doch besser:
oder so:
Merke: Bei += gilt der goldene Mittelweg!
Da in C-Sprachen eine Zuweisung gleichzeitig ein Ausdruck ist, sind die folgenden Fehler - gestatten Sie das Wortspiel - vorprogrammiert:
if(x = 4) { ... } |
if(ok = true) { ... } |
Hier wird zugewiesen, statt verglichen! | |
Lösung: | |
Konstanten wenn möglich nach links: | Boole'sche Variable direkt verwenden: |
if(4 == x) { ... } |
if(ok) { ... } |
Um zu prüfen, ob eine Zahl in einem gegebene Intervall (sagen wir zwischen 10 und 20) liegt, müssen zwei Vergleiche angestellt werden:
Obschon ein switch wie eine Fallunterscheidung aussieht, ist es lediglich eine Einsprungtabelle! In diesem Sinne ist weder C noch Java eine geeignete Sprache, um nach den Vorgaben der strukturierten Programmierung vorzughen. Ein Sprung ist dabei nämlich nur erlaubt, wenn es sich um eine Subroutine, eine Schleife oder eine Selektion handelt. Betrachten wir dazu den folgenden Code:
Drei Lösungen:
1. Am besten ist es ganz auf die switch-Anweisung zu Gunsten des Polymorphismus zu verzichten.
2. Sobald auf ein break bewusst verzichtet wird, so soll dies mit Kommentar angegeben werden (z. B. //falls through).
3. Im Entwicklungstool (sofern vorhanden) eine Fehlermeldung bei "missing break" einschalten und komplett aufs "falls through" zu verzichten:
Häufig haben wir ein Feld (Array) gegeben (z. B. fld) und kennen davon die Anzahl der Elemente (z. B. anz = 20)
Nun kann in C-Sprachen der folgende Code nicht funktionieren:
Lösung: C-Sprachen indizieren immer ab Element Null (0). Mit anderen Worten: Das erste Element hat index Null, das zweite Element hat index 1 usw.
Obschon sich Java in den letzten Jahren sehr rasch entwickelt hat, so bleiben vor allem für Anfänger viele Fallen vorhanden. Diese zu korrigieren ist beinahe nicht mehr möglich, denn in der Zwischenzeit ist bereits zu viel Code vorhanden, der unter Umständen nicht mehr funktionieren würde, wenn diese Fehler korrigiert würden. Der Pfad der Technik ist bereits zu weit fortgeschritten.
In Java müssen alle Anweisungen immer innerhalb von Methoden stehen (Ausnahmen sind sog. Initialisierer). Somit wird folgender Code nicht funktionieren:
Tipp an Trainer: Zeige wo immer möglich - und vor allem den Programmieranfängern - keine Initialisierungen außerhalb von Methoden. Also bitte nicht so:
Doch besser so:
Strings in Java verhalten sich nicht wie herkömmliche Objekte. Strings sind mit der virtuellen Maschine stark verknüpft. Es geht sogar so weit, dass Strings nicht veränderbar sind, auch wenn das der Name ihrer Methoden manchmal vermuten lässt. Wie folgt kann einem bestehenden String beispielsweise kein einziger Leerschlag entfernt werden:
Doch so funktioniert es:
Betrachten Sie die folgenden String Definitionen:
Nun gilt, dass alle Vergleiche mit x.equals(y) immer wahr (true) sind. Hingegen sind nicht alle Vergleiche mit dem ==-Operator zufriedenstellend. So gelten die folgenden Beziehungen:
aber:
Der '+'-Operator (plus) zählt zwei Zahlen zusammen. Ist jedoch einer der beiden Operatoren ein String, so wird zunächst der andere auch in einen String verwandelt, bevor die beiden Strings miteinander einfach verkettet werden.
Daher ist es nicht verwunderlich, dass der folgende Ausdruck
Es gibt kaum eine andere Programmiersprache mit dem folgenden Fehler! Warum sollte denn 127 = 127 sein, aber 128 ≠ 128 ?
Daher: Java Integers (Ausnahme primitive Typen int,... ) immer mit equals() vergleichen.
Java Referenzvariable zeigen auf Objekte. Wird eine Referenz kopiert, so wird nicht ein neues Objekt angelegt: Die Kopie zeigt auf das selbe Objekt:
Um ein Objekt zu kopieren müssen alle Attributwerte auch kopiert werden (clone):
Obschon sich Java Mühe gab, sinnvolle Namen zu vergeben, passierte es hin und wieder dennoch, dass die Entwickler gehörig daneben griffen, wie folgendes Beispiel zeigt.
Bei der Klasse java.io.File handelt es sich nicht um eine Datei (engl. File). Es handelt sich vielmehr um einen möglichen Eintrag im File-System. Genaugenommen sind hier Methoden zum Umgang mit Dateinamen, Verzeichnsnamen und generell dem File-Handling anzutreffen. Ein File kann damit aber weder geöffnet werden, noch können Inhalte in einem File verändert oder gelesen werden.
Das Problem schien rasch erkannt und es wurde innerhalb der Klasse File eine Methode boolean isFile() eingeführt, welche vom File prüfen soll, ob es sich um ein File handelt. Dies machte das Problem aber nur noch schlimmer: Um welche Art von File soll sich das File denn handeln? Um ein Java-File oder um ein File im üblichen Wortgebrauch?
Besser gewesen wären Namen wie "FileHandling", "DirectoryEntry", ...
Obschon es eine Klasse Boolean gibt, verhalten sich deren Objekte im Boole'schen Kontext leider nicht korrekt:
Dieser Fehler ist kein generelles Programmierer Anfängerproblem. Aber JavaScript-Anfänger sollten unbedingt vor den ersten größeren Programmen etwas mit ==, ===, 0,null,undefined und NaN üben.
Nutzereingaben in das Programm sind meist Zeichenketten (Strings), auch wenn diese oft Zahlen bedeuten sollten.
Abhilfe schafft man, indem man die Strings zunächst in Zahlen verwandelt. Dazu bieten sich die folgenden Methoden an:
Alle drei Verfahren müssen zunächst die Zeichenkette in eine Zahl verwandeln und rechnen danach korrekt.
Es ist auch ratsam nicht die Methode parseInt() zu verwenden, denn diese kann Nutzereingaben auch falsch verstehen, wohingegen bei obigem Vorgehen NaN (not a number) resultiert:
In JavaScript sind Variable standardmäßig global. Es ist gerade in
größeren Applikationen von Vorteil, jede Variable vor dem ersten
Gebrauch zu deklarieren.
Sei dies als Globale Variable:
Oder als lokale Variable:
Generell gilt: Je weniger globale Variable verwendet werden, umso geringer die Gefahr, Namen mehrfach zu vergeben.
Nie sollte man jedoch die Deklaration mittels var vergessen. Variablen werden dadurch sofort global!
Auch wenn JavaScript dies nicht vorschreibt, so sollten Anweisungen dennoch stets mit einem Strichpunkt (= Semikolon ';') beendet werden. Dies hat zwar den Nachteil, dass man sich evtl. angewöhnt auch dort einen Strichpunkt zu setzen, wo dies nicht gewünscht ist:
Der klare Vorteil hingegen kommt dann zur Geltung, wenn wir mehrere Anweisungen auf eine Zeile bringen wollen, oder wenn wir Anweisungen benötigen, die über mehrere Zeilen hinweggehen.
Obschon es bereits mehrere verbindliche Standards gibt, wie
sich gewisse JavaScript Funktionen verhalten sollten, so gibt es
dennoch immer noch Abweichungen im Verhalten verschiedener
Browser.
Es lohnt sich, die JavaScript-Applikationen auf den gängigsten
Webbrowsern zu testen.
Javascript sollte nie herhalten, die Sicherheit von Webapplikationen zu verbessern. Da Javascript im Browser läuft, hat der Server keine Kontrolle darüber, ob ein Skript korrekt abläuft, oder ob ein Programmierer das Skript auf der Clientseite sogar modifiziert
Javascript kann aber bei Formularen sehr gut genommen werden, um dem Benutzer die Eingabe zu erleichtern. So kann bereits im Browser geprüft werden, ob eine E-Mailadresse syntaktisch korrekt ist oder z. B. ein Kalenderdatum kann via Auswahlfenster eingegeben werden.
Bedenken Sie aber immer, dass ca. 1.5% der User ohne JavaScript surft. Mit ausgeschaltetem JavaScript sollte ein Formular immer auch noch bedient werden können. (Dies gilt natürlich nicht für Browser-Spiele oder sonstige reine Browser-Applikationen.)
Bitte Feedbacks direkt an mich (Philipp): Kontaktformular.
© Philipp Gressly Freimann 2012-2017