4.2 Strukturen
Alle Objekte, die als Datentyp eine Klasse haben, nutzen eine Referenz auf das Objekt für den Zugriff auf das Objekt. Bei einer Weitergabe des Objekts wird nur die Referenz und nicht das Objekt selbst weitergegeben. Demgegenüber haben Sie bei Werttypen das Objekt selbst »in der Hand«. Damit nun das Originalobjekt nicht »verschwindet«, wird bei einer Weitergabe jedes Mal eine Kopie angefertigt. Dieser Unterschied zwischen Referenz- und Werttypen ist in Abschnitt 3.1.9, »Datentypen«, näher beschrieben. Mit den Strukturen haben Sie zum ersten Mal die Möglichkeit, selbst Werttypen zu definieren. Sie haben folgende Syntax (optionale Teile stehen in eckigen Klammern, kursiv gesetzte Teile müssen Sie Ihren Erfordernissen anpassen, zur Typen-Spezifikation siehe Abschnitt 4.4, »Generisches«):
[<Modifikatoren>] Structure Struktur[Typen] [Implements <Schnittstellen>] Variable oder Ereignis ... End Structure |
Eine Struktur ist bis auf das Wort Structure statt Class identisch mit einer Klasse (inklusive der Aufteilbarkeit mit Partial), außer dass sie
- nie mit Inherits die Elternklasse angibt, sondern implizit von ValueType erbt (konsequenterweise ist die Verwendung von MyBase verboten),
- nie beerbt werden kann, sondern immer implizit NotInheritable ist,
- als Modifikatoren nur Partial, Public und Friend erlaubt – sowie Shadows, Private und Protected für Strukturen als Teil anderer Datentypen,
- mindestens eine Variable oder ein Ereignis deklarieren muss,
- nie benutzerdefinierte parameterlose Objektkonstruktoren hat (ein mit Shared deklarierter Klassenkonstruktor ist erlaubt),
- den parameterlosen Konstruktor nicht durch benutzerdefinierte Konstruktoren verliert,
- nie objektgebundene Strukturvariablen innerhalb der Deklaration initialisiert (mit Shared deklarierte klassengebundene Variablen dürfen initialisiert werden) und
- für Strukturvariablen WithEvents nicht spezifizieren darf und auch nicht als Datentyp für eine beliebige Variable benutzt werden darf, die WithEvents spezifiziert.
Die folgenden Codefragmente sind daher alle fehlerhaft:
'Compilerfehler: Struktur enthält weder eine Variable noch ein Ereignis!! Structure Struktur End Structure 'Compilerfehler: Angabe einer Elternklasse!! Structure Struktur Inherits ValueType Dim var As Single End Structure 'Compilerfehler Struktur: Strukturen sind NotInheritable!! Structure Basis : Dim var As Single : End Structure Structure Struktur : Inherits Basis : Dim var As Single : End Structure Class Klasse : Inherits Basis : End Class Structure Struktur Dim var As Single = 0 'Compilerfehler: Initialisierung!! Sub New() 'Compilerfehler: parameterlos ohne Shared!! End Sub WithEvents x As String 'Compilerfehler: WithEvents!! End Structure
Als erstes Beispiel schauen wir uns die Definition einer Person an. Es fällt auf, dass eine Objekterzeugung mit New fehlt. Da eine Struktur ein Werttyp ist, werden ihre Daten direkt in dem Block gespeichert, in dem die Strukturvariable deklariert wird, und nicht auf dem blockunabhängigen Heap, der alle Referenztypen speichert. Die Struktur enthält auch eine Methode Info(), um darauf aufmerksam zu machen, dass Strukturen nicht auf Variablen und Ereignisse beschränkt sind. Der Zugriff in Test() unterscheidet sich formal nicht von dem, den Objekte einer Klasse nutzen.
'...\Datentypen\Strukturen\Prinzip.vb |
Option Strict On Namespace Datentypen Structure Person Friend Name As String Sub Info() Console.WriteLine("Name: {0}", Name) End Sub End Structure Module Prinzip Sub Test() Dim p As Person 'kein New!! p.Info() p.Name = "Winston Smith" p.Info() Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe zeigt, dass der durch die Deklaration implizit aufgerufene parameterlose Konstruktor keinen Einfluss auf die Strukturvariable hat. Sie ist mit einem Leerstring "" belegt. Andere Datentypen erhalten analog eine datentypspezifische »Null« (siehe Abschnitt 2.5.6, »Initialisierung von Variablen«). Nach der Zuweisung eines Namens wird dieser in der zweiten Ausgabe verwendet.
Name: Name: Winston Smith
Hinweis |
Ganz neue Werttypen analog zu Strukturen können nicht von Ihnen geschaffen werden. |
4.2.1 Vor- und Nachteile von Referenz- und Werttypen
Klassen und Strukturen unterscheiden sich im Wesentlichen darin, wo die Daten eines Objekts gespeichert werden. Erstere sind Referenztypen, und Variablen dieses Typs halten nur einen Verweis auf die eigentlichen Daten, während bei Letzteren die Daten direkt in der Objektvariablen gespeichert sind. Wird eine Variable an eine Methode weitergegeben oder einer anderen Variablen zugewiesen, erfolgt bei einem Referenztyp der Zugriff immer auf die Originaldaten, während bei Werttypen mit einer Kopie der Daten gearbeitet wird. Zusammengefasst hat ein Werttyp folgende Charakteristika:
- Vorteile: Originaldaten bleiben erhalten, Objektkopien erfolgen implizit durch Weitergabe.
- Nachteile: Originaldaten können nicht geändert werden, Kopien verbrauchen Ressourcen.
Um den Kopiermechanismus zu veranschaulichen, zeigt das nächste Codefragment die Veränderung einer Struktur namens Glasscheibe in einer Methode Verkauf(), die die Struktur als Parameter entgegennimmt und die Mitgliedsvariable Preis ändert. Die Änderung der Struktur selbst ist erforderlich, da die Methode Info() die Struktur selbst als einzige Informationsquelle nutzt. Ob das ein glückliches Design darstellt, sei dahingestellt, hier kommt es nur auf den Mechanismus des Kopierens an.
'...\Datentypen\Strukturen\Kopien.vb |
Option Strict On Namespace Datentypen Structure Glasscheibe Public Preis As Double Public Farbe As String Sub Info() Console.WriteLine("{0}es Glas kostet {1}", Farbe, Preis) End Sub End Structure Module Kopien Sub Verkauf(ByVal glas As Glasscheibe) glas.Preis *= 1.19 glas.Info() End Sub Sub Test() Dim rot As Glasscheibe : rot.Farbe = "Rot" : rot.Preis = 172 Verkauf(rot) : Verkauf(rot) Dim blau As Glasscheibe = rot : blau.Farbe = "Blau" : blau.Preis = 124 Verkauf(rot) : Verkauf(blau) Console.ReadLine() End Sub End Module End Namespace
Die ersten beiden Ausgaben zeigen, dass die Änderung der Strukturvariablen in der Methode Verkauf() sich trotz des zweimaligen Aufrufs nicht auf das Original auswirkt und daher immer der korrekte Preis ausgegeben wird (und nicht durch jeden Verkauf erhöht wird). Auch bei der Zuweisung der Variablen blau wird keine zweite Referenz auf dieselben Daten erzeugt, sondern die Daten werden kopiert, wie die gegenüber den ersten Zeilen unveränderte Ausgabe in der dritten Zeile zeigt:
Rotes Glas kostet 204.68 Rotes Glas kostet 204.68 Rotes Glas kostet 204.68 Blaues Glas kostet 147.56
Hinweis |
Bei Werttypen gibt es nie zwei Referenzen auf dieselben Daten. |
4.2.2 Initialisierung und Konstruktoren
Strukturen haben natürlich auch die Möglichkeit, Konstruktoren zu deren Initialisierung zu definieren. Anders als bei Klassen muss jeder selbst definierte Konstruktor Parameter besitzen, wobei der parameterlose Standardkonstruktor in jeder Situation zur Verfügung gestellt wird (und gar nicht selbst definiert werden kann). Damit geht einher, dass es unmöglich ist, durch private Konstruktoren die Instanziierung einer Struktur komplett zu kontrollieren oder gar zu verhindern. Im Kontext automatisch erstellter Kopien bei der Weitergabe oder Zuweisung ist eine solche Beschränkung sinnvoll. Da die Initialisierung von Variablen in einer Struktur während der Deklaration nicht erlaubt ist, sind benutzerdefinierte Konstruktoren selbst dann sinnvoll, wenn die Struktur mit den immer gleichen Werten ungleich »Null« starten soll. In einem solchen Fall müssen Sie halt mit einem Dummy-Parameter im Konstruktor arbeiten, da Ihnen der parameterlose Konstruktor ja verwehrt ist.
Das nächste Codefragment definiert eine Struktur zur Speicherung eines Punktes mit zwei benutzerdefinierten Konstruktoren, wobei der zweite zufällige Werte für x und y nimmt und die z-Koordinate nicht initialisiert, sondern bei 0 belässt (im Gegensatz zu C# müssen in Visual Basic nicht alle verwendeten Strukturvariablen explizit initialisiert werden). Da die Koordinatenvariablen mit Private deklariert sind, kann ein einmal erzeugter Punkt nicht mehr geändert werden. Die überschriebene Methode ToString() wird »lesbare« Ausgaben produzieren.
'...\Datentypen\Strukturen\Konstruktoren.vb |
Option Strict On Namespace Datentypen Structure Punkt Private x, y, z As Double Sub New(ByVal x As Double, ByVal y As Double, _ Optional ByVal z As Double = z0) 'z0 siehe unten Me.x = x : Me.y = y : Me.z = z End Sub Sub New(ByVal start As Integer) Dim rand As Random = New Random(start) x = rand.NextDouble() : y = rand.NextDouble() End Sub Public Overrides Function ToString() As String Return "{" & x.ToString("0.##") & "," & y.ToString("0.##") & _ "," & z.ToString("0.##") & "}" End Function End Structure ... End Namespace
Zum Test werden Tetraederpunkte mit zwei und drei Koordinaten erzeugt und in der Methode Ausgabe() protokolliert, die ein Array von Punkten erwartet. Analog werden Dreieckspunkte mit einem Parameter generiert und ausgegeben. Schließlich wird bei der Variablen null ganz auf Parameter verzichtet.
'...\Datentypen\Strukturen\Konstruktoren.vb |
Option Strict On Namespace Datentypen ... Module Konstruktoren Public Const z0 As Double = –0.20412414523193151 '-1/(2*Math.Sqrt(6)) Sub Ausgabe(ByVal was As String, ByVal p As Punkt()) Console.Write("{0}: |", was) For Each pt As Punkt In p : Console.Write("{0}|", pt) : Next Console.WriteLine() End Sub Sub Test() Dim x As Double = 1 / (2 * Math.Sqrt(3)) Dim tetraeder As Punkt() = {New Punkt(0, 0, z0 + Math.Sqrt(2 / 3)), _ New Punkt(-x, –1 / 2), New Punkt(-x, 1 / 2), New Punkt(2 * x, 0)} Ausgabe("Tetraeder", tetraeder) Dim dreieck As Punkt() = {New Punkt(0), New Punkt(1), New Punkt(2)} Ausgabe("Dreieck", dreieck) Dim null As Punkt Console.WriteLine("Nullpunkt: {0}", null) null = New Punkt() Console.WriteLine("Nullpunkt: {0}", null) Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe bestätigt die Verwendung des dreiparametrigen Konstruktors unter Verwendung des optionalen z-Wertes. Das Dreieck verwendet den einparametrigen Konstruktor mit zufälligen Koordinaten. Die letzten beiden Ausgaben dokumentieren den impliziten und den expliziten Einsatz des immer vorhandenen parameterlosen Konstruktors.
Tetraeder: |{0,0,0.61}|{-0.29,-0.5,-0.2}|{-0.29,0.5,-0.2}|{0.58,0,-0.2}| Dreieck: |{0.73,0.82,0}|{0.25,0.11,0}|{0.77,0.4,0}| Nullpunkt: {0,0,0} Nullpunkt: {0,0,0}
4.2.3 Boxing
Jedes Objekt ist (unter anderem) vom Typ Object und – beim Einsatz einer Implements-Klausel – vom entsprechenden Schnittstellentyp. Da beide Referenztypen sind, kann die Situation auftreten, dass eine Referenz auf eine Struktur gebraucht wird. Damit die Handhabung einfach bleibt, wird die hierfür notwendige Funktionalität, inklusive des Kopierens, vom Compiler erzeugt bzw. von der Laufzeitumgebung bereitgestellt. Der Mechanismus wird Boxing genannt, da ein Werttyp in einen Referenztyp verpackt wird. Der umgekehrte Weg heißt Unboxing.
Das nächste Codefragment prüft, ob durch das Boxing und Unboxing durch CType eine Kopie der Struktur erstellt wird. Dazu wird durch Zuweisung an eine Variable vom Typ Object eine Referenz auf eine Struktur erzeugt. Danach wird die Struktur geändert, und es werden sowohl die Struktur als auch die Referenz ausgegeben. Analog wird die Referenz an die Strukturvariable zugewiesen, Letztere wird geändert, und beide werden ausgegeben. Dabei muss das Auslesen der Referenz vom Typ Object über Reflection erfolgen, da sie sonst nur über eine Typumwandlung auszulesen wäre und es ja gerade darum geht, ob eine solche Umwandlung eine Kopie erzeugt.
'...\Datentypen\Strukturen\Boxing.vb |
Option Strict On Namespace Datentypen Structure Schauspieler Public Name As String Sub New(ByVal name As String) Me.Name = name End Sub End Structure Module Boxing Sub Test() Dim person As New Schauspieler("Marion Robert Morrison") Dim ref As Object = person Dim name As Reflection.FieldInfo = ref.GetType().GetField("Name") person.Name = "Marion Michael Morrison" Console.WriteLine("Schauspieler {0}", person.Name) Console.WriteLine("Referenz {0}", name.GetValue(ref)) person = CType(ref, Schauspieler) person.Name = "The Duke" Console.WriteLine("Schauspieler {0}", person.Name) Console.WriteLine("Referenz {0}", name.GetValue(ref)) Console.ReadLine() End Sub End Module End Namespace
Der Vergleich der ersten beiden und der letzten beiden Ausgaben zeigt jeweils, dass sowohl beim Boxing als auch beim Unboxing das Original erhalten bleibt und eine Kopie der Struktur erstellt wird.
Schauspieler Marion Michael Morrison Referenz Marion Robert Morrison Schauspieler The Duke Referenz Marion Robert Morrison
Hinweis |
Wie jede andere Art der Konvertierung greifen die Boxing-Mechanismen wenn nötig auf implizite Konvertierungen zurück. |
Hinweis |
Wenn eine Struktur eine Schnittstelle implementiert und als Letztere angesprochen wird, dann findet ein Boxing statt, da jede Schnittstelle ein Referenztyp ist. |
4.2.4 Innere Strukturen
Wie Klassen können Strukturen sowohl andere Datentypen enthalten als auch Teil anderer Datentypen sein. Deklaration und Zugriff funktionieren wie bei Klassen (siehe Abschnitt 3.8, »Innere Klassen«). Eine Besonderheit gibt es aber: Da Strukturen Werttypen sind, wird bei der Weitergabe oder Zuweisung eine Kopie der Struktur erstellt. Wie wird da mit geschachtelten Datentypen verfahren?
Im folgenden Codefragment enthält die Struktur Regal eine Referenzvariable box, deren Klasse die Variable Buchnummer zur Identifikation enthält. In der Methode Test() wird die Struktur instanziiert und danach zur Erstellung einer automatischen Kopie einer anderen Variablen zugewiesen. Schließlich wird die Buchnummer in der ersten Struktur abgeändert, und beide Strukturen werden ausgegeben.
'...\Datentypen\Strukturen\FlacheKopie.vb |
Option Strict On Namespace Datentypen Class Schachtel Public Buchnummer As Integer Sub New(ByVal nummer As Integer) Buchnummer = nummer End Sub End Class Structure Regal Public box As Schachtel Sub New(ByVal nummer As Integer) box = New Schachtel(nummer) End Sub End Structure Module FlacheKopie Sub Test() Dim Wohnzimmer As New Regal(27) Dim Schlafzimmer As Regal = Wohnzimmer Wohnzimmer.box.Buchnummer = 12 Console.WriteLine("Wohnzimmer {0}", Wohnzimmer.box.Buchnummer) Console.WriteLine("Schlafzimmer {0}", Schlafzimmer.box.Buchnummer) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die Änderung der Nummer in der einen Struktur auch die andere Struktur betrifft. Das, worauf die Variable box verweist, ist also nicht kopiert worden.
Wohnzimmer 12 Schlafzimmer 12
Damit lässt sich festhalten:
Kopien von Werttypen sind flach, das heißt, die Daten von Referenzen werden nicht kopiert, sondern nur die Referenz selbst. |
4.2.5 ValueType
Durch die Deklaration einer Struktur wird implizit eine gleichnamige Klasse identischen Inhalts erzeugt, die auf der Klasse ValueType basiert. Sie dürfen diese Vererbungsbeziehung nicht herstellen, dies bleibt dem Compiler vorbehalten. Die Deklaration
Structure Struktur Public Variable As Single End Structure
entspricht folgender Situation (fiktiv in Bezug auf die Darstellung der Struktur als Klasse):
Public NotInheritable Class Struktur Inherits ValueType Public Variable As Single End Struktur Public MustInherit Class ValueType Inherits Object Public Overrides Function GetHashCode() As Integer Public Overrides Function Equals(ByVal obj As Object) As Boolean Public Overrides Function ToString() As String Private Shared Function CanCompareBits(ByVal obj As Object) As Boolean Private Shared Function _ FastEqualsCheck(ByVal a As Object, ByVal b As Object) As Boolean End Class
Daraus können Sie erkennen, dass die Elternklasse aller Werttypen selbst ein Referenztyp ist (so merkwürdig das auch scheinen mag).
Hinweis |
Neben Strukturen zählen noch die Primitiven (außer Strings) und Enumerationen zu den Werttypen. |
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.