3.7 Eigenschaften
»Die Ungeduld ist ein schnelles Pferd, aber ein schlechter Reiter.« (Serbisches Sprichwort)
Der direkte Zugriff auf Objektzustände mag schnell sein, aber er kann leicht dazu führen, dass ein Objekt »ungültig« wird. So ist es zum Beispiel unsinnig, einem Rechteck eine negative Länge zu geben. Es kann auch passieren, dass durch das Ändern eines Objektzustandes Aktionen erforderlich werden. So sollte vor Umleitung des Standardausgabekanals geprüft werden, ob dieser Kanal benutzbar ist, um zu vermeiden, dass Ausgaben im Nirwana landen. Daher beschäftigen wir uns nun mit einem kontrollierten Zugriff auf Objektzustände.
3.7.1 Kontrolle beim Aufrufer
Die einfachste Art, »falsche« Werte für Objektzustände zu vermeiden, ist es, vor jeder Zuweisung die Werte auf ihre Richtigkeit zu überprüfen. Dies hat ein paar Nachteile:
- Bei jeder Zuweisung muss vorher die Richtigkeit kontrolliert werden.
- Wird eine Kontrolle vergessen, kann das Objekt »ungültig« werden.
- Ändern sich die Bedingungen der Kontrolle, müssen alle Zuweisungen angepasst werden.
Diese Nachteile machen es fast unmöglich, eine Klasse an andere weiterzugeben, da es in der Praxis fast sicher ist, dass bei so vielen Stellen irgendwo etwas schiefgeht. Wir werden daher diese Möglichkeit hier nicht weiter verfolgen.
3.7.2 Zugriffsmethoden
Mit den bisherigen Mitteln können wir den Zugang zu Objekteigenschaften mittels Methoden kontrollieren, wenn wir die Objektzustände als privat kennzeichnen, um Änderungen von außen zu unterbinden. Das folgende Codefragment zeigt ein Quadrat, dessen Länge nicht null oder negativ werden kann. Dadurch, dass auch der Konstruktorparameter die Zugriffsmethode durchläuft, ist es auch nicht möglich, ein »ungültiges« Quadrat zu erzeugen. Der Grund für die Namen der Zugriffsmethoden wird im nächsten Abschnitt klar.
'...\Klassendesign\Eigenschaften\Zugriffsmethoden.vb |
Option Explicit On Namespace Klassendesign Class QuadratZugriff Private Länge As Double Sub set_Länge(ByVal value As Double) If value <= 0 Then Throw New ArgumentException() Länge = value End Sub Function get_Länge() As Double Return Länge End Function Sub New(ByVal value As Double) set_Länge(value) End Sub End Class Module Zugriffsmethoden Sub Test() Dim q As QuadratZugriff = New QuadratZugriff(8) Try q.set_Länge(-4) Catch ex As Exception Console.WriteLine("Negative Länge.") End Try Console.WriteLine("Quadrat {0}x{0}", q.get_Länge()) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die Zuweisung der negativen Länge zurückgewiesen wurde:
Negative Länge. Quadrat 8x8
3.7.3 Getter und Setter: Property
Das vorherige Beispiel kann mithilfe von Eigenschaftsmethoden konsistenter formuliert werden. Die Syntax solcher Zugriffe lautet (optionale Teile stehen in eckigen Klammern und kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
[<Modifikatoren>] Property Name ([<Indizes>]) As Typ [Effekt] [<Sichtbarkeit>] Get [<Anweisungen>] End Get [<Sichtbarkeit>] Set(Wert As Typ) [<Anweisungen>] End Set End Property |
Ob zuerst Get oder Set definiert wird, spielt keine Rolle. Die Indizes können beliebige Typen haben. Der Typ der Eigenschaft und des Wert-Parameters des Set-Teils müssen übereinstimmen. Tabelle 3.9 zeigt die erlaubten Modifikatoren und Tabelle 3.10 die Effekte.
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (siehe Abschnitt 3.2, »Kapselung«) |
Bindung |
Objekt- oder klassenbezogen (siehe Abschnitt 3.4, »Bindung«) |
Redefinition |
Art des Ersatzes oder Zwangs zu einer Definition (siehe Abschnitt 3.13, »Vererbung« und Abschnitt 3.3.4, »Überladung (Overloads)«; Letzteres nur für Indizes) |
Standard |
Standardeigenschaften (siehe Abschnitt 3.7.5, »Standardeigenschaft: Default«) |
Art | Beschreibung |
Implementation |
Erfüllung einer Schnittstelle (siehe Abschnitt 3.15, »Schnittstellen: Interface und Implements«) |
Die Benutzung einer Eigenschaft ist sehr einfach:
- Eigenschaft = wert ruft automatisch die Set-Methode auf und übergibt wert.
- Eigenschaft allein ruft automatisch die Get-Methode auf.
Damit ist es nach außen völlig transparent, ob man ein Feld oder eine Eigenschaft verwendet. Dies erleichtert auch enorm die Änderung eines Feldes in eine Eigenschaft: Nur die Deklaration muss angepasst werden; bei der Benutzung ändert sich rein gar nichts.
Das folgende Codefragment hat die gleiche Semantik (Bedeutung) wie das Beispiel des vorigen Abschnitts. Der Zugriff auf die Eigenschaft in der Methode Test erfolgt, als wäre es ein Objektfeld.
'...\Klassendesign\Eigenschaften\GetUndSet.vb |
Option Explicit On Namespace Klassendesign Class QuadratGetSet Private len As Double Property Länge() As Double Get Return len End Get Set(ByVal val As Double) If val <= 0 Then Throw New ArgumentException() len = val End Set End Property Sub New(ByVal value As Double) Länge = value End Sub End Class Module Eigenschaftsmethoden Sub Test() Dim q As QuadratGetSet = New QuadratGetSet(7) Try q.Länge = –3 Catch ex As Exception Console.WriteLine("Negative Länge. ") End Try Console.WriteLine("Quadrat {0}x{0}", q.Länge) Console.ReadLine() End Sub End Module End Namespace
Im vorigen Abschnitt wurden als Namen der Zugriffsmethoden set_Länge und get_Länge gewählt. Wenn Sie nun versuchen, in der nun definierten Klasse mit der Eigenschaft Länge eine dieser Methoden zu definieren, bekommen Sie vom Compiler eine Fehlermeldung, die besagt, dass diese Methoden implizit bereits definiert sind und ein Namenskonflikt vorliegt. Trotzdem ist es Ihnen nicht gestattet, eine dieser Methoden direkt selbst aufzurufen. Dies bleibt der Eigenschaft vorbehalten. Wenn Sie dennoch darauf bestehen, die Methoden selbst aufzurufen, bleibt Ihnen nur der Weg über eine dynamische Typanalyse, Reflection genannt. Das folgende Codefragment spricht die Get-Methode an und ist nur als Hinweis gedacht, dass manchmal mehr möglich ist, als es auf den ersten Blick scheint.
Dim q As QuadratGetSet = New QuadratGetSet(7) Dim t As Type = GetType(QuadratGetSet) Dim m As System.Reflection.MethodInfo = t.GetMethod("get_Länge") Dim len As Double = m.Invoke(q, New Object() {}) Console.WriteLine("len {0}", len)
Hinweis |
Automatisch implementierte Eigenschaften wie in C# gibt es in Visual Basic nicht. |
3.7.4 Indexer
Eine Eigenschaft kann über Parameter zusätzlich qualifiziert werden. Die Anzahl und Typen der Parameter sind beliebig, aber alle müssen mit ByVal als Wert übergeben werden. Unter gleichem Namen dürfen Eigenschaften existieren, die über verschiedene Arten (und/oder Anzahl) von Parametern angesprochen werden. Dies ist analog zur Methodenüberladung (siehe Abschnitt 3.3.4, »Überladung (Overloads)«). Da wie dort der Rückgabetyp nicht zur Signatur gehört (siehe Abschnitt 3.3.1, »Prinzip«), dürfen gleich benannte Eigenschaften mit verschiedenen Parameterlisten unterschiedliche Typen haben. Programmtechnisch gesehen, haben sie nichts miteinander zu tun; der gleiche Name macht es Ihnen leichter, den Quelltext zu lesen.
Das nächste Codefragment stellt für platonische Körper deren Eckenzahl zur Verfügung. Es gibt nur fünf solcher Körper, die durch identische regelmäßige Vielecke begrenzt sind. Dadurch ist es nicht nötig, ein Objekt zu generieren, und alle Klassenelemente sind mit Shared an die Klasse gebunden. Auf einen Körper kann sowohl lesend als auch schreibend entweder mit einem numerischen Integer-Index oder mit dem String-Namen des Körpers zugegriffen werden. Beim Aufruf mit einem Namen wandelt Array.IndexOf diesen in einen Index um und verwendet dann die mit einer ganzen Zahl indizierte Ecken-Eigenschaft, sodass alle Zugriffe über diese laufen. Dort wäre der Platz für Prüfroutinen, die hier weggelassen worden sind, um das Beispiel einfach zu halten.
'...\Klassendesign\Eigenschaften\Indexer.vb |
Option Explicit On Namespace Klassendesign Class Platonisch Friend Shared ReadOnly Fig() As String = { _ "Tetraeder", "Würfel", "Oktaeder", "Dodekaeder", "Ikosaeder"} Private Shared eckenzahl(4) As Byte Shared Property Ecken(ByVal index As Integer) As Byte Get Return eckenzahl(index) End Get Set(ByVal val As Byte) eckenzahl(index) = val End Set End Property Shared Property Ecken(ByVal name As String) As Byte Get Return Ecken(Array.IndexOf(Fig, name)) End Get Set(ByVal val As Byte) Ecken(Array.IndexOf(Fig, name)) = val End Set End Property End Class ... End Namespace
Die Verwendung der überladenen Eigenschaft kann über den Namen oder den numerischen Index erfolgen. Da die Eigenschaft an die Klasse gebunden ist, erfolgt der Zugriff über den Klassennamen. Wie die dritte Zuweisung zeigt, sind auch Operatoren erlaubt, die implizit den Zuweisungsoperator verwenden.
'...\Klassendesign\Eigenschaften\Indexer.vb |
Option Explicit On Namespace Klassendesign ... Module Indexer Sub Test() Platonisch.Ecken("Tetraeder") = 4 Platonisch.Ecken(1) = 7 'Würfel Platonisch.Ecken("Würfel") += 1 Platonisch.Ecken("Oktaeder") = 6 Platonisch.Ecken("Dodekaeder") = 20 Platonisch.Ecken("Ikosaeder") = 12 Try Platonisch.Ecken("Unbekannt") = 10 Catch ex As Exception Console.WriteLine("Fehler: " & ex.Message) End Try Console.WriteLine("{0} hat {1} Ecken", "Würfel", _ Platonisch.Ecken("Würfel")) Console.WriteLine("{0} hat {1} Ecken", "Dodekaeder", _ Platonisch.Ecken(3)) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass der »falsche« Index erfasst wurde und dass der namentliche und indizierte Zugriff gleichwertig sind:
Fehler: Index was outside the bounds of the array. Würfel hat 8 Ecken Dodekaeder hat 20 Ecken
Implizite Umwandlung
Durch implizite Umwandlungen kann es Ihnen passieren, dass eine Eigenschaft angesprochen wird, auch wenn der Typ nicht passt. Im nächsten Codefragment werden die Zahlen von der Eigenschaft akzeptiert, obwohl eine Zeichenkette erwartet wird:
'...\Klassendesign\Eigenschaften\Implizit.vb |
Option Explicit On Namespace Klassendesign Class Implizit Friend Shared ReadOnly m() As String = {8.35} Shared Property Zugriff(ByVal name As String) As String Get Return m(Array.IndexOf(m, name)) End Get Set(ByVal val As String) m(Array.IndexOf(m, name)) = val End Set End Property End Class Module Typumwandlung Sub Test() Implizit.Zugriff(8.35) = –8.35 Console.WriteLine(Implizit.Zugriff(-8.35)) Console.ReadLine() End Sub End Module End Namespace
Hinweis |
Beim Aufruf einer indizierten Eigenschaft werden, wie bei Methoden, auch benutzerdefinierte implizite Umwandlungen berücksichtigt. |
3.7.5 Standardeigenschaft: Default
Um den Zugriff auf parametrisierte Eigenschaften zu vereinfachen, kann je eine Eigenschaft pro Klasse mit Default als Standardeigenschaft gekennzeichnet werden. Bei einer solchen Eigenschaft muss der Eigenschaftsname beim Zugriff nicht angegeben werden. Bei einparametrigen Eigenschaften mit einer Zeichenkette als Parameter kann die Eingabe durch die Verwendung des Ausrufezeichens noch weiter verkürzt werden: Dann fallen selbst Klammern und Anführungszeichen weg.
Das nächste Codefragment hat eine ähnliche Bedeutung (Semantik) wie das erste Beispiel des vorigen Abschnitts. Die Eigenschaft ist nun objektgebunden und als Standardeigenschaft gekennzeichnet. Ebenso wie bisher ist die Eigenschaft überladen (Integer- bzw. String-Parameter).
'...\Klassendesign\Eigenschaften\Default.vb |
Option Explicit On Namespace Klassendesign Class Eder Friend Fig() As String = {} Private Shared zahl(-1) As Byte Default Property Flächen(ByVal index As Integer) As Byte Get Return zahl(index) End Get Set(ByVal val As Byte) zahl(index) = val End Set End Property Default Property Flächen(ByVal name As String) As Byte Get Return Flächen(Array.IndexOf(Fig, name)) End Get Set(ByVal val As Byte) Dim index As Integer = Array.IndexOf(Fig, name) If index < 0 Then index = Fig.Length ReDim Preserve Fig(index) ReDim Preserve zahl(index) Fig(index) = name End If Flächen(index) = val End Set End Property End Class ... End Namespace
Bei der Verwendung kann sowohl beim Schreiben als auch beim Lesen der Eigenschaftsname Fläche wegfallen. Der letzte Zugriff zeigt, dass mit einem Ausrufezeichen die Formulierung noch straffer wird:
'...\Klassendesign\Eigenschaften\Default.vb |
Option Explicit On Namespace Klassendesign ... Module Standard Sub Test() Dim platonisch As Eder = New Eder() platonisch("Ikosaeder") = 20 platonisch("Tetraeder") = 4 platonisch("Würfel") = 6 platonisch("Oktaeder") = 8 platonisch("Dodekaeder") = 12 Console.WriteLine("{0} hat {1} Flächen", _ "Würfel", platonisch("Würfel")) Console.WriteLine("{0} hat {1} Flächen", _ "Oktaeder", platonisch(3)) Console.WriteLine("{0} hat {1} Flächen", _ "Tetraeder", platonisch!Tetraeder) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt den korrekten Zugriff:
Würfel hat 6 Flächen Oktaeder hat 8 Flächen Tetraeder hat 4 Flächen
3.7.6 Schreibschutz und Leseschutz: ReadOnly und WriteOnly
Alle bisherigen Eigenschaften konnten gelesen und geschrieben werden. Es gibt Fälle, in denen dies unerwünscht ist. Zum Beispiel besteht ein fester Zusammenhang zwischen der Seitenlänge eines Quadrats und seiner Fläche. Da sich die Größe des Quadrats ändern kann, die Formel für die Flächenberechnung aber konstant bleibt, bietet es sich an, die Fläche mit einer Methode zu ermitteln. Eine Alternative ist die Verwendung einer mit ReadOnly schreibgeschützten Eigenschaft. Ob man eine Methode oder eine ReadOnly-Eigenschaft wählt, ist Geschmackssache.
Analog kann sich die Gelegenheit ergeben, dass ein Wert gespeichert werden soll, aber danach nicht mehr von außen zugänglich sein soll. Ein Beispiel ist die Speicherung eines Kennworts. Es reicht, wenn dieses intern zu Vergleichszwecken verwendet wird. Es sollte nicht lesbar sein.
Das folgende Codefragment definiert ein Quadrat, dessen Länge gesetzt, aber nicht mehr ausgelesen werden kann:
'...\Klassendesign\Eigenschaften\Schutz.vb |
Option Explicit On Namespace Klassendesign Class QuadratischeFläche Private len As Double WriteOnly Property Länge() As Double Set(ByVal val As Double) If val <= 0 Then Throw New ArgumentException() len = val End Set End Property ReadOnly Property Fläche() As Double Get Return len * len End Get End Property Sub New(ByVal value As Double) Länge = value End Sub End Class ... End Namespace
Eine geschützte Eigenschaft (ReadOnly oder WriteOnly) wird genauso benutzt wie jede andere Eigenschaft. Der Schutz betrifft die Möglichkeit, das Schreiben oder Lesen ganz zu unterbinden. Dies hat keinen Einfluss darauf, wie eine erlaubte Aktion durchgeführt wird.
'...\Klassendesign\Eigenschaften\Schutz.vb |
Option Explicit On Namespace Klassendesign ... Module Schutz Sub Test() Dim q As QuadratischeFläche = New QuadratischeFläche(7) q.Länge = 4 Try q.Länge = –3 Catch ex As Exception Console.WriteLine("Negative Länge. ") End Try Console.WriteLine("Quadratfläche {0}", q.Fläche) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bietet keine Überraschung:
Negative Länge. Quadratfläche 16
3.7.7 Sichtbarkeit
Die Sichtbarkeit der Get- und der Set-Methode können Sie getrennt steuern. Das folgende Codefragment zeigt eine mittels einer privaten Set-Methode schreibgeschützte Eigenschaft, die jeden Lesezugriff protokolliert. Eine Konstante könnte den Aspekt des Schreibgeschützten erfüllen, nicht aber die Protokollierung. Seiteneffekte mit permanenter Wirkung, wie zum Beispiel die Änderung von Variablen, sollten in Leseroutinen vermieden werden, da diese sich konzeptuell wie eine einfache Variable verhalten sollten.
'...\Klassendesign\Eigenschaften\Sichtbarkeit.vb |
Option Explicit On
Namespace Klassendesign
Class Erlaubnis
Private erlaubt As Boolean
Friend ReadOnly log As String
Property Darf() As Boolean
Get
Console.WriteLine("Zugriff {0}", log)
Return erlaubt
End Get
Private Set(ByVal val As Boolean)
erlaubt = val
End Set
End Property
Sub New(ByVal user As String)
Me.log = user : erlaubt = user = "Admin"
End Sub
End Class
Module Sichtbarkeit
Sub Test()
Dim admin As Erlaubnis = New Erlaubnis("Admin")
Dim user As Erlaubnis = New Erlaubnis("Nutzer")
Console.WriteLine("{0} darf {1}",admin.log,admin.Darf)
Console.WriteLine("{0} darf {1}", user.log, user.Darf)
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe zeigt die Protokollierung der Zugriffe:
Zugriff Admin Admin darf True Zugriff Nutzer Nutzer darf False
3.7.8 Klammern
In seltenen, zweideutigen Situationen wird ein geklammerter Ausdruck vorrangig als parametrisierte Eigenschaft interpretiert und nicht zum Beispiel als Arrayzugriff. Das folgende Codefragment hat im Else-Zweig der Eigenschaft den Zugriff Q(i-1). Er wird als Aufruf der Eigenschaft interpretiert (rekursiver Aufruf der Eigenschaft) und nicht als Indizierung des zurückgegebenen Arrays (As Int32()). Die Eigenschaft berechnet eine Liste der ersten i Quadrate (das ginge auch einfacher mit einer Schleife …).
'...\Klassendesign\Eigenschaften\Klammern.vb |
Option Explicit On Namespace Klassendesign Class Mathe Shared ReadOnly Property Q(ByVal i As Int32) As Int32() Get If i = 0 Then Q = New Int32() {} _ Else Q = Q(i-1).Concat(New Int32() {i*i}).ToArray() End Get End Property End Class Module Klammern Sub Test() Randomize() For Each data As Integer In Mathe.Q(7) Console.Write(data & " ") Next Console.WriteLine() Console.ReadLine() End Sub End Module End Namespace
3.7.9 Grafikbibliothek: Eigenschaften des Rechtecks
In diesem Abschnitt erweitern wir die in Abschnitt 3.1.12, »Grafikbibliothek: Beispiel für Kapitel 3 und 4«, eingeführte und zuletzt in Abschnitt 3.6.5, »Grafikbibliothek: Konstanten des Rechtecks«, erweiterte Grafikanwendung. Die Länge wird als Eigenschaft zugänglich gemacht, wobei der Aufruf Größe() in Set negative Längen abfängt (siehe Abschnitt 3.3.7, »Grafikbibliothek: Zugriffsmethoden auf die Größe des Rechtecks«). Die über einen Namen spezifizierten Abmessungen sind nur-lesbar.
'...\Klassendesign\Graphik\Eigenschaften.vb |
Option Explicit On Namespace Klassendesign Partial Public Class Rechteck Property Länge() As Double Get Return a End Get Set(ByVal a As Double) Größe(a, b) End Set End Property Default ReadOnly Property Abmessung(ByVal was As String) As Double Get Select Case was Case "Länge" : Return a Case "Breite" : Return b Case Else Throw New ArgumentException("Weder Länge noch Breite.") End Select End Get End Property End Class End Namespace
Zuerst wird ein neues Rechteck erzeugt und dessen Größe ausgegeben. Dann wird die Länge über die gleichnamige Eigenschaft geändert und erneut die Größe protokolliert. Die Zuweisung einer negativen Länge erzeugt eine Ausnahme und wird in einen Try-Catch-Block eingeschlossen. Die vorletzte Ausgabe dient zur Prüfung, dass die Länge daraufhin tatsächlich unverändert ist. Dieselbe Länge wird dann erneut über die Standardeigenschaft ausgelesen.
'...\Klassendesign\Zeichner\Eigenschaften.vb |
Option Explicit On Namespace Klassendesign Partial Class Zeichner Sub Eigenschaften() Dim rechteck As New Rechteck(7, 8) : rechteck.Größe() rechteck.Länge = 4 : rechteck.Größe() Try rechteck.Länge = –3 Catch ex As Exception Console.WriteLine("Ausnahme: " & ex.Message) End Try Console.WriteLine("Länge des Rechtecks: {0}", rechteck.Länge) Console.WriteLine("Länge des Rechtecks: {0}", rechteck("Länge")) End Sub End Class End Namespace
Zur Kontrolle sehen Sie hier die Ausgabe der Methode Eigenschaften():
Dimension des Rechtecks: 7x8 Dimension des Rechtecks: 4x8 Ausnahme: Negative Länge! Länge des Rechtecks: 4 Länge des Rechtecks: 4
Die nächste Erweiterung erfolgt in Abschnitt 3.8.3, »Grafikbibliothek: Position des Rechtecks«.
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.