Kapitel 33 Defensive Programmierung
Die .NET-Laufzeitumgebung stellt einige Funktionen
für eine risikolosere Programmierung bereit. Bedingte Methoden
und die Ablaufverfolgung ermöglichen das Hinzufügen von Prüfungen
und Protokollcode zu einer Anwendung, das Auffangen von Fehlern während
der Entwicklung sowie das Diagnostizieren von Fehlern in bereits veröffentlichtem
Code.
33.1 Bedingte Methoden
 
Bedingte Methoden werden üblicherweise zum
Schreiben von Code eingesetzt, der eine Operation nur dann ausführt,
wenn die Kompilierung auf eine bestimmte Art und Weise erfolgt. Diese
Vorgehensweise wird häufig zum Hinzufügen von Code verwendet,
der nur bei der Erstellung eines Debugbuilds aufgerufen wird, da die
zusätzlichen Prüfungen den Code ansonsten zu langsam machen.
In C++ würde dies durch den Einsatz eines Makros
in der include-Datei erreicht, mit dem ein Funktionsaufruf
nicht erfolgt, wenn das Debugsymbol nicht definiert wird. Dies funktioniert
in C# nicht, da weder include-Dateien noch Makros vorhanden
sind.
In C# kann eine Methode mit dem Attribut conditional
versehen werden, um festzulegen, wann ein Aufruf generiert werden sollte.
Beispiel:
using System;
using System.Diagnostics;
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
if (i != 0)
Console.WriteLine("Bad State");
}
int i = 0;
}
class Test
{
public static void Main()
{
MyClass c = new MyClass(1);
c.VerifyState();
}
}
Auf die Funktion VerifyState() wird
das Conditional-Attribut angewendet, hierbei bezeichnet
DEBUG die Bedingungszeichenfolge. Wenn der Compiler über einen
Funktionsaufruf einer solchen Funktion stößt, prüft er,
ob die Bedingungszeichenfolge definiert ist. Wurde diese nicht definiert,
erfolgt kein Funktionsaufruf.
Wird der Code unter Verwendung der Befehlszeilenoption
/D:DEBUG kompiliert, wird bei Ausführung ein Statusfehler
(»Bad State«) ausgegeben. Erfolgt die Kompilierung ohne Definition
von DEBUG, wird die Funktion nicht aufgerufen und es erfolgt
keine Ausgabe.
33.2 Debug- und Trace-Klassen
 
Die .NET-Laufzeitumgebung hat sich diesem Konzept angenommen, indem die Klassen
Debug und Trace über den Namespace System.Diagnostics
bereitgestellt werden. Diese Klassen implementieren die gleiche Funktionalität,
unterscheiden sich jedoch leicht in der Verwendung. Code, der Trace-Klassen
verwendet, wird tendenziell in bereits veröffentlichter Software
eingesetzt. Da sie sich auf die Leistung auswirken, sollten diese Klassen
nicht übermäßig verwendet werden.
Debug dagegen wird üblicherweise
nicht in bereits veröffentlichter Software eingesetzt und kann
etwas freigiebiger verwendet werden.
Aufrufe von Debug sind basierend auf
der Definition von DEBUG bedingt, Aufrufe von Trace
sind ebenfalls bedingt, richten sich jedoch nach der Definition von
TRACE. Standardmäßig definiert die Visual Studio-IDE
TRACE sowohl für Debugversionen als auch für
Einzelhandelsversionen und DEBUG nur für Debugversionen.
Bei der Kompilierung über die Befehlszeile muss die geeignete Option
verwendet werden.
In den weiteren Beispielen dieses Kapitels, in denen
Debug verwendet wird, kann alternativ auch Trace
eingesetzt werden.
33.3 Assert-Anweisungen
 
Eine Assert-Anweisung ist einfach eine
bedingte Anweisung, die wahr sein sollte; gefolgt von Text, der ausgegeben
wird, wenn dies nicht zutrifft. Das zuvor genannte Codebeispiel lautet
in verbesserter Form:
// Kompilieren mit: csc /r:system.dll file_1.cs
using System;
using System.Diagnostics;
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
Debug.Assert(i == 0, "Bad State");
}
int i = 0;
}
class Test
{
public static void Main()
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
MyClass c = new MyClass(1);
c.VerifyState();
}
}
Standardmäßig werden Assert-Anweisungen
und weitere Debugausgaben an alle Empfänger der Debug.Listeners-Auflistung
gesendet. Da beim Standardverhalten ein Dialogfeld angezeigt wird, löscht
der Code in Main() die Listeners-Auflistung
und fügt anschließend einen neuen Empfänger
hinzu, der mit Console.Out gekoppelt wird. Dies führt
zu einer Ausgabe an die Konsole.
Assert-Anweisungen sind äußerst
nützlich bei komplexen Projekten, da mit ihnen sichergestellt wird,
dass erwartete Bedingungen zutreffen.
33.4 Debug- und Trace-Ausgabe
 
