7.2 Debuggen mit Programmcode
7.2.1 Einführung
In Abschnitt 7.1 haben wir uns mit Fehlern beschäftigt, die nach der erfolgreichen Kompilierung zur Laufzeit auftreten können und, falls sie nicht behandelt werden, unweigerlich zum Absturz des Programms führen. Vielleicht noch schlimmer sind Fehler, die weder vom Compiler erkannt werden noch einen Laufzeitfehler verursachen. Es sind die logischen Fehler, aus denen ein falsches oder zumindest unerwartetes Ergebnis resultiert. Um logische Fehler aufzuspüren, muss die Anwendung unter Zuhilfenahme des integrierten Debuggers untersucht werden.
Das .NET Framework stellt Ihnen eine Reihe von Hilfsmitteln zur Verfügung, um den Programmcode zu debuggen. Die Spanne reicht von der einfachen Ausgabe von Meldungen im Ausgabe-Fenster bis zur Umleitung der Meldungen in eine Datei oder das Windows-Ereignisprotokoll. Dabei können Sie das Laufzeitverhalten einer Anwendung sowohl mit Programmcode als auch mit der Unterstützung von Visual Studio überprüfen. Wir werden in den nächsten Abschnitten auf alle Debugging-Techniken eingehen.
7.2.2 Die Klasse »Debug«
In den vorangegangenen Beispielen haben wir uns sehr häufig eines Kommandos bedient, um beispielsweise den Inhalt von Variablen zu überprüfen. Es war die Methode WriteLine der Klasse Console:
int value = 12;
Console.WriteLine(value);
Diese Technik hat zur Folge, dass die Ausgabe an der Konsole unübersichtlich wird und zwischen den erforderlichen Programminformationen immer wieder Informationen zu finden sind, die im Grunde genommen nur dazu dienen, die Entwicklung zu unterstützen. Bevor ein solches Programm an den Kunden ausgeliefert wird, müssen die Testausgaben aus dem Programmcode gelöscht werden.
Die Entwicklungsumgebung bietet uns eine bessere Alternative an. Dazu wird die Ausgabe nicht in das Konsolenfenster geschrieben, sondern in das Ausgabe-Fenster von Visual Studio. Standardmäßig wird dieses Fenster am unteren Rand der Entwicklungsumgebung angezeigt. Sie können es sich anzeigen lassen, indem Sie im Menü Ansicht den Menüpunkt Ausgabe wählen.
Abbildung 7.6 Das Fenster »Ausgabe«
Sie haben dieses Fenster wahrscheinlich schon häufig gesehen und aufmerksam seinen Inhalt gelesen, denn bei jeder Kompilierung werden hier Informationen ausgegeben, beispielsweise ob die Kompilierung fehlerfrei war. Das Ausgabe-Fenster zeigt uns aber nicht nur Informationen an, die der Compiler hineinschreibt, wir können auch eigene Meldungen in dieses Fenster umleiten.
Eine Debug-Information in das Ausgabe-Fenster zu schreiben, ist genauso einfach wie die Ausgabe an der Konsole. Wir müssen nur die Anweisung
Console.WriteLine("...");
durch
Debug.WriteLine("...");
ersetzen. Debug ist eine nicht ableitbare Klasse des Namespaces System.Diagnostics, die ausschließlich statische Member bereitstellt.
Die Methode Debug.WriteLine unterscheidet sich von der Methode Console.WriteLine dahingehend, dass sie keine Formatierungsmöglichkeiten erlaubt. Um mehrere Informationen in einer gemeinsamen Zeichenfolge unterzubringen, müssen Sie daher den Verknüpfungsoperator »+« benutzen:
Debug.WriteLine("Inhalt von value = " + value);
Programmablaufinformationen anzeigen
Debug.WriteLine ist mehrfach überladen und kann ein Argument vom Typ string oder object entgegennehmen. Eine parameterlose Überladung gibt es nicht.
public static void WriteLine(object value);
public static void WriteLine(string message);
Optional können wir auch ein zweites string-Argument übergeben, das eine detaillierte Beschreibung bereitstellt, die vor der eigentlichen Debug-Information ausgegeben wird.
public static void WriteLine(object value, string category);
public static void WriteLine(string message, string category);
Sehen wir uns das an einem Beispiel an. Die Anweisung
Debug.WriteLine("Inhalt von value = " + value, "Variable value");
wird in das Ausgabe-Fenster
Variable value: Inhalt von value = 34
schreiben – vorausgesetzt, der Inhalt von value ist 34.
Neben WriteLine sind in der Klasse Debug noch weitere Methoden zur Ausgabe von Informationen definiert. Tabelle 7.2 gibt darüber Auskunft.
Methode | Beschreibung |
Schreibt Debug-Informationen ohne Zeilenumbruch. |
|
Schreibt Debug-Informationen mit Zeilenumbruch. |
|
Schreibt Debug-Informationen ohne Zeilenumbruch, wenn eine bestimmte Bedingung erfüllt ist. |
|
Schreibt Debug-Informationen mit Zeilenumbruch, wenn eine bestimmte Bedingung erfüllt ist. |
Die beiden zuletzt aufgeführten Methoden WriteIf und WriteLineIf schreiben nur dann Debug-Informationen, wenn eine vordefinierte Bedingung erfüllt ist. Damit lässt sich der Programmcode übersichtlicher gestalten. Beide Methoden sind genauso überladen wie Write bzw. WriteLine, erwarten jedoch im ersten Parameter zusätzlich einen booleschen Wert, z. B.:
public static void WriteIf(bool condition, string message);
Verdeutlichen wir uns den Einsatz an einem Beispiel. Um den Inhalt des Feldes value zu testen, könnten wir in herkömmlicher Weise codieren:
if (value == 77)
Debug.WriteLine("Inhalt von value ist 77");
Mit WriteLineIf wird daraus eine Codezeile:
Debug.WriteLineIf(value == 77, "Inhalt von value ist 77");
Einrücken der Ausgabeinformation
Die Klasse Debug stellt uns Eigenschaften und Methoden zur Verfügung, um die Debug-Ausgaben einzurücken. Mit der Methode Indent wird die Einzugsebene um eins erhöht, mit Unindent um eins verringert. Standardmäßig beschreibt eine Einzugsebene vier Leerzeichen. Mit der Eigenschaft IndentSize kann ein anderer Wert bestimmt werden. IndentLevel erlaubt, eine bestimmte Einzugsebene festzulegen, ohne Indent mehrfach aufrufen zu müssen. An einem Beispiel wollen wir uns noch die Auswirkungen ansehen.
Debug.WriteLine("Ausgabe 1");
Debug.Indent();
Debug.WriteLine("Ausgabe 2");
Debug.IndentLevel = 3;
Debug.WriteLine("Ausgabe 3");
Debug.Unindent();
Debug.WriteLine("Ausgabe 4");
Debug.IndentSize = 2;
Debug.IndentLevel = 1;
Debug.WriteLine("Ausgabe 5");
Listing 7.17 Strukturierte Ausgabe im »Ausgabe«-Fenster
Der Code führt zu folgender Ausgabe:
Ausgabe 1
Ausgabe 2
Ausgabe 3
Ausgabe 4
Ausgabe 5
Die Methode »Assert«
Mit der Methode Assert können Sie eine Annahme prüfen, um beispielsweise unzulässige Zustände festzustellen. Die Methode zeigt eine Fehlermeldung an, wenn ein Ausdruck mit false ausgewertet wird.
Debug.Assert(value >= 0, "value ist negativ");
Hat die Eigenschaft value einen Wert, der kleiner 0 ist, erscheint auf dem Bildschirm die in Abbildung 7.7 gezeigte Nachricht.
Abbildung 7.7 Die Meldung der Methode »Debug.Assert«
Das Dialogfenster enthält neben der dem zweiten Parameter übergebenen Zeichenfolge auch Informationen darüber, in welcher Klasse und welcher Methode der Assertionsfehler aufgetreten ist.
7.2.3 Die Klasse »Trace«
Die Klasse Trace unterscheidet sich in der Liste ihrer Eigenschaften und Methoden nicht von Debug. Dennoch gibt es einen Unterschied, der sich nur bei einem Wechsel der Build-Konfiguration zwischen Release und Debug bemerkbar macht (siehe Abbildung 7.8).
Abbildung 7.8 Die Einstellung der Debug/Release-Build-Konfiguration
Die Debug/Release-Konfiguration
Standardmäßig ist bei jedem neuen Projekt die Konfiguration Debug eingestellt. Anweisungen, die auf den Klassen Debug oder Trace basieren, werden dann grundsätzlich immer bearbeitet. Wird jedoch die Konfiguration Release gewählt, ignoriert der C#-Compiler Aufrufe der Klasse Debug, während Aufrufe auf Trace weiterhin bearbeitet werden.
Das ist aber noch nicht das Wesentlichste. Viel wichtiger ist die Tatsache, dass Aufrufe auf Trace kompiliert werden – unabhängig davon, ob Sie die Konfiguration Debug oder Release eingestellt haben. Viele Trace-Anweisungen vergrößern deshalb auch das DLL- bzw. EXE-Kompilat. Andererseits hat der Entwickler hier auch eine einfache Möglichkeit, bestimmte Zustände zu protokollieren, die sich zur Laufzeit einstellen und geprüft werden müssen.
Unterhalb des Verzeichnisses, in dem sich die Quellcodedateien befinden, legt die Entwicklungsumgebung das Verzeichnis \bin an, dem selbst je nach eingestellter Build-Konfiguration die beiden Verzeichnisse \Debug und \Release untergeordnet sind. Abhängig von der Konfigurationseinstellung wird das Kompilat der ausführbaren Datei in eines dieser beiden Unterverzeichnisse gespeichert.
Debug-Informationen, die beim Kompilieren generiert werden, sind in einer Datei mit der Dateierweiterung .pdb im Verzeichnis gespeichert. Der Debugger nutzt die darin enthaltenen Informationen, um Variablennamen und andere Informationen während des Debuggens in einem sinnvollen Format anzuzeigen.
7.2.4 Bedingte Kompilierung
Die bedingte Kompilierung ermöglicht es, Codeabschnitte oder Methoden nur dann zu kompilieren, wenn ein bestimmtes Symbol definiert ist. Üblicherweise werden bedingte Codeabschnitte dazu benutzt, während der Entwicklungsphase den Zustand der Anwendung zur Laufzeit zu testen. Bevor ein Release-Build der Anwendung erstellt wird, wird das Symbol entfernt. Die Abschnitte, deren Code als bedingt kompilierbar gekennzeichnet ist, werden dann nicht kompiliert.
Der folgende Code zeigt ein Beispiel für bedingte Kompilierung:
#define MYDEBUG
using System;
class Program {
static void Main(string[] args) {
#if(MYDEBUG)
Console.WriteLine("In der #if-Anweisung");
#elif(TEST)
Console.WriteLine("In der #elif-Anweisung");
#endif
}
}
Mit der Präprozessordirektive #define wird das Symbol MYDEBUG definiert. Symbole werden immer vor der ersten Anweisung festgelegt, die selbst keine #define-Präprozessordirektive ist. Werte können den Symbolen nicht zugewiesen werden. Die Präprozessordirektive gilt nur in der Quellcodedatei, in der sie definiert ist, und wird nicht mit einem Semikolon abgeschlossen.
Mit #if oder #elif wird das Vorhandensein des angegebenen Symbols getestet. Ist das Symbol definiert, liefert die Prüfung das Ergebnis true, und der Code wird ausgeführt. #elif ist die Kurzschreibweise für die beiden Anweisungen #else und #if. Da im Beispielcode kein Symbol namens TEST definiert ist, wird die Ausgabe wie folgt lauten:
In der #if-Anweisung
Standardmäßig sind in C#-Projekten die beiden Symbole DEBUG und TRACE vordefiniert. Diese Vorgabe ist im Projekteigenschaftsfenster eingetragen (siehe Abbildung 7.9) und hat anwendungsweite Gültigkeit. Das Projekteigenschaftsfenster öffnen Sie, indem Sie im Projektmappen-Explorer den Knoten Properties doppelt anklicken. Sie können die Symbole löschen oder auch weitere hinzufügen, die ihrerseits alle durch ein Semikolon voneinander getrennt werden müssen.
Das Projekteigenschaftsfenster bietet darüber hinaus den Vorteil, dass sich die Symbole einer bestimmten Build-Konfiguration zuordnen lassen. Wählen Sie in der Dropdown-Liste Konfiguration die Build-Konfiguration aus, für welche die unter Bedingte Kompilierungskonstanten angegebenen Symbole gültig sein sollen. Wenn Sie beispielsweise keine #define-Präprozessordirektive im Code angeben, dafür aber der Debug-Konfiguration das Symbol DEBUG zugeordnet haben, wird der in #if - #endif eingeschlossene Code im Debug-Build mitkompiliert, im Release-Build jedoch nicht.
Die im Projekteigenschaftsfenster definierten Konstanten gelten projektweit. Um in einer einzelnen Codedatei die Wirkung eines Symbols aufzuheben, müssen Sie das Symbol hinter der #undef-Direktive angeben.
Abbildung 7.9 Festlegung der Symbole im Projekteigenschaftsfenster
Bedingte Kompilierung mit dem Attribut »Conditional«
Häufig ist es wünschenswert, eine komplette Methode als bedingt zu kompilierende Methode zu kennzeichnen. Hier hilft Ihnen .NET mit dem Attribut Conditional aus dem Namespace System.Diagnostics weiter.
Und wieder muss ich Attribute erwähnen, ohne dass wir uns bisher diesem Thema gewidmet haben. Ich weiß, es ist nicht immer schön, auf Features zuzugreifen, die noch nicht behandelt worden sind. Ich mag das bei den Büchern, die ich lese, auch nicht. Nur leider lässt es sich nicht immer vermeiden, weil die Zahnrädchen des .NET Frameworks so komplex ineinandergreifen. Also noch einmal der Hinweis: In Kapitel 10 werde ich Ihnen alles Wissenswerte zu den Attributen erzählen.
Damit eine komplette Methode als bedingt kompilierbar gekennzeichnet wird, muss das Conditional-Attribut (wie im folgenden Beispiel gezeigt) vor dem Methodenkopf in eckigen Klammern angegeben werden. In den runden Klammern wird das Symbol als Zeichenfolge genannt:
[Conditional("DEBUG")]
public void ConditionalTest() {
[...]
}
Listing 7.18 Methode mit dem Attribut »Conditional«
Die Methode ConditionalTest wird jetzt nur dann kompiliert, wenn das Symbol DEBUG gesetzt ist. Sie können auch mehrere Attribute mit unterschiedlichen Symbolen angeben. Kann eines der Symbole ausgewertet werden, wird die Methode ausgeführt. Anders als bedingter Code, der durch #if - #endif eingeschlossen ist, wird eine Methode, der das Conditional-Attribut angeheftet ist, immer kompiliert.
Sie müssen beachten, dass eine Methode mit einem Conditional-Attribut immer den Rückgabetyp void haben muss und nicht mit dem Modifizierer override gekennzeichnet sein darf.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.