3.14 Polymorphie 

Das Wort Polymorphie bedeutet Vielgestaltigkeit. In Visual Basic bezieht sich das auf das Verhalten und die Eigenschaften eines Objekts, wenn es verwendet wird und dann nicht gemäß der Verwendung, sondern seines wahren Typs reagiert. Wenn Sie zum Beispiel in einem Restaurant ein Essen bestellen, wird es der Koch gemäß seinen Fähigkeiten zubereiten, obwohl Sie keine konkreten Anweisungen dazu gegeben haben. So kann ohne Ihr Zutun aus der einfachen Bestellung »Fischsuppe« eine raffinierte »Bouillabaisse« werden. Als Gast sind Sie nicht primär daran interessiert, wie das gute Essen gemacht wird. Sie möchten bestellen und gut essen. Analog verhält es sich bei Software. Eine Bildbearbeitung zum Beispiel verwendet den immer gleichen Befehl zum Ausdrucken eines Fotos. Dabei soll die Qualität des Ausdrucks immer so gut sein, wie es mit der verwendeten Hardware geht. Damit nicht bei jedem neuen Drucker die Software angepasst werden muss, wird mit einer Referenz auf einen »allgemeinen« Drucker gearbeitet, die auf den gerade aktiven Drucker verweist. So kann während der Laufzeit der Drucker gewechselt werden, ohne Abstriche in der »optimalen« Qualität machen zu müssen.
Fangen wir mit einem einfachen Beispiel an. Das folgende Codefragment definiert einen allgemeinen Drucker mit einer Methode, die eine Überschrift druckt. Davon ist ein Textdrucker abgeleitet, der die Überschrift gesperrt ausgibt. In der Methode Test() wird der Druckbefehl ÜberschriftDrucken() eines Textdruckers über eine Referenz der Basisklasse Drucker ausgeführt.
'...\Klassendesign\Polymorphie\NichtPolymorph.vb |
Option Strict On Namespace Klassendesign Class Drucker Sub ÜberschriftDrucken(ByVal text As String) Console.Write(text) End Sub End Class Class Textdrucker : Inherits Drucker Sub ÜberschriftDrucken(ByVal text As String) For Each ch As Char In text : Console.Write(ch & " ") : Next End Sub End Class Module NichtPolymorph Sub Test() Dim dr As Drucker = New Textdrucker() dr.ÜberschriftDrucken("Überschrift") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die Druckmethode der Basisklasse angesprochen wird, da der Text nicht gesperrt (ohne Leerzeichen) erscheint.
Überschrift
Hinweis |
Es gibt viele Programmierer, die nichtpolymorphe Methoden als ungeeignet für die Vererbung ansehen (Java zum Beispiel bindet fast alle Methoden polymorph). |
3.14.1 Virtuelle Methoden: Overridable und Overrides 

