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 9 Mehr zu Klassen
  gp 9.1 Verschachtelte Klassen
  gp 9.2 Weitere Verschachtelungen
  gp 9.3 Erstellung, Initialisierung, Zerstörung
  gp 9.4 Erstellungsroutinen
  gp 9.5 Überladung und Namensausblendung
  gp 9.6 Statische Felder
  gp 9.7 Statische Mitgliedsfunktionen
  gp 9.8 Statische Erstellungsroutinen
  gp 9.9 Konstanten
  gp 9.10 Schreibgeschützte Felder
  gp 9.11 Private Erstellungsroutinen
  gp 9.12 Parameterlisten variabler Länge

Kapitel 9 Mehr zu Klassen

In diesem Kapitel werden verschiedene Probleme bei der Verwendung von Klassen behandelt, einschließlich Erstellungsroutinen, Verschachtelung sowie Überladungsregeln.


Galileo Computing

9.1 Verschachtelte Klassen  downtop

Gelegentlich ist es hilfreich, Klassen in anderen Klassen zu verschachteln, beispielsweise dann, wenn eine Helferklasse nur von einer weiteren Klasse verwendet wird. Die Zugänglichkeit der verschachtelten Klasse folgt ähnlichen Regeln wie denen für die Interaktion von Klassen- und Mitgliedsmodifikatoren. Wie bei Mitgliedern auch, definieren die Zugriffsmodifikatoren einer verschachtelten Klasse den Zugriff auf die verschachtelte Klasse von außerhalb. Ebenso wie ein privates Feld innerhalb einer Klasse immer sichtbar ist, so ist auch eine private verschachtelte Klasse innerhalb der umgebenden Klasse sichtbar.

Im folgenden Beispiel verfügt die Parser-Klasse über die Klasse Token, die intern verwendet wird. Ohne Klassenverschachtelung könnte der entsprechende Code so aussehen:

public class Parser
{
    Token[]    tokens;
}
public class Token
{
    string name;
}

In diesem Beispiel sind sowohl die Parser- als auch die Token-Klasse öffentlich zugänglich, was nicht optimal ist. Nicht nur, dass die Klasse Token bei der Auflistung von Klassen Platz einnimmt, sie ist darüber hinaus nicht besonders nützlich. Daher ist es sinnvoll, eine Klassenverschachtelung vorzunehmen, um die Klasse als private zu deklarieren und sie damit nur gegenüber der Klasse Parser offen zu legen.

Hier der überarbeitete Code:

public class Parser
{
    Token[]    tokens;
    private class Token
    {
        string name;
    }
}

Nun ist die Klasse Token für niemanden außer für Parser sichtbar. Eine andere Möglichkeit besteht darin, die Klasse Token als internal zu deklarieren, damit sie außerhalb der Assemblierung nicht sichtbar ist. Bei dieser Lösung wird Token allerdings innerhalb der Assemblierung weiterhin offen gelegt.

Des Weiteren kann bei dieser Lösung ein wichtiger Vorteil der Klassenverschachtelung nicht genutzt werden. Bei einer verschachtelten Klasse kann der Leser des Quellcodes davon ausgehen, dass die Token-Klasse gefahrlos ignoriert werden kann, solange nicht die internen Details von Parser von Bedeutung sind. Wird dieser Aufbau auf eine vollständige Assemblierung angewendet, kann der Code erheblich vereinfacht werden.

Die Verschachtelung kann auch als Mittel zur Strukturierung eingesetzt werden. Wenn die Parser-Klasse sich beispielsweise in einem Namespace namens Language befände, müssten Sie vielleicht einen separaten Namespace namens Parser bereitstellen, um die Klassen für Parser gut zu strukturieren. Dieser neue Namespace würde dann die Token-Klasse und eine umbenannte Parser-Klasse enthalten. Durch Verwendung verschachtelter Klassen kann die Klasse Parser im Namespace Language verbleiben und die Klasse Token enthalten.


Galileo Computing

9.2 Weitere Verschachtelungen  downtop

