Wie man kleine Programme debuggt

(Aus Eric Lipperts englischem Original)

Eine der häufigsten Kategorien von schlechten Fragen, die auf StackOverflow zu sehen ist, ist:

Ich habe dieses Programm für meine Aufgabe geschrieben und es funktioniert nicht.
[20 Zeilen Code].

Und… das ist es.

Wenn Sie dies lesen, stehen die Chancen gut, dass jemand, einen Link hierher von Ihrer StackOverflow-Frage gesetzt hat, kurz bevor sie geschlossen und gelöscht wurde. (Wenn Sie dies lesen und das nicht der Fall ist, sollten Sie Ihre Lieblingstipps für das Debuggen kleiner Programme in den Kommentaren hinterlassen.)

StackOverflow ist eine Frage-und-Antwort-Seite für spezifische Fragen zum aktuellen Code; „Ich habe einen Buggy-Code geschrieben, den ich nicht beheben kann“ ist keine Frage, es ist eine Geschichte und nicht einmal eine interessante Geschichte. „Warum erzeugt das Subtrahieren von Eins von Null eine Zahl, die größer als Null ist, so dass mein Vergleich mit Null in Zeile 12 fälschlicherweise wahr wird“, ist eine spezifische Frage nach dem tatsächlichen Code.

Sie bitten also das Internet, ein defektes Programm, das Sie geschrieben haben, zu debuggen. Sie haben wahrscheinlich noch nie gelernt, wie man ein kleines Programm debuggt, denn lassen Sie mich Ihnen sagen, was Sie gerade tun, ist kein effizienter Weg, um dieses Problem zu lösen. Heute ist ein guter Tag, um zu lernen, wie man Dinge selbst debuggt, denn StackOverflow wird Ihnen nicht helfen, Ihre Programme für Sie zu debuggen.

Ich gehe davon aus, dass Ihr Programm tatsächlich kompiliert, aber seine Aktion falsch ist, und dass Sie außerdem einen Testfall haben, der zeigt, dass es falsch ist. Nachfolgend ist beschrieben, wie Sie den Fehler finden.

Zuerst schalten Sie alle Compiler-Warnungen ein. Es gibt keinen Grund, warum ein 20-Zeilen-Programm auch nur eine einzige Warnung erzeugen sollte. Warnungen kommen vom Compiler, der Ihnen sagt: „Dieses Programm kompiliert, aber tut nicht, was Sie denken, dass es tut“, und da das genau die Situation ist, in der Sie sich befinden, obliegt es Ihnen, auf diese Warnungen zu achten.

Lesen Sie sie sehr sorgfältig durch. Wenn Sie nicht verstehen, warum eine Warnung ausgegeben wird, ist das eine gute Frage für StackOverflow, da es sich um eine spezifische Frage über den tatsächlichen Code handelt. Achten Sie darauf, den genauen Text der Warnung, den genauen Code, der sie erzeugt, und die genaue Version des verwendeten Compilers zu veröffentlichen.

Wenn Ihr Programm immer noch einen Fehler hat, besorgen Sie sich ein Quietscheentchen. Oder wenn ein Quietscheentchen nicht verfügbar ist, besorgen Sie sich einen anderen Informatikstudenten, damit geht es ähnlich gut. Erklären Sie der Ente mit einfachen Worten, warum jede Zeile jeder Methode in Ihrem Programm offensichtlich korrekt ist. Irgendwann werden Sie das nicht mehr tun können, entweder weil Sie die Methode, die Sie geschrieben haben, nicht verstehen, oder weil sie falsch ist, oder beides. Konzentrieren Sie Ihre Bemühungen auf diese Methode; dort liegt wahrscheinlich der Fehler. Im Ernst, das Quietscheentchen-Debugging funktioniert wirklich. Und wie der legendäre Programmierer Raymond Chen in einem Kommentar zum Englischen Original dieses Artikels betont, wenn man der Ente nicht erklären kann, warum man eine bestimmte Aussage ausführt, vielleicht liegt das daran, dass man mit der Programmierung begonnen hat, bevor man einen Angriffsplan hatte.

