Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


C# von Eric Gunnerson
Die neue Sprache für Microsofts .NET-Plattform
C# - Zum Katalog
gp Kapitel 33 Defensive Programmierung
  gp 33.1 Bedingte Methoden
  gp 33.2 Debug- und Trace-Klassen
  gp 33.3 Assert-Anweisungen
  gp 33.4 Debug- und Trace-Ausgabe
  gp 33.5 Verwenden von Switch-Klassen zur Steuerung von Debug und Trace
  gp 33.6 BooleanSwitch

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.


Galileo Computing

33.1 Bedingte Methoden  downtop

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.


Galileo Computing

33.2 Debug- und Trace-Klassen  downtop

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.


Galileo Computing

33.3 Assert-Anweisungen  downtop

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.


Galileo Computing

33.4 Debug- und Trace-Ausgabe  downtop

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

Galileo Computing

33.5 Verwenden von Switch-Klassen zur Steuerung von Debug und Trace  downtop

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.


Galileo Computing

33.6 BooleanSwitch  downtop

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.


Galileo Computing

33.6.1 TraceSwitch  downtop

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();
    }
}

Galileo Computing

33.6.2 Benutzerdefinierte Switch-Klassen  toptop

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();
    }
}
   

Select * from SQL Server 2000




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de