Es können neben Klassen auch Schnittstellen, struct- oder enum-Schlüsselwörter innerhalb von Klassen verschachtelt werden.


Galileo Computing

9.3 Erstellung, Initialisierung, Zerstörung  downtop

In allen objektorientierten Systemen spielen Erstellung, Initialisierung und Zerstörung eines Objekts eine wichtige Rolle. In der .NET-Laufzeitumgebung kann der Programmierer die Zerstörung von Objekten nicht steuern, es ist jedoch wichtig zu wissen, welche Bereiche gesteuert werden können.


Galileo Computing

9.4 Erstellungsroutinen  downtop

In C# gibt es keine Standarderstellungsroutine zur Erstellung von Objekten. Für Klassen kann ggf. eine standardmäßige (z. B. eine parameterlose) Erstellungsroutine geschrieben werden.

Eine Erstellungsroutine kann über die Syntax base eine Erstellungsroutine des Basistyps auslösen:

using System;
public class BaseClass
{
    public BaseClass(int x)
    {
        this.x = x;
    }
    public int X
    {
        get
        {
            return(x);
        }
    }
    int    x;
}
public class Derived: BaseClass
{
    public Derived(int x): base(x)
    {
    }
}
class Test
{
    public static void Main()
    {
        Derived d = new Derived(15);
        Console.WriteLine("X = {0}", d.X);
    }
}

In diesem Beispiel leitet die Erstellungsroutine für die Derived-Klasse lediglich die Erstellung des Objekts an die Erstellungsroutine BaseClass weiter.

Gelegentlich ist es für eine Erstellungsroutine hilfreich, die Erstellung an eine andere Erstellungsroutine im selben Objekt weiterzuleiten.

using System;
class MyObject
{
    public MyObject(int x)
    {
        this.x = x;
    }
    public MyObject(int x, int y): this(x)
    {
        this.y = y;
    }
    public int X
    {
        get
        {
            return(x);
        }
    }
    public int Y
    {
        get
        {
            return(y);
        }
    }
    int x;
    int y;
}
class Test
{
    public static void Main()
    {
        MyObject my = new MyObject(10, 20);
        Console.WriteLine("x = {0}, y = {1}", my.X, my.Y);
    }
}

Galileo Computing

9.4.1 Initialisierung  downtop

Wenn es sich beim Standardwert des Feldes nicht um den gewünschten Wert handelt, kann dieser in der Erstellungsroutine gesetzt werden. Sind mehrere Erstellungsroutinen für das Objekt vorhanden, kann es bequemer sein, den Wert statt durch eine Festlegung in jeder Erstellungsroutine über eine Initialisierung festzulegen. Die Initialisierung ist gleichzeitig weniger fehleranfällig.

Hier ein Beispiel zur Funktionsweise der Initialisierung:

public class Parser
{
    public Parser(int number)
    {
        this.number = number;
    }
    int number;
}
class MyClass
{
    public int        counter = 100;
    public string        heading = "Top";
    private Parser    parser = new Parser(100);
}

Die Handhabung ist einfach, die Anfangswerte können bei der Deklaration eines Mitglieds gesetzt werden. Gleichzeitig vereinfacht dieses Vorgehen die Verwaltung, da klar ist, wie der anfängliche Wert eines Mitglieds lautet.



Galileo Computing

9.4.2 Zerstörungsroutinen  downtop

Streng genommen hat C# keine Zerstörungsroutinen (Destruktoren), zumindest nicht in dem Sinne, dass eine Zerstörungsroutine aufgerufen wird, wenn das Objekt gelöscht werden soll.

Die in C# verwendete Zerstörungsroutine ist mit den so genannten Finalisierungsroutinen anderer Sprachen vergleichbar und wird von der Speicherbereinigung aufgerufen, wenn das Objekt bereinigt wird. Dies bedeutet, dass der Programmier keine direkte Kontrolle darüber hat, wann die Zerstörungsroutine aufgerufen wird. Aus diesem Grund ist die Zerstörungsroutine auch weniger hilfreich als in Sprachen wie beispielsweise C++. Wenn die Bereinigung über eine Zerstörungsroutine durchgeführt wird, sollte eine weitere Methode vorhanden sein, mit der der Benutzer die gleiche Operation auch direkt durchführen kann.

