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] |
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.