Um die Polymorphie eines Klassenmitglieds zu ermöglichen, muss es mit dem Modifikator Overridable gekennzeichnet werden und, wenn gewünscht, in einer Kindklasse mit dem Modifikator Overrides neu definiert werden. Dadurch legt der Compiler die sogenannte virtuelle Methodentabelle an, die für jedes Objekt das Klassenmitglied auf die Implementierung umlenkt, die dem Typ des Objekts am nächsten liegt.
Die folgende Syntax zeigt alle erlaubten polymorphen Klassenmitglieder. Alle frei wählbaren Bezeichner sind kursiv gesetzt, und Typ und Ret stehen für einen beliebigen Datentyp. Das Zeichen <...> ist ein Platzhalter für einen Zeilenvorschub, optionale Anweisungen und ein korrespondierendes End-Konstrukt. Bis auf den Parameter der Set-Routine sind die Anzahl und Typen der Parameter frei, aber eine Eigenschaft mit Default muss mindestens einen Parameter haben. Optionale Teile sind in eckige Klammern gesetzt, kursive Teile müssen Sie Ihren Bedürfnissen anpassen, und der Unterstrich verbindet wie üblich Teile einer einzelnen logischen Zeile.
Class Virtuell [<Modifikatoren>] Overridable Sub Prozedur([Parameter]) <...> [<Modifikatoren>] Overridable Function Funktion([Parameter]) As Typ <...> [<Modifikatoren>] Overridable [Default] _ Property Eigenschaft([Parameter]) As Ret Get <...> Set(wert As Ret) <...> End Property [<Modifikatoren>] Overridable [Default] ReadOnly _ Property Eigenschaft([Parameter]) As Ret Get <...> End Property [<Modifikatoren>] Overridable [Default] WriteOnly _ Property Eigenschaft([Parameter]) As Ret Set(wert As Ret) <...> End Property End Class |
Die Auswahl an Modifikatoren ist recht beschränkt (siehe Tabelle 3.25).
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit, aber nicht Private (siehe Abschnitt 3.2, »Kapselung«) |
Redefinition |
Verdecken mit Shadows oder ergänzen mit Overloads (siehe Abschnitt 3.13.4, »Modifikation: Shadows und Overloads (Overrides)«) sowie Zwang zur Definition mit MustOverride (siehe Abschnitt 3.13.5, »Abstrakte Klassen: MustInherit und MustOverride«) |
Dabei ist Folgendes zu beachten:
- Felder, Ereignisse, Delegaten, Typen, Operatoren und Konstruktoren sind nie polymorph.
- Polymorphe Klassenmitglieder sind nie mit Shared klassengebunden.
- Es gibt keine polymorphen externen Funktionen.
- Die Sichtbarkeit von Overridable-Mitgliedern muss in der Eltern- und der Kindklasse identisch sein. (Ausnahme: Protected Friend wird in einer anderen Anwendung zu Protected.)
- Private Overridable und Overridable NotOverridable sind verboten,
- MustOverride Overrides ist erlaubt, wenn die Elternklasse ein passendes Mitglied hat.
Damit lässt sich das Druckerbeispiel so formulieren, dass der Druckertyp auch in einer Basisklassenreferenz berücksichtigt wird:
'...\Klassendesign\Polymorphie\Polymorph.vb |
Option Strict On Namespace Klassendesign Class FlexiblerDrucker Overridable Sub ÜberschriftDrucken(ByVal text As String) Console.WriteLine(text) End Sub End Class Class Laserdrucker : Inherits FlexiblerDrucker Overrides Sub ÜberschriftDrucken(ByVal text As String) For Each ch As Char In text : Console.Write(ch & " ") : Next Console.WriteLine() End Sub End Class Module Polymorph Sub Test() Dim dr As FlexiblerDrucker = New Laserdrucker() dr.ÜberschriftDrucken("Überschrift") dr = New FlexiblerDrucker() dr.ÜberschriftDrucken("Überschrift") Console.ReadLine() End Sub ... End Module End Namespace
Nun erfolgt die Ausgabe so gut, wie es der gerade aktuelle Drucker kann, obwohl dieselbe Referenzvariable verwendet wird.
Ü b e r s c h r i f t Überschrift
Damit der Objekttyp auch bei Basisklassenreferenzen berücksichtigt werden kann, gilt:
Durch Overridable wird eine Methode oder Eigenschaft erst zur Laufzeit dynamisch an ein Objekt gebunden. Der Aufruf wird dadurch ein wenig langsamer. |
Sichtbarkeit
Im Gegensatz zu allgemeinen Klassenmitgliedern müssen polymorphe Klassenmitglieder über die Vererbungshierarchie dieselbe Sichtbarkeit haben. Daher wird das folgende Codefragment vom Compiler zurückgewiesen:
Class Basis Protected Overridable Sub f() End Sub End Class Class Kind : Inherits Basis Public Overrides Sub f() 'Compilerfehler: nicht identische Sichtbarkeit End Sub End Class
Eine Besonderheit ergibt sich für eine Kindklasse, die ein polymorphes Klassenmitglied in einer anderen Anwendung überschreibt:
Protected Friend Overridable muss in einer anderen Anwendung (Projekt) mit Protected Overrides überschrieben werden (wenn die Polymorphie erhalten bleiben soll). |
Inhomogene Mengen
Die Polymorphie ist besonders nützlich bei Sammlungen von Objekten verschiedenen Typs mit derselben Basisklasse. Da ein Array homogen sein muss, bleibt nichts anderes übrig, als Referenzen vom Typ der Basisklasse zu speichern. Das ist die Gemeinsamkeit der Objekte. Polymorphe Klassenmitglieder sind dadurch nicht eingeschränkt, da sie auch dann den Typ des Objekts berücksichtigen, wenn nur eine Referenz auf die Basisklasse vorliegt. Das folgende Codefragment nutzt die kleine Druckerhierarchie:
'...\Klassendesign\Polymorphie\Polymorph.vb |
Option Strict On Namespace Klassendesign ... Module Polymorph ... Sub Menge() Dim dr(4) As FlexiblerDrucker Dim rd As Random = New Random(348) For no As Integer = 0 To 4 If rd.NextDouble() < 0.5 Then dr(no) = New FlexiblerDrucker() Else dr(no) = New Laserdrucker() End If Next For Each d As FlexiblerDrucker In dr d.ÜberschriftDrucken(d.GetType().Name) Next Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die typrichtige Druckmethode verwendet wird:
L a s e r d r u c k e r L a s e r d r u c k e r FlexiblerDrucker L a s e r d r u c k e r FlexiblerDrucker
Aufruf über Basisklassenreferenz
Soll in einer polymorphen Methode auf die Methode der Basisklasse zurückgegriffen werden, kann dies nicht direkt erfolgen, denn der Aufruf würde polymorph wieder die Methode selbst erreichen: eine infinite Rekursion. Genau dasselbe passiert bei dem Versuch, über eine Typumwandlung an die Basisklassenmethode mit CType(Me, Basis).Methode() heranzukommen. Man kann dann nur mit MyBase.Methode() auf die nächstgelegene Basisklassendefinition der Methode zugreifen. Ein Überspringen einer Klasse in der Hierarchie ist also nicht möglich.
Das folgende Codefragment testet den Aufruf einer polymorphen Methode über eine Basisklassenreferenz (CType(Me, Basis)) von innerhalb der Methode. Zur Verdeutlichung ist Object als Elternklasse explizit angegeben. Ein Auffangen der Ausnahme StackOverflowException gelingt nur selten, da durch den Überlauf der Speicher meistens bereits so voll ist, dass für einen sauberen Ausstieg über Catch nicht genügend Speicher zur Verfügung steht. Daher verwendet das Beispiel den Zähler rec.
'...\Klassendesign\Polymorphie\MyBase.vb |
Option Strict On Namespace Klassendesign Class Typ : Inherits Object Private rec As Integer Public Overrides Function ToString() As String rec += 1 If rec > 100 Then Return "Gescheitert!" _ Else Return CType(Me, Object).ToString() End Function End Class Module Infinit Sub Test() Dim t As Object = New Typ() Console.WriteLine(t) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt das Scheitern:
Gescheitert!
3.14.2 Unterbrechen: Overloads, Shadows und Overrides 