Sobald Ihr Programm sauber kompiliert und die Ente keine größeren Einwände erhebt, und es dennoch immer noch einen Fehler gibt, schauen Sie nach, ob Sie Ihren Code in kleinere Methoden aufteilen können, von denen jede genau eine logische Operation durchführt. Ein häufiger Fehler unter allen Programmierern, nicht nur Anfängern, ist es, Methoden zu entwickeln, die versuchen, mehrere Dinge zu tun und sie dadurch schlecht zu machen. Kleinere Methoden sind leichter zu verstehen und daher sowohl für Sie als auch für die Ente einfacher, die Fehler zu erkennen.

Während Sie Ihre Methoden in kleinere Methoden umwandeln, nehmen Sie sich eine Minute Zeit, um eine technische Spezifikation für jede Methode zu schreiben. Auch wenn es nur ein oder zwei Sätze sind, hilft eine Spezifikation. Die technische Spezifikation beschreibt, was die Methode macht, welches formale Eingaben sind, welches erwartete Ausgaben sind, welche Fehlerfälle vorliegen und so weiter. Oftmals werden Sie beim Schreiben einer Spezifikation feststellen, dass Sie vergessen haben, einen bestimmten Fall in einer Methode zu behandeln, und das ist dann der Fehler.

Wenn Sie noch einen Fehler haben, dann überprüfen Sie zuerst, ob Ihre Spezifikationen alle Vor- und Nachbedingungen jeder Methode enthalten. Eine Vorbedingung ist eine Sache, die wahr sein muss, bevor ein Methodenrumpf korrekt funktionieren kann. Eine Nachbedingung („Postcondition“) ist eine Sache, die wahr sein muss, wenn eine Methode ihre Arbeit beendet hat. Eine Vorbedingung könnte beispielsweise sein: „Dieses Argument ist ein gültiger Nicht-Null-Zeiger“ oder „die übergebene verknüpfte Liste hat mindestens zwei Knoten“, oder „dieses Argument ist eine positive ganze Zahl“, oder was auch immer. Eine Nachbedingung könnte sein: „Die verknüpfte Liste hat genau ein Element weniger als bei der Eingabe“, oder „ein bestimmter Teil des Arrays ist jetzt sortiert“, oder was auch immer. Eine Methode, bei der eine Vorbedingung verletzt ist, zeigt einen Fehler im Aufrufer an. Eine Methode, bei der eine Nachbedingung verletzt wird, auch wenn alle ihre Voraussetzungen erfüllt sind, zeigt einen Fehler in der Methode an. Oftmals, wenn Sie Ihre Vor- und Nachbedingungen erneut angeben, werden Sie einen Fall bemerken, den Sie in der Methode vergessen haben.

Wenn Sie immer noch einen Fehler haben, dann lernen Sie, wie man Behauptungen („Assertions“) schreibt, die Ihre Vor- und Nachbedingungen überprüfen. Eine Behauptung ist wie ein Kommentar, der Ihnen sagt, wann eine Bedingung verletzt wird; eine verletzte Bedingung ist fast immer ein Fehler. In C# können Sie sagen, using System.Diagnostics; am Anfang Ihres Programms und dann Debug.Assert(value != null); oder was auch immer. Jede Sprache hat einen Mechanismus für Behauptungen; Holen Sie sich jemanden, der Ihnen beibringt, wie man sie in Ihrer Sprache benutzt. Setzen Sie die Vorbedingungs-Behauptungen oben auf den Methodenrumpf und die für die Nachbedingungen, bevor die Methode zurückkehrt. (Beachten Sie, dass dies am einfachsten ist, wenn jede Methode einen einzigen Rückgabepunkt hat.) Wenn Sie jetzt Ihr Programm ausführen, werden Sie, wenn eine Behauptung ausgelöst wird, über die Art des Problems informiert, und es wird nicht so schwer zu debuggen sein.