Weitere Informationen zu diesem Thema finden Sie im Abschnitt zur Speicherbereinigung in Kapitel 31, C# im Detail.


Galileo Computing

9.5 Überladung und Namensausblendung  downtop

In C#-Klassen – und in der Common Language Runtime im Allgemeinen – werden Mitglieder basierend auf Anzahl und Typ der zugehörigen Parameter überladen. Das Überladen erfolgt nicht auf Grundlage des Rückgabetyps der Funktion.

// Fehler
using System;
class MyObject
{
    public string GetNextValue(int value)
    {
        return((value + 1).ToString());
    }
    public int GetNextValue(int value)
    {
        return(value + 1);
    }
}
class Test
{
    public static void Main()
    {
        MyObject my = new MyObject();
        Console.WriteLine("Next: {0}", my.GetNextValue(12));
    }
}

Dieser Code kann nicht kompiliert werden, da die überladenen GetNextValue()-Funktionen sich nur im Rückgabetyp unterscheiden und der Compiler nicht ermitteln kann, welche Funktion aufgerufen werden muss. Es ist deshalb ein Fehler, Funktionen zu deklarieren, die sich lediglich hinsichtlich des Rückgabetyps unterscheiden.


Galileo Computing

9.5.1 Namensausblendung  downtop

In C# werden Methodennamen basierend auf dem Namen der Methode, nicht auf Grundlage der Methodensignatur verborgen. Sehen Sie sich folgendes Beispiel an:

// Fehler
using System;
public class Base
{
    public int Process(int value)
    {
        Console.WriteLine("Base.Process: {0}", value);
}
public class Derived: Base
{
    public int Process(string value)
    {
        Console.WriteLine("Derived.Process: {0}", value);
    }
}
class Test
{
    public static void Main()
    {
        Derived d = new Derived();
        d.Process("Hello");
        d.Process(12);        // Fehler
        ((Base) d).Process(12);    // in Ordnung
    }
}

Wenn die zwei überladenen Process()-Funktionen sich in derselben Funktion befänden, wären sie beide zugänglich. Da sie sich jedoch in unterschiedlichen Klassen befinden, wird durch die Definition von Process() in der abgeleiteten Klasse der Name in der Basisklasse an allen Stellen ausgeblendet, an denen er verwendet wird.

Um auf beide Funktionen zugreifen zu können, müsste über Derived die in der Basisklasse enthaltene Process()-Version überladen werden; anschließend müsste der Aufruf an die Implementierung der Basisklasse weitergeleitet werden.


Galileo Computing

9.6 Statische Felder  downtop

Es ist manchmal sinnvoll, Mitglieder eines Objekts zu definieren, die nicht mit einer spezifischen Klasseninstanz, jedoch mit der Klasse als Ganzes verknüpft sind. Solche Mitglieder werden als statische (static) Mitglieder bezeichnet.

Ein statisches Feld ist die einfachste Form eines statischen Mitglieds. Zur Deklaration eines statischen Feldes platzieren Sie einfach den Modifikator static vor der Variablendeklaration. Der folgende Code könnte beispielsweise dazu verwendet werden, die Anzahl der erstellten Klasseninstanzen zu verfolgen.

using System;
class MyClass
{
    public MyClass()
    {
        instanceCount++;
    }
    public static int instanceCount = 0;
}
class Test
{
    public static void Main()
    {
        MyClass my = new MyClass();
        Console.WriteLine(MyClass.instanceCount);
        MyClass my2 = new MyClass();
        Console.WriteLine(MyClass.instanceCount);
    }
}

Die Erstellungsroutine für das Objekt erhöht den Instanzenzähler, und der Instanzenzähler kann referenziert werden, um die Anzahl der bisher erstellten Objektinstanzen zu ermitteln. Auf ein statisches Feld wird nicht über die Instanz der Klasse, sondern über den Namen der Klasse zugegriffen; dies gilt für alle statischen Mitglieder.



Galileo Computing

9.7 Statische Mitgliedsfunktionen  downtop

Im vorangegangenen Beispiel wird ein internes Feld offen gelegt – eine nicht empfohlene Vorgehensweise. Das Codebeispiel kann so geändert werden, dass statt eines statischen Feldes eine statische Mitgliedsfunktion verwendet wird:

using System;
class MyClass
{
    public MyClass()
    {
        instanceCount++;
    }
    public static int GetInstanceCount()
    {
        return(instanceCount);
    }
    static int instanceCount = 0;
}
class Test
{
    public static void Main()
    {
        MyClass my = new MyClass();
        Console.WriteLine(MyClass.GetInstanceCount());
    }
}

Mit diesem Code wird das gewünschte Ziel erreicht, und gleichzeitig wird das Feld den Benutzern der Klasse gegenüber nicht offen gelegt, d. h., die Flexibilität hinsichtlich zukünftiger Codeänderungen wird erhöht. Da es sich um eine statische Mitgliedsfunktion handelt, wird sie über den Klassennamen und nicht über den Namen einer Klasseninstanz aufgerufen.

Im wirklichen Leben würde in diesem Fall wohl eher eine statische Eigenschaft verwendet. Diese werden in Kapitel 18, Eigenschaften, besprochen.


Galileo Computing

9.8 Statische Erstellungsroutinen  downtop

Ebenso wie es statische Mitglieder gibt, gibt es auch statische Erstellungsroutinen. Eine statische Erstellungsroutine wird vor der Erstellung der ersten Objektinstanz aufgerufen und ist bei der Ausführung von einmalig anfallenden Setupaufgaben nützlich.


Eine statische Erstellungsroutine wird durch einfaches Hinzufügen von static vor der Erstellungsroutinendefinition deklariert. Eine statische Erstellungsroutine kann keine Parameter tragen.

class MyClass
{
    static MyClass()
    {
    }
}

Es gibt zur Zerstörungsroutine keine analoge statische Zerstörungsroutine.


Galileo Computing

9.9 Konstanten  downtop

C# ermöglicht die Definition von Werten als Konstanten. Soll ein Wert als Konstante definiert werden, muss der Wert etwas sein, dass als Konstante geschrieben werden kann. Diese Anforderung schränkt die Konstantentypen auf die integrierten Typen ein, die als literale Werte geschrieben werden können.

Das Platzieren von const vor einer Variablen bedeutet, dass deren Wert nicht geändert werden kann. Hier ein Konstantenbeispiel:

using System;
enum MyEnum
{
    Jet
}
class LotsOLiterals
{
        // const-Elemente können nicht geändert werden
        // const impliziert static
    public const int value1 = 33;
    public const string value2 = "Hello";
    public const MyEnum value3 = MyEnum.Jet;
}
class Test
{
    public static void Main()
    {
        Console.WriteLine("{0} {1} {2}",
                LotsOLiterals.value1,
                LotsOLiterals.value2,
                LotsOLiterals.value3);
    }
}

Galileo Computing

9.10 Schreibgeschützte Felder  downtop

Aufgrund der Einschränkung für Konstantentypen, dass diese nämlich zur Kompilierungszeit bekannt sein müssen, kann const nicht in vielen Situationen eingesetzt werden.

In einer Color-Klasse kann es sehr hilfreich sein, Konstanten als Teil der Klasse für die Standardfarben zu verwenden. Würden keine Beschränkungen für const gelten, könnte der folgende Code verwendet werden:

// Fehler
class Color
{
    public Color(int red, int green, int blue)
    {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    int    red;
    int    green;
    int    blue;
        // new-Aufruf kann nicht mit static verwendet werden
    public static const Color    Red = new Color(255, 0, 0);
    public static const Color    Green = new Color(0, 255, 0);
    public static const Color    Blue = new Color(0, 0, 255);
}
class Test
{
    static void Main()
    {
        Color background = Color.Red;
    }
}

Dies funktioniert nicht, da die statischen Mitglieder Red, Green und Blue nicht zur Kompilierungszeit berechnet werden können. Diese Mitglieder in normale öffentliche Mitglieder zu ändern, funktioniert auch nicht, da jedermann den Wert Rot in Olivgrün oder Braunrot ändern könnte.

Der Modifikator readonly ist genau für diese Situation gedacht. Durch das Verwenden von readonly kann der Wert in der Erstellungs- oder Initialisierungsroutine gesetzt, später jedoch nicht mehr bearbeitet werden.

Da die Farbwerte der Klasse und nicht einer spezifischen Klasseninstanz angehören, werden sie in der statischen Erstellungsroutine initialisiert.

class Color
{
    public Color(int red, int green, int blue)
    {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    int    red;
    int    green;
    int    blue;

    public static readonly Color    Red;
    public static readonly Color    Green;
    public static readonly Color    Blue;

        // statische Erstellungsroutine
    static Color()
    {
        Red = new Color(255, 0, 0);
        Green = new Color(0, 255, 0);
        Blue = new Color(0, 0, 255);
    }
}
class Test
{
    static void Main()
    {
        Color background = Color.Red;
    }
}

So wird das richtige Verhalten sichergestellt.

Wenn die Anzahl der statischen Mitglieder sehr hoch oder deren Erstellung sehr ressourcenintensiv ist (entweder im Hinblick auf die Zeit oder den Speicher), ist es eventuell sinnvoller, diese als readonly-Eigenschaften zu deklarieren, damit die Mitglieder nach Bedarf erstellt werden können.

Auf der anderen Seite kann es einfacher sein, eine Auflistung der unterschiedlichen Farbnahmen zu definieren und nach Bedarf Instanzen der Werte zurückzugeben.

class Color
{
    public Color(int red, int green, int blue)
    {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    public enum PredefinedEnum
    {
        Red,
        Blue,
        Green
    }
    public static Color GetPredefinedColor(
    PredefinedEnum pre)
    {
        switch (pre)
        {
            case PredefinedEnum.Red:
                return(new Color(255, 0, 0));

            case PredefinedEnum.Green:
                return(new Color(0, 255, 0));

            case PredefinedEnum.Blue:
                return(new Color(0, 0, 255));

            default:
                return(new Color(0, 0, 0));
        }
    }
    int red;
    int blue;
    int green;
}
class Test
{
    static void Main()
    {
        Color background =
            Color.GetPredefinedColor(Color.PredefinedEnum.Blue);
    }
}

Diese Methode erfordert etwas mehr Eingaben, es ergibt sich jedoch weder eine Starteinbuße noch sind viele Objekte vorhanden, die Speicher belegen. Darüber hinaus wird die Klassenschnittstelle einfach gehalten. Wenn die vordefinierten Farben 30 Mitglieder umfassen würden, wäre die Klasse sehr viel schwerer zu verstehen.

In C# jedoch stellt dies kein Problem dar, da die Laufzeitumgebung sich um die Speicherzuordnung kümmert. Im vorangegangenen Beispiel wird das in der Color.GetPredefinedColor()-Funktion erstellte Objekt sofort in die Hintergrundvariable kopiert und kann dann entfernt werden.



Galileo Computing

9.11 Private Erstellungsroutinen  downtop

Da es in C# keine globalen Variablen oder Konstanten gibt, müssen alle Deklarationen innerhalb einer Klasse vorgenommen werden. Manchmal entstehen so Klassen, die vollständig aus statischen Mitgliedern bestehen. In diesem Fall besteht keine Veranlassung dazu, jemals ein Objekt der Klasse zu instanziieren. Um eine Instanziierung zu verhindern, wird der Klasse eine private-Erstellungsroutine hinzugefügt.

// Fehler
using System;
class PiHolder
{
    private PiHolder() {}
    static double Pi = 3.1415926535;
}
class Test
{
    PiHolder pi = new PiHolder();    // Fehler
}

Obwohl durch das Hinzufügen von private vor der Erstellungsroutinendefinition der tatsächliche Zugriff auf die Erstellungsroutine nicht geändert wird, wird durch die explizite Kennzeichnung deutlich, dass die Klasse über eine private Erstellungsroutine verfügen soll.


Galileo Computing

9.12 Parameterlisten variabler Länge  toptop

Gelegentlich ist es sinnvoll, einen Parameter zu definieren, der eine variable Anzahl Parameter enthalten kann (Console.WriteLine() ist ein gutes Beispiel). C# bietet für diese Fälle eine einfache Lösung:

using System;
class Port
{
        // Version mit einem einzigen Objektparameter
    public void Write(string label, object arg)
    {
        WriteString(label);
        WriteString(arg.ToString());
    }
        // Version mit einem Array aus Objektparametern
        public void Write(string label, params object[] args)
    {
        WriteString(label);
        for (int index = 0; index < args.GetLength(0); index++)
        {
            WriteString(args[index].ToString());
        }
    }
    void WriteString(string str)
    {
        // Zeichenfolge wird an diesen Port (Anschluss) geschrieben
        Console.WriteLine("Port debug: {0}", str);
    }
}

class Test
{
    public static void Main()
    {
        Port    port = new Port();
        port.Write("Single Test", "Port ok");
        port.Write("Port Test: ", "a", "b", 12, 14.2);
        object[] arr = new object[4];
        arr[0] = "The";
        arr[1] = "answer";
        arr[2] = "is";
        arr[3] = 42;
        port.Write("What is the answer?", arr);
    }
}

Das params-Schlüsselwort des letzten Parameters ändert die Art, mit der der Compiler die Funktionen ermittelt. Wird ein Aufruf dieser Funktion ermittelt, wird zunächst geprüft, ob eine exakte Übereinstimmung mit dieser Funktion vorliegt. Der erste Funktionsaufruf passt:

public void Write(string, object arg)

Ebenso übergibt die dritte Funktion ein passendes Objektarray:

public void Write(string label, params object[] 
args)

Interessant wird es beim zweiten Funktionsaufruf. Die Definition stimmt nicht mit dem Objektparameter überein, aber ebenso wenig liegt eine Übereinstimmung mit dem Objektarray vor.

Wenn der Abgleich in beiden Fällen fehlschlägt, erkennt der Compiler das params-Schlüsselwort. Der Compiler versucht dann, die Parameterliste abzugleichen, indem er den Arraybestandteil des params-Parameters entfernt und den Parameter dupliziert, bis die Parameteranzahl übereinstimmt.

Führt dies zu einer übereinstimmenden Funktion, wird der Code zum Erstellen des Objektarrays geschrieben. Mit anderen Worten, die Zeile

port.Write("Port Test: ", "a", "b", 12, 14.2);

wird umgeschrieben in

object[] temp = new object[4];
temp[0] = "a";
temp[1] = "b";
temp[2] = 12;
temp[3] = 14.2;
port.Write("Port Test: ", temp);

In diesem Beispiel handelte es sich bei dem params-Parameter um ein object-Array, es kann jedoch ein Array beliebigen Typs vorliegen.

Zusätzlich zu der Arrayversion ist es normalerweise sinnvoll, eine oder mehrere spezifische Versionen der Funktion bereitzustellen. Dies wirkt sich nicht nur positiv auf die Effizienz aus (das Objektarray muss nicht erstellt werden): Sprachen ohne Unterstützung der params-Syntax müssen das Objektarray nicht für alle Aufrufe verwenden. Das Überladen einer Funktion mit Versionen, die einen, zwei oder drei Parameter tragen, sowie eine Arrayversion stellen gute Faustregeln dar.






1    Mit anderen Worten, der C++-Rückgabetyp covariant wird nicht unterstützt.

   

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