Wird ein Klassenmitglied mit Overloads oder Shadows gekennzeichnet, unterbricht dies die Polymorphie an dieser Stelle. Die Polymorphie bis zu der Elternklasse ist nicht betroffen, genauso wenig wie der Zugriff auf die Elternklasse. Ebenso ist es möglich, mit den Kombinationen Oveloads Overridable sowie Shadows Overridable eine neue Polymorphie zu beginnen. Es kommt nicht oft vor, dass eine Unterbrechung der Polymorphie angemessen ist. Sie sollten deshalb genau prüfen, ob Ihr Klassendesign sich nicht besser formulieren lässt.
Das folgende Codefragment zeigt eine Anwendung, in der der Bruch der dynamischen Bindung durch die verschiedene Arbeitsweise eines Antriebs in oder außerhalb der Erdatmosphäre bewirkt wird. Allen Antrieben ist gemeinsam, dass sie betankt werden müssen, und daher wird der Methodenname trotz des Bruchs beibehalten. Anders ist die Notwendigkeit, außerhalb der Erdatmosphäre einen Ersatz für die Luft mitzunehmen. Die Linie rechts vom Code deutet die Bereiche an, über die jeweils die Polymorphie funktioniert. Die allgemeine Rakete hat einen Hybridantrieb, und der Atmosphärenteil wird durch den Aufruf von MyBase.Verbrennung() simuliert.
'...\Klassendesign\Polymorphie\OverloadsUndOverrides.vb |
'...\Klassendesign\Polymorphie\OverloadsUndOverrides.vb Option Strict On Namespace Klassendesign Class Triebwerk Sub Verbrennung(ByVal art As String, ByVal kraftstoff As String, _ Optional ByVal gas As String = "Luft") Console.WriteLine("{0,9} als {1,9} verbrennt {2,11} mit {3,10}", _ Me.GetType().Name, art, kraftstoff, gas) End Sub Overridable Sub Verbrennung() '+-- Verbrennung("Triebwerk", "Brennstoff") '| End Sub '| End Class '| Class Düse : Inherits Triebwerk '| Overrides Sub Verbrennung() '| Verbrennung("Düse", "Kerosin") '| End Sub '| End Class '| Class Rakete : Inherits Düse '| Overridable Overloads Sub Verbrennung() '| MyBase.Verbrennung() 'Å-- Atmosphäre Verbrennung("Rakete", "Brennstoff", "Oxidator") '| End Sub '| End Class '| Class Ariane : Inherits Rakete '| Overrides Sub Verbrennung() '| Verbrennung("Ariane", "Wasserstoff", "Sauerstoff")'| End Sub '| End Class '? ... End Namespace
Durch verschiedene Datentypen in den For Each-Schleifen wird die Polymorphie getestet:
'...\Klassendesign\Polymorphie\OverloadsUndOverrides.vb |
Option Strict On Namespace Klassendesign ... Module Unterbrochen Sub Test() Dim er() As Triebwerk = {New Triebwerk(), New Düse(), _ New Rakete(), New Ariane()} Console.WriteLine("--Referenz auf Triebwerk--") For Each t As Triebwerk In er : t. Verbrennung() : Next Console.WriteLine("--Referenz auf Düse--") For Each t As Düse In er.Skip(1) : t. Verbrennung() : Next Console.WriteLine("--Referenz auf Rakete--") For Each t As Rakete In er.Skip(2) : t. Verbrennung() : Next Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass eine Referenz von einem Typ, der oberhalb des Bruchs der dynamischen Bindung liegt (Triebwerk und Düse) in der Polymorphie vor dem Bruch (Rakete) gestoppt wird. Die Rakete taucht im dritten Block zweifach auf, da durch MyBase.Verbrennung() eine weitere Ausgabe erzeugt wird. Eine Alternative ist der Aufruf CType(Me, Triebwerk).Verbrennung(), der durch den Bruch der Polymorphie auch bei der Düse endet.
--Referenz auf Triebwerk-- Triebwerk als Triebwerk verbrennt Brennstoff mit Luft Düse als Düse verbrennt Kerosin mit Luft Rakete als Düse verbrennt Kerosin mit Luft Ariane als Düse verbrennt Kerosin mit Luft --Referenz auf Düse-- Düse als Düse verbrennt Kerosin mit Luft Rakete als Düse verbrennt Kerosin mit Luft Ariane als Düse verbrennt Kerosin mit Luft --Referenz auf Rakete-- Rakete als Düse verbrennt Kerosin mit Luft Rakete als Rakete verbrennt Brennstoff mit Oxidator Ariane als Ariane verbrennt Wasserstoff mit Sauerstoff
Ohne Overrides = Shadows
Wenn Sie beim Überschreiben vergessen, Overrides zu spezifizieren, ist dies gleichbedeutend mit Shadows. Dieses verdeckt alle Signaturen des Symbols in der Elternklasse, nicht nur die gerade neu definierte. Damit ist dann natürlich auch Schluss mit der Polymorphie. Das folgende Codefragment hat in der abgeleiteten Klasse Konditor keine Overrides-Spezifikation für die Methode Backen(). In der Methode Test() wird ein Konditor als Bäcker und Konditor angesprochen. Der auskommentierte Aufruf ist nicht erlaubt, da das implizite Shadows den Zugriff auf Bäcker.Backen() über eine Konditor-Referenz unterbindet (siehe Abschnitt 3.13.4, »Modifikation: Shadows und Overloads (Overrides)«).
'...\Klassendesign\Polymorphie\OverloadsUndOverrides.vb |
Option Strict On Namespace Klassendesign Class Bäcker Overridable Sub Backen() Console.WriteLine("Brot backen.") End Sub Overridable Sub Backen(ByVal klein As Boolean) If klein Then Console.WriteLine("Brötchen backen.") Else Backen() End Sub End Class Class Konditor : Inherits Bäcker Sub Backen() Console.WriteLine("Teilchen backen.") End Sub End Class Module OhneOverrides Sub Test() Dim k As Konditor = New Konditor() Dim b As Bäcker = k b.Backen() : k.Backen() b.Backen(False) ' k.Backen(False) existiert nicht! Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt, dass die Polymorphie unterbrochen ist.
Brot backen. Teilchen backen. Brot backen.
3.14.3 Unterbinden: NotInheritable, NotOverridable und MyClass 