Neben den Assert-Anweisungen können
die Klassen Debug und Trace zum Versenden
nützlicher Informationen an den aktuellen Debug- oder
Trace-Empfänger verwendet werden. Dies ist eine nützliche
Beigabe zur Ausführung des Debuggers, denn es ist weniger störend
und kann in veröffentlichten Versionen zur Erzeugung von Protokolldateien
eingesetzt werden.
Die Funktionen Write() und WriteLine()
senden eine Ausgabe an die aktuellen Empfänger. Die Funktionen
Write() und WriteLine() eignen sich gut für
das Debuggen, sind bei veröffentlichten Softwareversionen jedoch
nicht besonders nützlich, da selten eine permanente Protokollierung
erfolgen sollte.
Die Funktionen WriteIf() und WriteLineIf()
versenden nur Ausgabedaten, wenn der erste Parameter wahr ist. Dies
ermöglicht eine Steuerung des Verhaltens über eine statische
Klassenvariable, die zur Laufzeit geändert werden sollte,
um die Menge der protokollierten Daten festzulegen.
// Kompilieren mit: csc /r:system.dll file_1.cs
using System;
using System.Diagnostics;
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
Debug.WriteLineIf(debugOutput, "In VerifyState");
Debug.Assert(i == 0, "Bad State");
}
static public bool DebugOutput
{
get
{
return(debugOutput);
}
set
{
debugOutput = value;
}
}
int i = 0;
static bool debugOutput = false;
}
class Test
{
public static void Main()
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
MyClass c = new MyClass(1);
c.VerifyState();
MyClass.DebugOutput = true;
c.VerifyState();
}
}
Dieser Code erzeugt die folgende Ausgabe:
Fail: Bad State
In VerifyState
Fail: Bad State
33.5 Verwenden von Switch-Klassen zur Steuerung von Debug und Trace
 
Das letztgenannte Beispiel zeigte die Protokollsteuerung
basierend auf einer bool-Variablen. Der Nachteil bei diesem
Ansatz besteht darin, dass es eine Möglichkeit geben muss, diese
Variable innerhalb des Programms zu setzen. Es wäre sinnvoller,
den Wert einer solchen Variable extern festzulegen.
Die Klassen BooleanSwitch und TraceSwitch
bieten diese Möglichkeit. Ihr Verhalten kann zur Laufzeit entweder
durch das Festlegen einer Umgebungsvariablen oder eines Registrierungseintrags
gesteuert werden.
33.6 BooleanSwitch
 
Die Klasse BooleanSwitch kapselt eine
einfache boolesche Variable, die anschließend zur Protokollierungssteuerung eingesetzt wird.
// Datei=boolean.cs
// Kompilieren mit: csc /D:DEBUG /r:system.dll boolean.cs
using System;
using System.Diagnostics;
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
Debug.WriteLineIf(debugOutput.Enabled, "VerifyState Start");
if (debugOutput.Enabled)
Debug.WriteLine("VerifyState End");
}
BooleanSwitch debugOutput =
new BooleanSwitch("MyClassDebugOutput", "Control debug output");
int i = 0;
}
class Test
{
public static void Main()
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
MyClass c = new MyClass(1);
c.VerifyState();
}
}
In diesem Beispiel wird eine Instanz von BooleanSwitch
als statisches Mitglied der Klasse erstellt, und über diese Variable
wird gesteuert, ob eine Ausgabe erfolgt. Bei Ausführung dieses
Codes erfolgt keine Ausgabe, die debugOutput-Variable kann
jedoch durch das Setzen einer Umgebungsvariable gesteuert werden.
set _Switch_MyClassDebugOutput=1
Der Umgebungsvariablenname wird durch Voranstellen
von _Switch_ vor dem Anzeigenamen (erster Parameter) der
Erstellungsroutine für BooleanSwitch erstellt. Die
Codeausführung nach dem Setzen dieser Variable erzeugt die folgende
Ausgabe:
VerifyState Start
VerifyState End
Der Code in VerifyState zeigt zwei
Methoden der Variablenverwendung zur Ausgabesteuerung. Die erste Verwendung
übergibt das Flag an die WriteLineIf()-Funktion und
ist einfacher zu schreiben. Sie ist jedoch gleichzeitig weniger effizient,
da der Funktionsaufruf von WriteLineIf() auch dann erfolgt,
wenn die Variable falsch ist. Die zweite Version, bei der die Variable
vor dem Aufruf getestet wird, vermeidet einen Funktionsaufruf und ist
daher etwas effizienter.
Der Wert einer BooleanSwitch-Variable
kann auch über die Windows-Registrierung gesetzt werden. Für
dieses Beispiel wird ein neuer DWORD-Wert mit dem Schlüssel
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\COMPlus\Switches\MyClassDebugOutput
erstellt und der DWORD-Wert wird auf 0
oder 1 gesetzt, um den Wert von BooleanSwitch festzulegen.
33.6.1 TraceSwitch
 
