6 Strukturen und Enumerationen
6.1 Strukturen – eine Sonderform der Klassen
.NET stellt mit der Struktur ein Konstrukt bereit, das einer Klasse sehr ähnlich ist. Strukturen gehören zur Gruppe der Wertetypen und werden somit nicht im Heap, sondern auf den Stack gespeichert.
Strukturen werden meistens dann eingesetzt, wenn sehr viele Objekte eines bestimmten Typs erwartet werden können. Nehmen wir dazu beispielsweise an, Sie beabsichtigen, jedes Pixel des Monitors durch ein Objekt zu beschreiben. Selbst bei einer »Standardauflösung« von 1.024 x 768 würde man 786.432 Objekte benötigen. Das ist schon eine Zahl, bei der man sich Gedanken darüber machen sollte, ob anstatt einer Klasse nicht eine Strukturdefinition die hohen Speicher- und Performanceanforderungen besser erfüllen könnte. Denn im Gegensatz zu klassenbasierten Objekten, für die ein verhältnismäßig hoher Verwaltungsaufwand notwendig ist, beanspruchen strukturbasierte Objekte, die zu den Wertetypen gerechnet werden, relativ wenige Verwaltungsressourcen und sind deshalb performancetechnisch deutlich besser.
6.1.1 Die Definition einer Struktur
Stellen Sie sich vor, Sie möchten eine Person durch eine Klassendefinition beschreiben. Eine Person sei durch ihren Namen und das Alter gekennzeichnet. Außerdem soll die Klasse die Methode Run veröffentlichen. Die Klassendefinition könnte folgendermaßen lauten:
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public void Run() {
[...]
}
}
Listing 6.1 Definition einer Klasse »Person«
Tatsächlich unterscheidet sich die analoge Definition des Typs Person durch eine Struktur kaum von der einer Klasse – abgesehen vom Austausch des Schlüsselwortes class durch struct:
Listing 6.2 Definition der Struktur »Person«
Im ersten Moment mag das zu der ersten Schlussfolgerung verleiten, man könne eine komplette Klasse gleichwertig durch eine Struktur ersetzen, denn Strukturen können Eigenschaften, Methoden und auch Ereignisse definieren, Schnittstellen implementieren und Methoden nach den bekannten Regeln überladen. Es gibt jedoch auch ein paar Einschränkungen, die in Kauf genommen werden müssen, wenn Sie sich anstelle einer Klasse für eine Struktur entscheiden:
- Eine Struktur kann nicht aus einer beliebigen Klasse abgeleitet werden. Grundsätzlich ist ValueType die Basisklasse aller Strukturen. ValueType selbst ist direkt von Object abgeleitet.
- Eine Struktur kann nicht abgeleitet werden.
- Strukturen besitzen immer einen parameterlosen Konstruktor, der auch nicht überschrieben werden darf.
- Felder dürfen nicht mit einem Wert vorinitialisiert werden. Damit würde die folgende
Strukturdefinition zu einem Fehler führen:
struct Person {
private int Age = 0;
[...]
}
6.1.2 Initialisieren einer Strukturvariablen
Wäre der Typ Person als Klasse definiert, müsste vor dem ersten Aufruf eine Instanz durch das Aufrufen des Operators new erzeugt werden:
// Annahme: Person liegt als class-Definition vor
Person pers = new Person();
pers.Age = 34;
Eine Struktur wird demgegenüber von der Laufzeitumgebung jedoch wie die Variable eines elementaren Datentyps eingesetzt, da kein Verweis damit verknüpft ist. Der Zugriff auf die Elemente einer Struktur erfolgt ebenfalls mit dem Punktoperator.
// Person liegt als struct-Definition vor
Person pers = new Person();
pers.Name = "Willi Jakob";
pers.Age = 34;
Die Eigenschaften Name und Age sind einem ganz bestimmten Element zugeordnet, nämlich pers. Das erinnert an die Instanzvariablen einer Klasse. Der Vergleich ist auch nicht falsch, denn eine Strukturvariable ist einem Objektverweis sehr ähnlich und deutet darauf hin, dass es innerhalb einer Struktur einen Konstruktor geben muss, der parameterlos ist. Mit new wird dieser Konstruktor aufgerufen, der – wie auch der Konstruktor einer Klasse – die Felder des Objekts initialisiert.
Vereinfachte Initialisierung
Eine abweichende, einfachere Initialisierung eines Strukturtyps ist unter Umständen ebenfalls möglich. Nehmen wir an, die Klasse Person wäre folgendermaßen implementiert:
struct Person {
public string Name;
public int Age;
}
Nun lässt sich ein Objekt vom Typ Person auch ohne Angabe des Konstruktors erstellen, z. B.:
Person pers;
pers.Name = "Hans";
Dabei ist aber Vorsicht angesagt. Dem Feld Name wird in diesem Fall ausdrücklich ein Wert zugewiesen. Damit ist es auch initialisiert. Das Feld Age hingegen ist noch nicht initialisiert, denn es wird schließlich auch kein Konstruktor aufgerufen. Der Zugriff auf ein nichtinitialisiertes Feld mündet in einen Fehler.
Sie können ohne den Operator new ein Objekt erzeugen, wenn die Struktur ausschließlich Felder hat (automatisch implementierte Eigenschaften zählen nicht zu den Feldern) und keine Methoden. Enthält die Struktur jedoch Methoden, müssen zuerst alle Felder initialisiert werden, um die Methoden des Objekts fehlerfrei aufrufen zu können.
6.1.3 Konstruktoren in Strukturen
Standardmäßig stellt eine Struktur einen parameterlosen Konstruktor bereit, der mit
Person pers = new Person();
aufgerufen werden kann. Strukturen lassen die Definition weiterer Konstruktoren zu, die jedoch parametrisiert sein müssen, denn das Überschreiben des parameterlosen Konstruktors einer Struktur ist nicht erlaubt. Fügen Sie einen parametrisierten Konstruktor hinzu, muss eine Bedingung erfüllt werden: Alle Felder der Struktur müssen initialisiert werden. Im folgenden Listing wird das gezeigt:
public struct Person {
public string Name { get; set; }
public int Age { get; set; }
// Konstruktor
public Person(string name) {
Age = 0;
Name = name;
}
}
Listing 6.3 Konstruktor in einer Struktur
Der Aufruf eines parametrisierten Konstruktors führt nur über den new-Operator. Vorsicht ist hierbei geboten, denn das folgende Codefragment hat die doppelte Initialisierung der Variablen pers zur Folge, weil in der zweiten Anweisung ein parametrisierter Konstruktor aufgerufen wird:
Person pers;
pers = new Person("Willi");
Läge Person eine Klasse zugrunde, würde es nur zu einem Konstruktoraufruf kommen.
6.1.4 Änderung im Projekt »GeometricObjects«
Wir wollen uns nun erneut der Anwendung GeometricObjectsSolution zuwenden. An einer Stelle bietet es sich an, eine Struktur einzusetzen: Es handelt sich dabei um die beiden Mittelpunktskoordinaten XCoordinate und YCoordinate, die in der Klasse GeometricObject definiert sind und nun durch die Struktur Point ersetzt werden sollen. Der Typ Point ist sehr einfach aufgebaut und hat nur zwei Eigenschaften, die später den Bezugspunkt des geometrischen Objekts beschreiben sollen. Selbstverständlich werden die entsprechenden Felder auch gekapselt, d. h. über die Kombination der beiden Accessoren get und set veröffentlicht. Außerdem enthält die Struktur einen zweiparametrigen Konstruktor, dem beim Aufruf die Punktkoordinaten übergeben werden.
public struct Point {
// Felder
private double _X;
private double _Y;
// Eigenschaften
public double X {
get { return _X; }
set { _X = value; }
}
public double Y {
get { return _Y; }
set { _Y = value; }
}
// Konstruktor
public Point(double x, double y) {
_X = x;
_Y = y;
}
}
Listing 6.4 Komplette Definition der Struktur »Point«
In der Klasse GeometricObject zieht das selbstverständlich Änderungen nach sich. Wir definieren zuerst ein Feld vom Typ der Struktur. Dieses soll in der überarbeiteten Fassung die Werte des Bezugspunktes aufnehmen.
protected Point _Center = new Point();
Dabei sollte explizit der parameterlose Konstruktor aufgerufen werden, damit X und Y im Feld _Center von Anfang an initialisiert sind und einen definierten Anfangszustand haben.
Einen wichtigen Punkt dürfen wir an dieser Stelle nicht außer Acht lassen. Da wir nun mit dem Feld _Center eine Point-Struktur eingeführt haben, die die Werte von XCoordinate und YCoordinate speichern soll, müssen wir die beiden noch vorhandenen privaten Felder _XCoordinate und _YCoordinate aus der Klasse GeoemtricObject löschen. Darüber hinaus gilt es, die beiden Eigenschaften XCoordinate und YCoordinate so anzupassen, dass die den Eigenschaften übergebenen Werte an die Felder der Point-Struktur übergeben bzw. daraus ausgelesen werden.
public virtual double XCoordinate {
get { return _Center.X; }
set {
_Center.X = value;
OnPropertyChanged("XCoordinate");
}
}
public virtual double YCoordinate {
get { return _Center.Y; }
set {
_Center.Y = value;
OnPropertyChanged("YCoordinate");
}
}
Listing 6.5 Änderung der Eigenschaften »XCoordinate« und »YCoordinate«
Von der Einführung der Struktur Point sind auch die Konstruktoren von Circle und Rectangle betroffen, die die beiden Mittelpunktskoordinaten in ihren Parametern erwarten.
public Circle(int radius, double x, double y) {
Radius = radius;
_Center.X = x;
_Center.Y = y;
Circle._CountCircles++;
}
public Rectangle(int length, int width, double x, double y) {
Length = length;
Width = width;
_Center.X = x;
_Center.Y = y;
Rectangle._CountRectangles++;
}
Listing 6.6 Änderung der Konstruktoren in »Circle« und »Rectangle«
Eine gute Klassendefinition zeichnet sich nicht nur dadurch aus, dass sie die Implementierung auf das Notwendigste beschränkt, sondern deckt auch die Fälle ab, die für einen Benutzer unter Umständen sinnvoll sein könnten. Soll der Mittelpunkt eines Kreisobjekts diagonal verschoben werden, sind zwei Anweisungen notwendig. Vorteilhafter ist es, dasselbe mit einer Anweisung zu erreichen. Die Verbesserung soll durch eine Methode erzielt werden, die wir als Move bezeichnen. Sie nimmt ein Point-Objekt vom Aufrufer entgegen und wird in GeometricObject definiert.
public virtual void Move(Point center) {
_Center = center;
}
Da eine Struktur ein Wertetyp ist, schreiben sich die Felder X und Y der im Parameter center übergebenen Koordinaten in die gleichlautenden Felder von _Center.
Sehen wir uns nun in einem Codefragment an, wie einfach es ist, diese Methode zu benutzen. Es wird dabei davon ausgegangen, dass ein konkretes Circle-Objekt namens kreis vorliegt. Mit
Point pt = new Point(150, 315);
kreis.Move(pt);
übergeben wir der Methode ein Point-Objekt. Benötigen wir dieses Objekt zur Laufzeit der Anwendung nicht mehr, kann es auch in der Argumentenliste erzeugt werden.
kreis.Move(new Point(150, 315));
Zuletzt ergänzen wir die Klassen Circle und Rectangle noch um einen Konstruktor, der neben dem Radius bzw. den entsprechenden Längenangaben eine Point-Referenz als Argument erwartet:
public Circle(int radius, Point center) {
Radius = radius;
_Center = center;
Circle._CountCircles++;
}
public Rectangle(int length, int width, Point center) {
Length = length;
Width = width;
_Center = center;
Rectangle._CountRectangles++;
}
Listing 6.7 Zusätzliche Konstruktoren in »Circle« und »Rectangle«
Da bei Aufruf dieses Konstruktors kein weiterer der Klasse ausgeführt wird, ist es auch notwendig, den Objektzähler der jeweiligen Klasse zu erhöhen.
Damit wird eine Instanziierung der Klasse Circle beispielsweise mit
Circle kreis = new Circle(2, new Point(5, 12));
möglich.
Den Code des Beispiels GeometricObjectsSolution mit allen Änderungen, die wir bisher in diesem Kapitel vorgenommen haben, finden Sie auf der Buch-DVD unter ...\Kapitel 6\GeometricObjectsSolution_7.
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.