So vielfältig die Möglichkeiten der Vererbung und der dynamischen Bindung auch sein mögen, manchmal möchten Sie diesen Variantenreichtum beschneiden. Zum Beispiel sollte eine Klasse zur Buchhaltung nicht veränderbar sein, damit die Revisionssicherheit erhalten bleibt. In anderen Fällen wollen Sie gut getestete Methoden vor Veränderungen schützen, ohne die Option zu versperren, der Klasse neue Methoden hinzuzufügen. Um diese Beschränkungen im Rahmen der Vererbung zu ermöglichen, kann die Polymorphie in drei Stufen unterbunden werden:
- NotInheritable: Die Klassenhierarchie wird beendet.
- NotOverridable: Ein einzelnes Klassenmitglied ist unveränderbar.
- MyClass: Nicht-polymorpher Zugriff in der aktuellen Klasse.
NotInheritable
Der heftigste Eingriff in die Vererbung ist das Verhindern jeglicher Kindklassen. Haben Sie beispielsweise folgende Vererbungshierarchie, ist es nicht sinnvoll, von der untersten Ebene weiter abzuleiten, da die Software für eine Hardware gedacht ist, die mit dem Druckermodell festliegt. Eine neue Druckerhardware braucht auch andere Steuerbefehle, und es wäre fehlerträchtig, auf dem anderen Druckermodell aufzubauen.
Drucker +-Laserdrucker +-Apple Laserwriter
Das nächste Codefragment zeigt eine andere Anwendung für dieses Konzept. Von einer Klasse Login mit einer Kennwortprüfung können durch NotInheritable keine Kindklassen erzeugt werden, da der Schutz sonst einfach durch eine Neudefinition der Prüfung in einer Kindklasse ausgehebelt werden könnte (zur Erinnerung: eine Kindklasse ist ein vollwertiger Stellvertreter). Da versucht wird, mit Superuser eine Kindklasse zu erzeugen, wird das Beispiel vom Compiler nicht fehlerfrei übersetzt.
'...\Klassendesign\Polymorphie\NotInheritable.vb |
Option Strict On Namespace Klassendesign NotInheritable Class Login Private kennwort As String Sub New(ByVal kennwort As String) Me.kennwort = kennwort End Sub Function Berechtigt(ByVal kennwort As String) As Boolean Return Me.kennwort = kennwort End Function End Class Class Superuser Inherits Login 'Compilerfehler!! Function Berechtigt(ByVal k As String) As Boolean Return True End Function End Class End Namespace
Hinweis |
Um den Code besser lesbar zu halten, sollte NotInheritable in jedem Teil einer mit Partial aufgeteilten Klasse angegeben werden. |
NotOverridable
Wollen Sie nur eine Variante eines polymorphen Klassenmitglieds gegen Änderungen in Kindklassen sperren, kennzeichnen Sie diese mit NotOverridable. Dabei ist die Sperrung nicht beliebig:
- Ausschließlich mit Overrides überschreibende Signaturen können gesperrt werden.
- Nur diese eine Signatur ist gesperrt.
- Ein gleichzeitiges Überdecken aller Signaturen desselben Symbols in einer Kindklasse mit Shadows ist erlaubt, die Sperrung ist also nicht absolut.
- Mit NotInheritable gesperrte Klassen dürfen NotOverridable nicht verwenden.
Die folgenden drei Codefragmente zeigen die Beschneidung der Vererbungsfähigkeit mit NotOverridable an einer Klassenhierarchie, die die Wanddicken verschiedener Gebäudetypen ermittelt. Da eine Signatur nur dann gesperrt werden kann, wenn sie mit Overrides gekennzeichnet ist, wird in einer Klasse Gebäude die Methode Wand() mit MustOverride als abstrakt markiert und in der Klasse Haus mit Overrides überschrieben (eine Verwendung von Overrides in Gebäude hätte das Beispiel länger gemacht). Diese Signatur wird in Haus gleichzeitig mit NotOverridable gegen Überschreiben geschützt. Die Methode ist mit einem anderen Parametertyp überladen, um weiter unten in Kindklassen die Konsequenzen von NotOverridable der anderen Signatur deutlich zu machen.
'...\Klassendesign\Polymorphie\NotOverridable.vb |
Option Strict On Namespace Klassendesign MustInherit Class Gebäude MustOverride Sub Wand(ByVal material As String) End Class Class Haus : Inherits Gebäude NotOverridable Overrides Sub Wand(ByVal material As String) Console.WriteLine("Wanddicke Haus " & If(material = "Beton", 20, 25)) End Sub Overridable Overloads Sub Wand(ByVal kostenrahmen As Double) Console.Write("Materialwahl Haus : ") Wand(If(kostenrahmen <= 15, "Beton", "Stein")) End Sub End Class ... End Namespace
Nun wird die Klasse Haus mit der NotOverridable-Signatur von zwei Klassen beerbt. Die Klasse Villa ändert die nicht gesperrte Methodensignatur und berücksichtigt, dass erst bei kleinerem Kostenrahmen mit Beton gearbeitet wird. Dies spiegelt die Realität wider, dass der Bauherr einer Villa mehr Wert auf das Ambiente legt, als er das bei einem »normalen« Haus tun würde, und einen Betonklotz vermeidet. Die Klasse berücksichtigt also die Besonderheiten des Gebäudetyps, ohne die Wanddicke ändern zu können, die aus Gründen der Statik gleich bleiben sollte. Der auskommentierte Versuch, die verschlossene Variante zu ändern, würde einen Compilerfehler erzeugen.
Die andere Klasse, Bude, entscheidet sich für einen Ersatz der Wanddickenberechnung, zum Beispiel aufgrund lockerer Baubestimmungen (»genehmigungsfreies Verfahren« …). Da eine Methodensignatur gesperrt ist, bleibt nichts anderes übrig, als alle Wand()-Signaturen komplett zu ersetzen und so die Polymorphie zu der Elternklasse zu kappen. Da durch den Modifikator NotInheritable keine Kindklassen von Bude möglich sind, überschatten die Methoden mit Overloads die Elternklassensignaturen nichtpolymorph. Sind Kindklassen erlaubt, ist Overridable die bessere Wahl.
'...\Klassendesign\Polymorphie\NotOverridable.vb |
Option Strict On Namespace Klassendesign ... Class Villa : Inherits Haus 'Overrides Sub Wand(ByVal material As String) 'Compilerfehler!! ' Console.WriteLine("Wanddicke Haus " & If(material = "Beton", 40, 50)) 'End Sub Overrides Sub Wand(ByVal kostenrahmen As Double) Console.Write("Materialwahl Villa: ") Wand(If(kostenrahmen <= 10, "Beton", "Stein")) End Sub End Class NotInheritable Class Bude : Inherits Haus Overloads Sub Wand(ByVal material As String) 'implizit überdeckend! Console.WriteLine("Wanddicke Bude " & If(material = "Beton", 15, 20)) End Sub Overloads Sub Wand(ByVal kostenrahmen As Double) 'implizit überdeckend! Console.Write("Materialwahl Bude : ") Wand(If(kostenrahmen <= 20, "Beton", "Stein")) End Sub End Class ... End Namespace
Im folgenden dritten Teil wird die kleine Vererbungshierarchie getestet. Um die polymorphen Aspekte zu erfassen, werden eine Villa und eine Bude auch unter einer Basisklassenreferenz vom Typ Haus gespeichert. Über alle vier Variablen wird zuerst die in Haus gesperrte Signatur von Wand() aufgerufen, danach die nicht gesperrte, die in Villa und Bude überladen ist.
'...\Klassendesign\Polymorphie\NotOverridable.vb |
Option Strict On Namespace Klassendesign ... Module StrukturErhalten Sub Test() Dim villa As Villa = New Villa() : Dim villa_h As Haus = villa Dim bude As Bude = New Bude() : Dim bude_h As Haus = bude Dim material As String = "Beton" Console.Write("Villa : ") : villa.Wand(material) Console.Write("Bude : ") : bude.Wand(material) Console.Write("Villa als Haus: ") : villa_h.Wand(material) Console.Write("Bude als Haus: ") : bude_h.Wand(material) Dim kostenrahmen As Double = 15 Console.Write("Villa : ") : villa.Wand(kostenrahmen) Console.Write("Bude : ") : bude.Wand(kostenrahmen) Console.Write("Villa als Haus: ") : villa_h.Wand(kostenrahmen) Console.Write("Bude als Haus: ") : bude_h.Wand(kostenrahmen) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass alle Basisklassenreferenzen (»als Haus« in der Ausgabe) und die Villa die Wanddicken aus der Klasse Haus nehmen, da die Villa durch NotOverridable und die Bude durch die implizite Abschattung mit Overloads von der dynamischen Bindung abgeschnitten sind. Die Klasse Villa überschreibt eine Signatur, sodass die Polymorphie für die Materialwahl erhalten bleibt, während durch die Abschattung bei der Basisklassenreferenz der Bude die Polymorphie in der Klasse Haus endet.
Villa : Wanddicke Haus 20 Bude : Wanddicke Bude 15 Villa als Haus: Wanddicke Haus 20 Bude als Haus: Wanddicke Haus 20 Villa : Materialwahl Villa: Wanddicke Haus 25 Bude : Materialwahl Bude : Wanddicke Bude 15 Villa als Haus: Materialwahl Villa: Wanddicke Haus 25 Bude als Haus: Materialwahl Haus : Wanddicke Haus 20
MyClass
Wenn die dynamische Bindung je nach Situation erwünscht oder unerwünscht ist, kann sie durch explizite Spezifikation von MyClass unterbunden werden. Alle nicht derart markierten Aufrufe und Eigenschaftszugriffe werden in ihrem polymorphen Verhalten nicht beeinflusst. Sie steuern also bei der Nutzung eines Klassenmitglieds, ob von der Polymorphie Gebrauch gemacht werden soll.
MyClass.Mitglied unterdrückt die Polymorphie. |
Das nächste Codefragment definiert eine zweistufige Vererbungshierarchie mit einer polymorphen Methode Info(). Durch die Angabe von MyClass in der Methode Allgemein() wird die dynamische Bindung unterdrückt. Der Aufruf der Methoden erfolgt innerhalb von Test().
'...\Klassendesign\Polymorphie\MyClass.vb |
Option Strict On Namespace Klassendesign Class Dozent Overridable Sub Info() Console.WriteLine("Dozent") End Sub Sub Allgemein() MyClass.Info() End Sub End Class Class Professor : Inherits Dozent Overrides Sub Info() Console.WriteLine("Professor") End Sub End Class Module OhnePolymorphie Sub Test() Dim d As Dozent = New Professor() d.Info() d.Allgemein() Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe dient zur Kontrolle der korrekten Polymorphie. Die zweite Ausgabe zeigt, dass die Polymorphie durch MyClass in der Methode Allgemein() unterbunden wird.
Professor Dozent
3.14.4 Konstruktoren 