Manchmal ist es sinnvoll, zur Protokollierungssteuerung keine boolesche Variable zu verwenden. Üblicherweise
erfolgt die Protokollierung auf verschiedenen Ebenen, wobei für
jede dieser Ebenen eine unterschiedliche Informationsmenge in das Protokoll
geschrieben wird.
Mit der TraceSwitch-Klasse werden vier
Ebenen der Informationsprotokollierung definiert. Diese werden in der
TraceLevel-Aufzählung festgelegt.
Ebene
|
Numerischer Wert
|
Off
|
0
|
Error
|
1
|
Warning
|
2
|
Info
|
3
|
Verbose
|
4
|
Jede der höheren Ebenen umfasst die niedrigeren
Ebenen; wenn z. B. die Ebene Info eingestellt wird,
werden Error und Warning ebenfalls gesetzt.
Die numerischen Werte werden beim Setzen des Flags über eine Umgebungsvariable
oder eine Registrierungseinstellung verwendet.
Die TraceSwitch-Klasse legt Eigenschaften
offen, über die gekennzeichnet wird, ob eine spezifische Ablaufverfolgungsebene
festgelegt wurde. Eine typische Protokollierungsanweisung würde
prüfen, ob die geeignete Eigenschaft gesetzt wurde. Hier das vorstehende
Bespiel, zur Verwendung verschiedener Protokollierungsebenen abgeändert:
// Kompilieren mit: csc /r:system.dll file_1.cs
using System;
using System.Diagnostics;
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
Debug.WriteLineIf(debugOutput.TraceInfo, "VerifyState Start");
Debug.WriteLineIf(debugOutput.TraceVerbose,
"Starting field verification");
if (debugOutput.TraceInfo)
Debug.WriteLine("VerifyState End");
}
static TraceSwitch debugOutput =
new TraceSwitch("MyClassDebugOutput", "Control debug output");
int i = 0;
}
class Test
{
public static void Main()
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
MyClass c = new MyClass(1);
c.VerifyState();
}
}
33.6.2 Benutzerdefinierte Switch-Klassen
 
Die Switch-Klasse kapselt auf schöne
Weise den Abruf des Switch-Wertes von der Registrierung,
daher kann leicht eine benutzerdefinierte Switch-Klasse
abgeleitet werden, wenn die Werte von TraceSwitch nicht
richtig funktionieren.
Im folgenden Beispiel wird SpecialSwitch
implementiert, mit dem die Protokollierungsebenen Mute,
Terse, Verbose und Chatty implementiert
werden:
// Kompilieren mit: csc /r:system.dll file_1.cs
using System;
using System.Diagnostics;
enum SpecialSwitchLevel
{
Mute = 0,
Terse = 1,
Verbose = 2,
Chatty = 3
}
class SpecialSwitch: Switch
{
public SpecialSwitch(string displayName, string
description) :
base(displayName, description)
{
}
public SpecialSwitchLevel Level
{
get
{
return(level);
}
set
{
level = value;
}
}
public bool Mute
{
get
{
return(level == 0);
}
}
public bool Terse
{
get
{
return((int) level >= (int) (SpecialSwitchLevel.Terse));
}
}
public bool Verbose
{
get
{
return((int) level >= (int) SpecialSwitchLevel.Verbose);
}
}
public bool Chatty
{
get
{
return((int) level >=(int) SpecialSwitchLevel.Chatty);
}
}
protected override void SetSwitchSetting(int level)
{
if (level < 0)
level = 0;
if (level > 4)
level = 4;
this.level = (SpecialSwitchLevel) level;
}
SpecialSwitchLevel level;
}
class MyClass
{
public MyClass(int i)
{
this.i = i;
}
[Conditional("DEBUG")]
public void VerifyState()
{
Debug.WriteLineIf(debugOutput.Terse, "VerifyState Start");
Debug.WriteLineIf(debugOutput.Chatty,
"Starting field verification");
if (debugOutput.Verbose)
Debug.WriteLine("VerifyState End");
}
static SpecialSwitch debugOutput =
new SpecialSwitch("MyClassDebugOutput", "Control debug output");
int i = 0;
}
class Test
{
public static void Main()
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new TextWriterTraceListener(Console.Out));
MyClass c = new MyClass(1);
c.VerifyState();
}
}
|