Nun schreiben Sie für jede Methode Testfälle, die das korrekte Verhalten überprüfen. Testen Sie jedes Teil unabhängig voneinander, bis Sie Vertrauen in das Teil haben. Testen Sie viele einfache Fälle; wenn Ihre Methode Listen sortiert, versuchen Sie es mit der leeren Liste, einer Liste mit einem Element, zwei Elementen, drei Elementen, die alle gleich sind, drei Elementen, die in umgekehrter Reihenfolge sind, und ein paar langen Listen. Die Chancen stehen gut, dass Ihr Fehler in einem einfachen Fall auftaucht, was die Analyse erleichtert.

Schließlich, wenn Ihr Programm noch einen Fehler hat, schreiben Sie auf ein Blatt Papier die genaue Aktion, die Sie erwarten, dass das Programm auf jeder Zeile des Programms für den defekten Fall übernimmt. Ihr Programm ist nur zwanzig Zeilen lang. Sie sollten in der Lage sein, alles aufzuschreiben, was es tut. Gehen Sie nun mit einem Debugger durch den Code und untersuchen Sie jede Variable bei jedem Schritt des Weges, und überprüfen Sie Zeile für Zeile, was das Programm gegen Ihre Liste macht. Wenn es etwas tut, das nicht auf Ihrer Liste steht, dann hat entweder Ihre Liste einen Fehler, in diesem Fall haben Sie nicht verstanden, was das Programm macht, oder Ihr Programm hat einen Fehler, in diesem Fall haben Sie es falsch kodiert. Beheben Sie die Sache, die falsch ist. Wenn Sie nicht wissen, wie Sie es beheben sollen, haben Sie zumindest jetzt eine spezifische technische Frage, die Sie an StackOverflow stellen können! So oder so, wiederholen Sie auf diesem Prozess, bis die Beschreibung der korrekten Ausführung des Programms und die tatsächliche Ausführung des Programms übereinstimmen.

Während Sie den Code im Debugger ausführen, empfehle ich Ihnen, auf kleine Zweifel zu hören. Die meisten Programmierer haben eine natürliche Tendenz zu glauben, dass ihr Programm wie erwartet funktioniert, aber Sie debuggen es, weil diese Annahme falsch ist! Sehr oft habe ich ein Problem debuggt und aus dem Augenwinkel gesehen, wie das kleine Highlight in Visual Studio auftaucht, was bedeutet, dass „ein Speicherort gerade geändert wurde“, und ich weiß, dass dieser Speicherort nichts mit meinem Problem zu tun hat. Warum wurde es dann modifiziert? Ignorieren Sie diese nagenden Zweifel nicht; untersuchen Sie das seltsame Verhalten, bis Sie verstehen, warum es entweder richtig oder falsch ist.

Wenn das nach viel Arbeit klingt, dann deshalb, weil es das auch ist. Wenn Sie diese Techniken nicht auf Zwanzig-Zeilen-Programmen anwenden können, die Sie selbst geschrieben haben, werden Sie sie wahrscheinlich auch nicht auf Zwei-Millionen-Zeilen-Programmen anwenden können, die von jemand anderem geschrieben wurden. Aber das ist das Problem, das Entwickler in der Industrie jeden Tag lösen müssen. Fangen Sie an zu üben!

Und wenn Sie das nächste Mal eine Aufgabe schreiben, schreiben Sie die Spezifikation, Testfälle, Vorbedingungen, Nachbedingungen und Behauptungen für eine Methode, bevor Sie den Rumpfder Methode schreiben! Es ist viel weniger wahrscheinlich, dass Sie einen Fehler haben, und wenn Sie einen Fehler haben, ist es sehr viel wahrscheinlicher, dass Sie ihn schnell finden können.

Diese Methodik wird nicht jeden Fehler in jedem Programm finden, aber sie ist sehr effektiv für die Art von kurzen Programmen, die Anfänger-Programmierer als Hausaufgabe zugewiesen bekommen. Diese Techniken werden dann auf das Auffinden von Fehlern in nicht-trivialen Programmen ausgeweitet.

(Aus Eric Lipperts englischem Original)