In der Konstruktorlogik kann sich eine Stolperfalle durch die Polymorphie ergeben. Wird in einem Konstruktor einer Basisklasse eine mit Overridable dynamisch gebundene Methode oder Eigenschaft verwendet, kommt sie zum Zuge, bevor der Konstruktor der Kindklasse eine Chance hat, Objektzustände vollständig zu initialisieren. Dadurch stehen für diese nur typangepasste »Nullen« zur Verfügung. In Abschnitt 2.5.6, »Initialisierung von Variablen«, steht, was in diesem Zusammenhang unter einer »Null« zu verstehen ist. In den seltensten Fällen ist das gewünscht. Im folgenden Codefragment wird versucht, im Konstruktor der Basisklasse Himmelskörper mit der polymorphen Methode Dichte() die Dichte auszugeben. Wenn das Objekt als Planet vom Typ der Kindklasse ist, wird der Aufruf in der Kindklasse verwendet. Er greift auf die noch nicht initialisierte masse zu. Die Ausführungsreihenfolge ist durch die Zahlen am Zeilenende gekennzeichnet.
'...\Klassendesign\Polymorphie\Konstruktoren.vb |
Option Strict On Namespace Klassendesign Class Himmelskörper Protected volumen As Double Sub New(ByVal radius As Double) '4 Me.volumen = 4 / 3 * Math.PI * radius ^ 3 Dichte() '5 End Sub Overridable Sub Dichte() End Sub End Class Class Planet : Inherits Himmelskörper Private masse As Double Sub New(ByVal volumen As Double, ByVal masse As Double) '2 MyBase.New(volumen) '3 Me.masse = masse '7 End Sub Overrides Sub Dichte() '6 Console.WriteLine("Dichte: {0}", masse / volumen) End Sub End Class Module Konstruktoren Sub Test() Dim erde As Planet = New Planet(12735 * 1000 / 2, 5.974 * 10 ^ 24) '1 erde.Dichte() Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe stammt aus dem Aufruf von Dichte() im Konstruktor von Planet, nutzt den Standardwert 0 von masse und berechnet daraufhin eine Dichte von 0. Dagegen ist die Berechnung nach Abschluss der Objektinitialisierung korrekt, wie die zweite Ausgabe zeigt:
Dichte: 0 Dichte: 5524.20450952082
Dieses Beispiel zeigt deutlich:
Sie sollten keine virtuellen Methoden im Konstruktor aufrufen. |
3.14.5 Equals 

Oft müssen Objekte miteinander verglichen werden. Wenn es dabei nicht um Identität, sondern um Gleichwertigkeit geht, sollte der Vergleichsoperator = oder die Methode Equals() der Klasse Object verwendet werden. Damit der Vergleich mit Equals() auch für Basisklassenreferenzen »wunschgemäß« läuft, sollte die Methode mit Overrides polymorph überschrieben werden. Da Operatoren nicht polymorph sind, ist der Vergleich mit Equals() flexibler. Da der Typ, mit dem verglichen wird, immer Object ist, müssen oft Typumwandlungen innerhalb der Methode vorgenommen werden. Im folgenden Codefragment steht explizit Object als Elternklasse, um die Vererbungshierarchie zu unterstreichen. Für die Klasse Kekse wird Equals() so überschrieben, dass ein Kalorienvergleich mit Objekten vom Typ Kekse oder Torte möglich ist. In der Methode Test() werden mit Equals() Vergleiche in beide Richtungen durchgeführt.
'...\Klassendesign\Polymorphie\OverloadsUndOverrides.vb |
Option Strict On Namespace Klassendesign Class Kekse : Inherits Object Friend kal As Integer = 600 Public Overrides Function Equals(ByVal obj As Object) As Boolean If TypeOf obj Is Kekse Then Return CType(obj, Kekse).kal = kal If TypeOf obj Is Torte Then Return CType(obj, Torte).kal = kal Return False End Function End Class Class Torte : Inherits Object Friend kal As Integer = 600 End Class Module Gleichheit Sub Test() Dim bahlsen As Object = New Kekse Dim sacher As Object = New Torte() Console.WriteLine("Kalorien gleich: {0}", bahlsen.Equals(sacher)) Console.WriteLine("Kalorien gleich: {0}", sacher.Equals(bahlsen)) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass der Vergleich nur in einer Richtung funktioniert, da die Klasse Torte die Methode Equals() nicht überschrieben hat:
Kalorien gleich: True Kalorien gleich: False
Hinweis |
Equals() erlaubt Object als Parametertyp, im Gegensatz zum Operator =. |
Hinweis |
Mit Equals() sollte auch immer GetHashCode() überschrieben werden. |
3.14.6 ToString 

Nicht nur bei Konsolenanwendungen muss häufig ein Objekt am Bildschirm »dargestellt« werden. Die einfachste Möglichkeit hierzu ist eine Repräsentation als Zeichenkette, da die Klassenbibliotheken von .NET eine breite Unterstützung für die Darstellung von Texten bieten. In einer Konsole ist sie sogar die einzige. Auch für Kontrollausdrucke während der Testphase ist eine Zeichenkette sehr bequem. Wegen der großen Bedeutung der Zeichenkettendarstellung ist in der Mutter aller Klassen, Object, eine Methode namens ToString() enthalten, die ein Objekt als Zeichenkette darstellt. Weil diese Methode über Ihre Klassen nichts wissen kann, ist die Darstellung nicht besonders hilfreich. Die einzige Gemeinsamkeit aller Objekte ist die Tatsache, dass sie einen Typ besitzen. Dessen Name wird von ToString() zurückgegeben. Da nur der Autor einer Klasse eine sinnvolle Darstellung als Zeichenkette bewirken kann, sind Sie dafür verantwortlich, in der ToString()-Methode die »richtige« Funktionalität bereitzustellen.
Das folgende Codefragment fasst die Elemente eines beliebigen Vektors in geschweiften Klammern zusammen. Jedes Einzelelement wird mit ToString() in eine Zeichenkette umgewandelt, was automatisch auch geschachtelte Vektoren erfasst. Nullreferenzen werden gesondert behandelt. Nur zur Verdeutlichung wird Inherits Object angegeben. Die Verwendung von Overrides ist hier notwendig, da WriteLine() der Vektor als vom Typ Object übergeben wird. Würde dann Shadows oder Overloads für ToString() in Vektor verwendet, wäre die Polymorphie unterbrochen mit der Konsequenz, dass die Methode von Object aus nicht »sichtbar« wäre und die generische Methode Object.ToString() nur den Datentyp ausgeben würde.
'...\Klassendesign\Polymorphie\ToString.vb |
Option Strict On Namespace Klassendesign Class Vektor : Inherits Object Private werte() As Object Sub New(ByVal ParamArray werte() As Object) Me.werte = werte End Sub Overrides Function ToString() As String Dim text As String = "{" For no As Integer = 0 To werte.Length – 1 text += If(no > 0, ", ", "") text += If(werte(no) Is Nothing, "-", werte(no).ToString()) Next Return text + "}" End Function End Class Module Zeichenkettendarstellung Sub Test() Dim v As Vektor = New Vektor( _ 3.14, New Vektor(5, New System.Random()), Nothing, New Exception()) Console.WriteLine("Vektor {0}", v) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, wie der geschachtelte Vektor als zweites Klammerpaar erscheint. Die Klasse Random hat keine spezielle Definition für ToString() und nutzt die von Object, die den Datentyp ausgibt. Die Klasse Exception hat für ToString() eine angepasste Definition.
Vektor {3.14, {5, System.Random}, -, System.Exception: Exception of type 'System.Exception' was thrown.}
3.14.7 Grafikbibliothek: Objektsammlungen 

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.13.9, »Grafikbibliothek: neue Vielecke«, erweiterte Grafikanwendung. Zum Vieleck wird die Eckenzahl als Eigenschaft hinzugefügt. Die Ausnahme sorgt dafür, dass die Programmlogik für den Fall erhalten bleibt, dass eine Kindklasse von Vieleck das Überschreiben der Eigenschaft vergisst. Dies ist eine (schlechte) Alternative zu MustOverride, von der im .NET Compact Framework für mobile Geräte leider oft Gebrauch gemacht wird, um die äußere Form der Klassen kompatibel zum normalen .NET zu halten. In der Klasse Rechteck wird die Eigenschaft mit NotOverridable gekennzeichnet, da alle von Rechteck abgeleiteten Klassen immer vier Ecken haben müssen.
'...\Klassendesign\Graphik\Polymorphie.vb |
Option Explicit On Namespace Klassendesign Partial Public MustInherit Class Vieleck Overridable ReadOnly Property Eckenzahl() As Integer Get Throw New NotImplementedException("Kein konkretes Vieleck!") End Get End Property End Class Partial Public Class Rechteck : Inherits Vieleck NotOverridable Overrides ReadOnly Property Eckenzahl() As Integer Get Return 4 End Get End Property End Class ... End Namespace
Zur bequemeren Ausgabe wird noch ToString() überschrieben. Bis auf die Angabe des Rechtecktyps kann alles in der Klasse Rechteck ablaufen. Das Quadrat nimmt mit MyBase Rückgriff auf diese Implementation, die durch den If-Operator die Zeichenfolge »Rechteck« nicht zurückgibt. Schließlich ist die Klasse Quadrat noch mit NotInheritable gekennzeichnet, da speziellere Quadrate nicht möglich sein sollen.
'...\Klassendesign\Graphik\Polymorphie.vb |
Option Explicit On Namespace Klassendesign ... Partial Public Class Rechteck : Inherits Vieleck Public Overrides Function ToString() As String Return If(Me.GetType() Is GetType(Rechteck), "Rechteck ", "") & _ "{" & a & "," & b & "}" End Function End Class Partial Public NotInheritable Class Quadrat : Inherits Rechteck Public Overrides Function ToString() As String Return "Quadrat " & MyBase.ToString() End Function End Class End Namespace
Zum Test wird eine Liste von Quadraten erzeugt und in einer Schleife ausgegeben.
'...\Klassendesign\Zeichner\Polymorphie.vb |
Option Explicit On Namespace Klassendesign Partial Class Zeichner Sub Polymorphie() Dim rechtecke() As Rechteck = {New Quadrat(7), New Rechteck(6, 8)} For Each r As Rechteck In rechtecke Console.WriteLine(r.ToString() & " mit " & r.Eckenzahl & " Ecken") Next End Sub End Class End Namespace
Zur Kontrolle sehen Sie hier die Ausgabe der Methode Polymorphie():
Quadrat {7,7} mit 4 Ecken Rechteck {6,8} mit 4 Ecken
Die nächste Erweiterung erfolgt in Abschnitt 3.15.5, »Grafikbibliothek: Flächen«.