3.13 Vererbung
In diesem Abschnitt betreten wir endlich die Bühne der Vererbung. Sie werden schon sehr bald feststellen, dass sich damit eine neue Welt der Programmierung öffnet. Trotzdem bleibt die Anzahl der Konzepte überschaubar. Es reicht, die folgenden zehn Begriffe in Tabelle 3.22 neu hinzuzunehmen. Die Erklärung, was sie bedeuten, wird fast den ganzen Rest des Kapitels füllen.
Art | Schlüsselwörter |
Typbeziehung |
Implements und Inherits |
Redefinition |
Shadows, Overridable und Overrides |
Erzwingung |
MustInherit und MustOverride |
Abschluss |
NotInheritable und NotOverridable |
Zugriff |
MyBase |
Der Grund dafür, Vererbung in der Programmierung einzuführen, ist im Grunde der gleiche wie im realen Leben: Eine neue Generation muss nicht wieder von vorn anfangen, sondern kann auf dem aufbauen, was ihr die vorherige Generation hinterlassen hat. Von außen ist nicht feststellbar, ob ein Vermögen selbst erarbeitet oder geerbt wurde. Genauso ist es in der Programmierung. Für die Verwendung eines Objekts ist es irrelevant, ob ein Wert oder eine Funktionalität direkt von dem Objekt oder mittels Vererbung zur Verfügung gestellt wird. Dies schafft eine Entkopplung der Funktionalität von dem, wie sie konkret programmiert ist. Außerdem beschränkt die Vererbung in keiner Weise die Schaffung neuer Funktionalität.
Vererbung ist eine reine Erweiterung der bisherigen Konzepte. |
Bei der Vererbung ist zu beachten, dass nicht die Objekte selbst an der Vererbung teilnehmen, sondern die Vorlagen für Objekte, die Klassen. Wenn eine Klasse durch Vererbung mehr kann, hat natürlich auch das Objekt, das durch die Klasse mittels des New-Operators erzeugt wird, die größere Funktionalität. Es werden aber keine konkreten Daten weitergegeben. Was vererbt wird, ist die Tatsache, dass ein gewisser Satz an Daten im Objekt vorhanden ist. Genauso wenig wie ein Lehrer sein Wissen durch dessen Weitergabe verliert, »verbraucht« sich eine Klasse, wenn sie beerbt wird. Dies ermöglicht es vielen Klassen, eine einzelne Klasse zu beerben: Ein und dieselbe Klasse kann die Elternklasse vieler Klassen sein.
Wie im realen Leben wird durch die Vererbung auch eine Nachfolge geschaffen. Tritt jemand ein Universalerbe an, übernimmt er alle Rechte und Pflichten des Erblassers. Hatte dieser zum Beispiel Schulden, können sich die Gläubiger an den Erben wenden, als sei er der Schuldner selbst. Der Erbe ist dann ein Stellvertreter für den Beerbten. Genau dasselbe passiert auch in der Vererbung in der Softwaretechnik. Eine Kindklasse kann auch verwendet werden, als sei sie die Elternklasse. Das Konzept eines Teilerbes gibt es in Visual Basic nicht. Die Devise heißt »Entweder alles oder nichts« , weil nur dann das Konzept des Stellvertreters ausnahmslos funktioniert. Dieses Verhalten lässt sich wie folgt formulieren:
Vererbung etabliert eine Ist-eine–Beziehung. |
Damit einher geht automatisch die Tatsache, dass Vererbung transitiv ist. Zum Beispiel ist eine Ellipse ein geometrisches Objekt, und ein Kreis ist ein Spezialfall einer Ellipse. Über diese Beziehungskette ist dann, ohne explizite Spezifikation, ein Kreis ein geometrisches Objekt. Damit ist es ausreichend, den direkten Vorfahren zu kennen. Die in der Hierarchie weiter oben stehenden Ahnen sind damit implizit (automatisch) auch bekannt.
Vererbung ist transitiv. |
Die Komplettübernahme von den Eltern lässt sich abmildern. Ein Kind ist oft unzufrieden mit den Eltern und möchte dann nie so werden wie sie. Glücklicherweise kann jeder seine eigenen Verhaltensweisen entwickeln und so Übernommenes variieren oder sogar komplett ersetzen. Genauso geht das in Visual Basic. Eine Kindklasse kann die Form übernehmen und sie mit neuem Inhalt füllen. Dabei darf sie auf die Definition der Elternklasse zurückgreifen. Im realen Leben wird ein Kind ja auch selten eine Verhaltensweise der Eltern gänzlich verdammen, sondern es wird wahrscheinlicher den »guten Kern« beibehalten wollen.
Bis jetzt sind wir noch gar nicht auf Schwierigkeiten eingegangen, die sich dadurch ergeben können, dass es mehr als einen Elternteil gibt. Angenommen, Sie übernehmen von Ihrer Mutter und Ihrem Vater ein Rezept für den »gleichen« Kuchen. Wenn Sie nun gebeten werden »diesen« Kuchen zu backen, stellt sich das Problem, welches der Rezepte Sie nehmen. Die Angabe »dieser« Kuchen ist zweideutig.
Im Gegensatz dazu haben Sie, wie Ihre Eltern, einen Namen. Es wäre nun unsinnig, von jedem Elternteil den Namen zu übernehmen, da sonst in der dritten Generation bereits acht Namen nötig wären, und seit der Gründung der ersten Städte wäre die Liste so lang geworden, dass die Zahl der Atome im Universum nicht ausreicht, um sie zu speichern. In dieser Situation möchten Sie also die Eigenschaft »Name« nur einmal von Ihren Eltern übernehmen. Das Beziehungsgeflecht wird damit schnell derart unübersichtlich, dass die Vorteile einer Mehrfachvererbung in den Hintergrund treten. Demgegenüber führt die Beschränkung auf einen Elternteil auf eine einfache Struktur:
Einfachvererbung lässt sich als Baum darstellen, der eine einzige Wurzel hat. |
Was ist die Konsequenz? Eine Beschränkung ausschließlich auf Einfachvererbung wäre ein recht drastischer Schritt. Um ihn abzumildern, wählt Visual Basic einen Mittelweg zwischen Einfach- und Mehrfachvererbung. Was zusätzlich zur Einfachvererbung erlaubt ist, wird durch eine Analyse der Vererbung verständlich. Sie besteht aus zwei Parteien:
- dem Nutzer: Der Nutzer muss wissen, in welcher Form das Geerbte angesprochen wird.
- dem Erblasser: Der Erblasser definiert die Form.
Auch ohne konkrete Ausgestaltung der Funktionalität ist es bereits möglich, das Geerbte formell korrekt anzusprechen. In der Terminologie von Visual Basic bedeutet dies die Festlegung der Signatur, zum Beispiel einer Methode. Damit lässt sich bereits der gesamte Quelltext fertigstellen, der sie aufruft. Da nichts inhaltlich festgelegt wird, kann es nicht zu den Problemen kommen, die sich durch Mehrfachvererbung ergeben. Das geht so weit, dass dieselbe Form von verschiedenen Seiten gleichzeitig kommen kann, da sich an der Nutzung nichts ändert und inhaltlich ja noch nichts festgelegt worden ist. Erst beim Erben selbst wird konkretisiert, was formell durch die Eltern festgelegt wurde. Zum Beispiel kann sich ein Fahrzeug fortbewegen. Ein Auto und ein Boot sind Beispiele für Fahrzeuge. Ein Amphibienfahrzeug ist sowohl ein Auto als auch ein Boot. Würde nun bereits beim Auto oder Boot festgelegt, was Fortbewegung bedeutet, wäre ein solches Sonderfahrzeug nicht möglich. Legt man dagegen nur formell fest, dass sich ein Fahrzeug bewegen lässt, bleibt es jeweils dem Auto, dem Boot und dem Amphibienfahrzeug vorbehalten, die konkrete Fortbewegungsart selbst festzulegen. Datentypen, die nur eine Aussage über die Signaturen machen, werden in Visual Basic Schnittstellen genannt.
Schnittstellen etablieren eine Verhält-sich-wie-Beziehung. |
Damit können wir die Vererbung in Visual Basic grob wie folgt charakterisieren:
- Spezialisierung: Allgemeines wird durch Vererbung übernommen und dort spezialisiert, wo es nötig ist.
- Verallgemeinerung: Gemeinsame Funktionalität kann in einer Elternklasse zusammengefasst werden.
- Modularisierung: Vererbung erlaubt die Aufteilung eines großen Funktionsumfangs in kleine, handhabbare Einheiten, die mittels Vererbung aneinandergekoppelt werden.
- Wiederverwendung: Da eine Klasse mehrfach beerbt werden kann, kann sie als Baustein für viele neue Klassen dienen, ohne dass man neu programmieren muss.
- Stellvertreter: Eine Kindklasse übernimmt die Möglichkeiten einer Elternklasse komplett und kann dadurch genauso verwendet werden wie diese.
- Einfachvererbung: Jede Klasse, außer Object als Wurzel aller Klassen, hat genau eine Elternklasse.
- Mehrfache Schnittstellenvererbung: Jede Klasse kann beliebig viele Schnittstellen konkretisieren.
- Explizite Vererbung findet nur zwischen Klassen und Schnittstellen statt, alle Werttypen und Module sind ausgeschlossen (Werttypen erben implizit von ValueType).
Bei den ganzen Möglichkeiten der Vererbung sollten Sie jedoch darauf achten, diese nur dort einzusetzen, wo sie angebracht ist. Sie sollten genau prüfen, ob wirklich ein Objekt ein anderes komplett repräsentieren kann (Ist-ein-Beziehung) oder ob eines das andere enthält (Hat-ein-Beziehung). Zum Beispiel ist ein Auto ein Fahrzeug, und es hat einen Motor. Kann ein Objekt ein anderes nur teilweise ersetzen, sollte in der Regel keine Vererbung eingesetzt werden.
Ist eine Vererbungsbeziehung sinnvoll, sollte bei der Deklaration von Variablen immer ein Typ in der Vererbungshierarchie gewählt werden, der gerade ausreicht, um die von der Variablen angesprochenen Klassenmitglieder anzusprechen. Das hat den Vorteil, dass sich Änderungen in den Klassen, die weiter unten in der Vererbungshierarchie liegen, nicht auf den Code auswirken. Dies macht Sie freier in der zukünftigen Anpassung Ihrer Klassen an neue Bedingungen. Zusammengefasst:
Variablentypen sollten so allgemein wie möglich und so speziell wie nötig sein. |
3.13.1 Klassenbeziehung durch Inherits
Eine Klasse erbt in Visual Basic durch eine einfache Deklaration der zu beerbenden Klasse hinter dem Schlüsselwort Inherits (deutsch: erben). Es darf nur eine Klasse angegeben werden, und die Deklaration folgt direkt auf die einleitende Class-Deklaration. Durch diese Deklaration übernimmt die erbende Klasse alle Funktionalität der Basis.
Class Basis
...
End Class
Class Abgeleitet
Inherits Basis
...
End Class |
Hinweis |
Basis darf weder direkt noch indirekt ValueType, Enum, Array, MulticastDelegate oder Delegate sein, oder Attribute bei genrischen Klassen (alle im Namensraum System). |
Die einfachste Form der Vererbung ist das Beerben, ohne selbst etwas hinzuzufügen. Das folgende Beispiel zeigt, wie sich die Klasse Champagner auf die Klasse Schaumwein stützt.
'...\Klassendesign\Vererbung\Prinzip.vb |
Option Strict On Namespace Klassendesign Class Schaumwein Friend perlt As Boolean = True End Class Class Champagner Inherits Schaumwein End Class Module Prinzip Sub Test() Dim wein As Schaumwein = New Champagner() Console.WriteLine("Der Wein perlt {0}", If(wein.perlt, "", "nicht")) Console.WriteLine("Der Wein ist ein {0}", wein.GetType().Name) Console.ReadLine() End Sub End Module End Namespace
Damit ist eine Beziehung zwischen den Klassen hergestellt: Jeder Champagner ist ein Schaumwein (aber nicht umgekehrt, wovon Sie sich selbst durch eigene Geschmacksexperimente überzeugen können). Dadurch kann er in einer Variablen vom Typ der Basisklasse gespeichert werden. Durch das Erbe kann der Champagner eine Aussage darüber machen, ob er perlt, da er ja alle Funktionalität des Schaumweins übernimmt. Wie die zweite Zeile der folgenden Ausgabe außerdem zeigt, hat der Wein seine Herkunft nicht vergessen, auch wenn er in der Deklaration Dim wein As Schaumwein als allgemeiner Schaumwein abgelegt wird.
Der Wein perlt Der Wein ist ein Champagner
Damit werden zwei ganz wesentliche Punkte deutlich:
- Ein Objekt kann als Basistyp auftreten.
- Der Typ des Objekts bleibt auch in einer Referenz auf den Basistyp erhalten.
Das ist erst der Anfang der Geschichte. Der nächste Schritt besteht in der Ergänzung beziehungsweise Änderung des Geerbten. Dazu ist nichts Besonderes nötig. Bis auf die Inherits-Klausel ist die Kindklasse eine ganz »normale« Klasse. Das nächste Codefragment zeigt eine Klasse Expedition, die von der Klasse Reise die Mitglieder Ziel und Dauer erbt. Zusätzlich ersetzt sie die Funktion Dauer und definiert ein neues Feld Leitung.
'...\Klassendesign\Vererbung\Ergaenzung.vb |
Option Strict On Namespace Klassendesign Class Reise Friend Ziel As String = "Nordwestpassage" Friend Function Dauer() As String Return "ewig" End Function End Class Class Expedition Inherits Reise Friend Leitung As String = "Arved Fuchs" 'Ergänzung Friend Function Dauer() As String 'Ersatz Return "wenige Monate" End Function End Class Module Ergänzung Sub Test() Dim expedition As Expedition = New Expedition() Dim reise As Reise = expedition Console.WriteLine("Ziel der Reise: {0}", reise.Ziel) Console.WriteLine("Ziel der Expedition: {0}", expedition.Ziel) Console.WriteLine("Leitung der Expedition: {0}", expedition.Leitung) Console.WriteLine("Die Reise dauert {0}", reise.Dauer()) Console.WriteLine("Die Expedition dauert {0}", expedition.Dauer()) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, wie auf das geerbte Ziel und die neue Leitung zugegriffen wird. Die beiden letzten Zeilen machen deutlich, dass auf diejenige Definition zugegriffen wird, die dem Objekt am nächsten ist, das auf das Klassenmitglied zugreift.
Ziel der Reise: Nordwestpassage Ziel der Expedition: Nordwestpassage Leitung der Expedition: Arved Fuchs Die Reise dauert ewig Die Expedition dauert wenige Monate
Object
Alle Klassen basieren direkt oder indirekt auf der Klasse Object und erben von ihr einige Klassenmitglieder. Auf die öffentlichen Mitglieder haben alle Objekte Zugriff, und die mit Protected gekennzeichneten Mitglieder können in beliebigen eigenen Klassen verwendet oder neu definiert werden. Alle anderen sind nur mit Klassen aus dem Namensraum System.Reflection zugänglich. Eine Verwendung in eigenen Klassen sollte nie nötig sein. Sie sind nur der Vollständigkeit halber aufgelistet. Um die Auflistung kurz zu halten, wurde auf den Modifikator ByVal der Parameter verzichtet.
Public Function GetType() As Type Overridable Function GetHashCode() As Integer Overridable Function ToString() As String Overridable Function Equals(obj As Object) As Boolean Shared Function Equals(a As Object, b As Object) As Boolean Shared Function ReferenceEquals(a As Object, b As Object) As Boolean Protected Function MemberwiseClone() As Object Overridable Sub Finalize() Friend Shared Function InternalEquals(a As Object, b As Object) As Boolean Shared Function InternalGetHashCode(obj As Object) As Integer Private Sub FieldSetter(type As String, fieldName As String, val As Object) Sub FieldGetter(type As String, fieldName As String, ByRef val As Object) Function GetFieldInfo(type As String, fieldName As String) As FieldInfo
Partial
Liegt eine Klassendefinition mit Partial in mehreren Teilen vor, muss nicht jeder Teil die Inherits-Klausel spezifizieren. Das folgende Codefragment ist daher gültig, aber es ist nicht sofort klar, ob perlt direkt in der Klasse oder auch in einer Elternklasse zu suchen ist.
'...\Klassendesign\Vererbung\Partial.vb |
Option Strict On Namespace Klassendesign Partial Class Champagner Private eigenschaften() As Object = {Me.perlt, "teuer"} End Class End Namespace
Hinweis |
Um den Code besser lesbar zu halten, sollte eine Vererbungsbeziehung in jedem Teil einer partiellen Klasse angegeben werden (auch bei abstrakten und gesperrten Klassen sowie Schnittstellen – siehe Abschnitt 3.13.5, »Abstrakte Klassen: MustInherit und MustOverride«, Abschnitt 3.14.3, »Unterbinden: NotInheritable, NotOverridable und MyClass« sowie Abschnitt 3.15, »Schnittstellen: Interface und Implements«). |
Typprüfung mit TypOf … Is
Die Ist-eine-Beziehung kann im Quelltext mit dem Operator TypeOf ... Is geprüft werden. Das folgende Beispiel testet Objekte auf alle in Frage kommenden Datentypen einer zwei- bzw. dreistufigen Vererbungshierarchie (bzw. einer Stufe mehr durch Object).
'...\Klassendesign\Vererbung\TypeOf.vb |
Option Strict On Namespace Klassendesign Class Behausung : End Class Class Höhle : Inherits Behausung : End Class Class Haus : Inherits Behausung : End Class Class Hochhaus : Inherits Haus : End Class Module Typprüfung Sub Test() For Each b As Behausung In New Behausung() { _ New Haus(), New Höhle(), New Hochhaus(), New Behausung() _ } Console.Write("Typ {0}: ", b.GetType()) If TypeOf b Is Object Then Console.Write("Object ") If TypeOf b Is Behausung Then Console.Write("Behausung ") If TypeOf b Is Haus Then Console.Write("Haus ") If TypeOf b Is Hochhaus Then Console.Write("Hochhaus ") If TypeOf b Is Höhle Then Console.Write("Höhle ") Console.WriteLine() Next Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt deutlich, dass ein Datentyp außer für sich selbst auch noch für alle Klassen innerhalb seiner Vererbungshierarchie stehen kann:
Typ Vererbung.Klassendesign.Haus: Object Behausung Haus Typ Vererbung.Klassendesign.Höhle: Object Behausung Höhle Typ Vererbung.Klassendesign.Hochhaus: Object Behausung Haus Hochhaus Typ Vererbung.Klassendesign.Behausung: Object Behausung
Ein Teil der Hierarchie ist Object. Dies liegt an folgender Tatsache (im Beispiel trifft dies auf Behausung zu):
Ohne explizite Angabe erbt eine Klasse von Object – oder anders betrachtet: Jede Klasse bis auf Object hat immer genau eine Elternklasse. |
Implizite Typumwandlung (von Methodenparametern)
Um Arbeit zu sparen sollten Sie, soweit dies sinnvoll ist, Ihre Programmlogik mit möglichst allgemeinen Datentypen formulieren. Sollten Sie in Zukunft speziellere Kindklassen ableiten, können die einmal formulierten Programme unverändert auch mit den neuen Datentypen arbeiten, weil ja eine Kindklasse immer als Stellvertreter für eines ihrer Eltern verwendet werden kann. Kurz formuliert:
Datentypen sollten so allgemein wie möglich und so speziell wie nötig sein. |
Das folgende Codefragment zeigt einen Kalorienrechner für Gebäck. Da jede Art von Gebäck Kalorien hat, ist es möglich, alle Kekse und Torten im Punkt der Kalorien gleich zu behandeln. So muss nicht für jede noch zu erfindende kulinarische Süßigkeit ein neuer Kalorienrechner geschrieben werden, sondern die neue muss nur von Gebäck abgeleitet werden. Das war’s.
'...\Klassendesign\Vererbung\Implizit.vb |
Option Strict On Namespace Klassendesign Class Gebäck Friend Kalorien As Short End Class Class Keks : Inherits Gebäck : End Class Class Torte : Inherits Gebäck : End Class Class Schokotorte : Inherits Torte : End Class Module Implizit Function Kalorien(ByVal ParamArray stücke() As Gebäck) As Short For Each g As Gebäck In stücke : Kalorien += g.Kalorien : Next End Function Sub Test() Dim diätkeks As Keks = New Keks() : diätkeks.Kalorien = 30 Dim sacher As Schokotorte = New Schokotorte() : sacher.Kalorien = 800 Dim marzipan As Torte = New Torte() : marzipan.Kalorien = 700 Console.WriteLine("Der Nachtisch hat {0} Kalorien", _ Kalorien(sacher, marzipan, diätkeks, marzipan)) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt: Der Diätkeks zwischendurch hilft nicht wirklich beim Abnehmen:
Der Nachtisch hat 2230 Kalorien
Innere Klassen
Ein Datentyp, der in einem anderen Datentyp definiert ist, ist komplett eigenständig. Vom Effekt her ist es, als stünde er in einem Namensraum, der den Namen des äußeren Datentyps trägt. Dies gilt auch für die Vererbung. Das folgende Beispiel zeigt eine Klasse namens Bus, in der eine Klasse Passagiere definiert ist. Mit Inherits wird von diesem Bus ein Reisebus und von der inneren Klasse Passagiere die Klasse Reisende abgeleitet.
'...\Klassendesign\Vererbung\InnereKlassen.vb |
Option Strict On Namespace Klassendesign Class Bus Class Passagiere Friend Anzahl As Short = 120 End Class End Class Class Reisebus Inherits Bus End Class Class Reisende Inherits Reisebus.Passagiere Friend Anzahl As Short = 80 End Class ... End Namespace
Der Test prüft durch den Zugriff auf das Feld Anzahl die durch Vererbung weitergegebene Funktionalität.
'...\Klassendesign\Vererbung\InnereKlassen.vb |
Option Strict On Namespace Klassendesign ... Module InnereKlassen Sub Test() Dim p As Reisebus.Passagiere = New Reisebus.Passagiere() Console.WriteLine("Bus hat {0} Passagiere", p.Anzahl) Dim r As Reisende = New Reisende() Console.WriteLine("Bus hat {0} Passagiere", r.Anzahl) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekten Vererbungsverhältnisse.
Bus hat 120 Passagiere Bus hat 80 Passagiere
3.13.2 Sichtbarkeitsmodifikatoren
Was ich nicht weiß, macht mich nicht heiß.
Auch bei Nutzung der Vererbung kann ich nur dringend dazu raten, alles, was nicht unbedingt von außen gebraucht wird, mittels Sichtbarkeitsmodifikatoren so weit es geht zu »verstecken«. Denn wovon die Nutzer Ihrer Klassen nichts wissen, das können Sie jederzeit ändern oder korrigieren, ohne deren Programme zu beeinflussen. Davon profitieren Sie auch, denn sehr wahrscheinlich sind Sie selbst der erste Nutzer der eigenen Klassen. Die meisten Sichtbarkeitsmodifikatoren haben Sie schon in Abschnitt 3.2, »Kapselung«, kennengelernt (siehe unter anderem Tabelle 3.2, »Sichtbarkeitsmodifikatoren eines Klassenmitglieds«). In diesem Abschnitt beschäftigen wir uns mit dem noch nicht besprochenen Sichtbarkeitsmodifikator Protected und den Effekten der Vererbung auf die Sichtbarkeit. Erst im Rahmen der Polymorphie (siehe Abschnitt 3.14, »Polymorphie«) kommt es zum Zwang, eine einmal festgelegte Sichtbarkeit beizubehalten.
Ohne Polymorphie kann die Sichtbarkeit in einer Kindklasse vollständig unabhängig von der Sichtbarkeit in der Elternklasse gewählt werden. Der Einzelfall entscheidet darüber, inwieweit davon Gebrauch gemacht werden sollte. |
Protected
Soll ein Klassenmitglied nur in Kindklassen sichtbar sein, wird es mit dem Modifikator Protected gekennzeichnet. Wenn es außerdem in derselben Anwendung sichtbar sein soll, spezifizieren Sie die Kombination Protected Friend. Gebraucht wird diese Sichtbarkeitsbeschränkung zum Beispiel, wenn ein Kunde in einem Webformular einen Preis für ein Produkt mit maßgeschneiderten Eigenschaften ermitteln kann und der Preis vom Produktionsprozess abhängt, der nicht offengelegt werden soll. Das nächste Codefragment zeigt die alltägliche Situation, dass der Einkaufspreis dem Endpreis nicht entnommen werden kann. Dies wird im Programm dadurch realisiert, dass man die Sichtbarkeit des Einkaufspreises mittels Protected auf die Kindklasse beschränkt.
'...\Klassendesign\Vererbung\Protected.vb |
Option Strict On
Namespace Klassendesign
Class Schaumstoffblock
Protected Einkaufspreis As Double = 100
End Class
Class Matratze
Inherits Schaumstoffblock
Private Marge As Double = 0.3
Sub Preis()
Console.WriteLine("Preis {0}", Einkaufspreis * (1 + Marge))
End Sub
End Class
Module Sichtbarkeit
Sub Test()
Dim matratze As Matratze = New Matratze()
matratze.Preis()
Console.ReadLine()
End Sub
End Module
End Namespace
Die Ausgabe bestätigt den korrekten Zugriff auf das mit Protected gekennzeichnete Klassenmitglied:
Preis 130
Hinweis |
Datentypen ohne Vererbung (zum Beispiel Module) dürfen Protected nicht spezifizieren (Ausnahme: zur Beerbung von Object). |
Zugriff
Jede Klasse kann mit einem der Sichtbarkeitsmodifikatoren Public und Friend gekennzeichnet werden (kein Modifikator ist gleichbedeutend mit Friend). Dabei ist zu beachten, dass eine Kindklasse nie sichtbarer sein darf als ihre Elternklasse. Dieses Prinzip gilt neben Klassen auch für andere Datentypen, zum Beispiel für Schnittstellen. Daher wird das folgende Codefragment vom Compiler zurückgewiesen:
Friend Class Anwendung : End Class Public Class Öffentlich : Inherits Anwendung : End Class 'Compilerfehler!!
Ein Kindtyp ist nie sichtbarer als die Eltern. |
Lecks
Wie im realen Leben bleibt ein Geheimnis nur dann gewahrt, wenn es keine undichte Stelle gibt. Wie in Abschnitt 3.2.1, »Kombinationen«, beschrieben wurde, kommt uns Visual Basic ein Stück entgegen, indem die restriktivste Transparenz die ganze Sichtbarkeit dominiert. Es reicht also, in der gesamten Kette von Objekten und Typen, die für den Zugriff auf ein Klassenmitglied nötig sind, eines zu haben, das restriktiv genug ist. Leider ist damit noch nicht alles getan, denn es gibt zwei Konzepte in Visual Basic, die an anderer Stelle nützlich sind, hier aber die Bemühungen um möglichst kleine Sichtbarkeit torpedieren:
- Kindklassen fügen immer nur Definitionen hinzu, aber löschen nie welche.
- Die Sichtbarkeit wird erst bei der Verwendung berücksichtigt, nicht bereits bei der Definition.
Wenn also in einer Klasse ein Klassenmitglied öffentlich gemacht wird, ist dies nicht mehr rückgängig zu machen. Wenn eine Kindklasse diese Definition durch eine weniger sichtbare ersetzt, wirkt sich das nur auf die Kindklasse und deren Kindklassen aus. Von außen erfolgt der Zugriff auf das veröffentlichte Mitglied, und die restriktiveren Modifikationen bleiben unsichtbar. Die Tabelle 3.23 zeigt, auf welche Klasse in Abhängigkeit von den Sichtbarkeitsmodifikatoren zugegriffen wird.
Eltern | Kind | Enkel sieht | Projekt sieht | Andere sehen |
Public |
Private |
Eltern |
Eltern |
Eltern |
Protected |
Kind |
Eltern |
Eltern |
|
Friend |
Eltern |
Kind |
Eltern |
|
Protected Friend |
Kind |
Kind |
Eltern |
|
Public |
Kind |
Kind |
Kind |
|
Protected Friend |
Private |
Eltern |
Eltern |
– |
Protected |
Kind |
Eltern |
– |
|
Friend |
Eltern |
Kind |
– |
|
Protected Friend |
Kind |
Kind |
– |
|
Public |
Kind |
Kind |
Kind |
|
Friend |
Private |
– |
Eltern |
– |
Protected |
Kind |
Eltern |
– |
|
Friend |
– |
Kind |
– |
|
Protected Friend |
Kind |
Kind |
– |
|
Public |
Kind |
Kind |
Kind |
|
Protected |
Private |
Eltern |
– |
– |
Protected |
Kind |
– |
– |
|
Friend |
Eltern |
Kind |
– |
|
Protected Friend |
Kind |
Kind |
– |
|
Public |
Kind |
Kind |
Kind |
|
Private |
Private |
– |
– |
– |
Protected |
Kind |
– |
– |
|
Friend |
– |
Kind |
– |
|
Protected Friend |
Kind |
Kind |
– |
|
Public |
Kind |
Kind |
Kind |
Das folgende Codefragment zeigt eine dreistufige Vererbungshierarchie. In der untersten Stufe, Person, wird die Methode Name() öffentlich zugänglich gemacht. In der zweiten Stufe legt sich der Politiker ein Pseudonym zu, auf das in der dritten Stufe über die Methode Ansprache() zugegriffen wird. In der Methode Test() wird der Name einmal über die gleichnamige Methode sowie über die Methode Ansprache() ausgegeben.
'...\Klassendesign\Vererbung\Lecks.vb |
Option Strict On Namespace Klassendesign Class Person Public Sub Name() Console.WriteLine("Herbert Frahm") 'bürgerlicher Name End Sub End Class Class Politiker Inherits Person Protected Sub Name() Console.WriteLine("Willy Brandt") 'bekanntes Pseudonym End Sub End Class Class Spitzenpolitiker Inherits Politiker Public Sub Ansprache() Name() End Sub End Class Module Lecks Sub Test() Dim kanzler As Spitzenpolitiker = New Spitzenpolitiker() kanzler.Name() kanzler.Ansprache() Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe zeigt, dass der einmal mittels Public veröffentlichte bürgerliche Name auch dann angesprochen werden kann, wenn er in der Vererbungshierarchie mit einem gleichnamigen Klassenmitglied überschrieben wurde, dessen Sichtbarkeit mittels Protected auf Kindklassen beschränkt wurde. Dieses Mitglied ist von außen schlichtweg nicht sichtbar, und das nächste sichtbare Element gleichen Namens in der Vererbungshierarchie wird verwendet, hier Name() in der Klasse Person. Die zweite Ausgabe zeigt, dass Name() in der Klasse Politiker über dessen Kindklasse Spitzenpolitiker in der Methode Ansprache() erreichbar ist.
Herbert Frahm Willy Brandt
Daraus können wir folgern:
Sichtbarkeitsmodifikatoren bestimmen, wer zusätzlich etwas zu sehen bekommt (war vorher nichts definiert, ist das natürlich gleichzeitig eine absolute Spezifikation). |
3.13.3 Zugriff auf Eltern mit MyBase
Oft steckt viel Arbeit in einer Klassenmethode. Wenn dann die Klasse, die diese Methode enthält, in einer Kindklasse spezialisiert wird, kommt es häufig vor, dass die Funktionalität der Methode ein wenig erweitert werden soll, ohne ihren Namen zu ändern. Wir haben also folgende Situation:
Class Basis
Sub Methode()
...
End Sub
End Class
Class Kind
Inherits Basis
Sub Methode()
...
Methode() 'Kind.Methode(), nicht Basis.Methode() => infinite Rekursion
End Sub
End Class
Das funktioniert so aber nicht! Denn der Aufruf Methode() in Kind würde sich selbst aufrufen und nicht die gleichnamige Methode in Basis. Der Aufruf muss daher mit folgender Syntax qualifiziert werden (das Mitglied müssen Sie Ihren Bedürfnissen anpassen):
MyBase.<Klassenmitglied> |
Hinweis |
MyBase greift auf das in der Vererbungshierarchie nächste sichtbare Klassenmitglied zu, ein Überspringen mittels MyBase.MyBase ist nicht erlaubt. |
Das folgende Codefragment zeigt, wie in der Methode Ansprache() der Klasse Professor mittels MyBase die Methode in der Elternklasse Forscher aufgerufen wird.
'...\Klassendesign\Vererbung\MyBase.vb |
Option Strict On Namespace Klassendesign Class Forscher Friend Name As String Sub Ansprache() Console.Write(Name) End Sub End Class Class Professor Inherits Forscher Sub Ansprache() Console.Write("Professor ") MyBase.Ansprache() End Sub End Class Module Basiszugriff Sub Test() Dim prof As Professor = New Professor() prof.Name = "Habakuk Tibatong" prof.Ansprache() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die Methoden beider Klassen zum Zuge kommen:
Professor Habakuk Tibatong
Konstruktorverkettung
Wenn ein Objekt einer Klasse mittels New erzeugt wird, müssen die Initialisierungen aller Ebenen der Vererbungshierarchie korrekt ablaufen. Da eine Kindklasse auf einer Elternklasse basiert, muss zuerst die Initialisierung der Elternklasse durchgeführt werden und dann erst die der Kindklasse. Ansonsten könnte sich die Kindklasse nicht sauber auf die Elternklasse stützen. Doch warum haben wir uns bisher darüber keine Gedanken gemacht? Ganz einfach: Wenn der Quelltext nichts explizit vorgibt, fügt der Compiler in jeden Konstruktor automatisch Code ein, der den parameterlosen Standardkonstruktor der Elternklasse aufruft, egal ob dieser explizit im Quelltext steht oder automatisch zur Verfügung gestellt wird, weil die Klasse keinen Konstruktor im Quelltext definiert (siehe Abschnitt 3.5, »Objektinitialisierung mit Konstruktoren«). Das folgende Codefragment zeigt die Idee:
Class Basis
End Class
Class Kind
Inherits Basis
Sub New()
MyBase.New() 'automatisch eingefügt
...
End Sub
End Class
Jeder Konstruktor, außer in Object, ruft in der ersten Anweisung (und nur dort) einen Konstruktor der eigenen Klasse oder der Basisklasse auf. |
Dieser Mechanismus ist nicht erwünscht, wenn ein Konstruktor der Basisklasse verwendet werden soll, der Parameter erwartet. Noch kritischer ist die Situation, in der es gar keinen parameterlosen Konstruktor gibt, weil alle benutzerdefinierten Konstruktoren Parameter haben. Dann ist der automatisch eingefügte Aufruf nicht möglich. Der Compiler meldet einen Fehler, und Sie müssen einen Aufruf eines Basisklassenkonstruktors selbst einfügen. Das folgende Beispiel zeigt, wie ein durch die Basisklasse Fenster im Konstruktor erzwungener Fenstertitel von der Kindklasse Startfenster weitergegeben wird:
'...\Klassendesign\Vererbung\Konstruktorverkettung.vb |
Option Strict On Namespace Klassendesign Class Fenster Private titel As String Sub New(ByVal titel As String) Me.titel = titel End Sub Sub Fenstertitel() Console.WriteLine(titel) End Sub End Class Class Startfenster Inherits Fenster Sub New(ByVal titel As String) MyBase.New(titel) End Sub End Class Module Konstruktorverkettung Sub Test() Dim fenster As Startfenster = New Startfenster("Dokument 1") fenster.Fenstertitel() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt den korrekten Zugriff:
Dokument 1
Damit die Initialisierungsreihenfolge – erst Eltern und dann Kinder – gewahrt bleibt, muss der Aufruf die erste Anweisung im Konstruktor sein. Das Erfordernis, dass eine Konstruktorverkettung die erste Anweisung sein muss, kann nicht umgangen werden. Zum Beispiel ist es nicht möglich, das zu erstellende Objekt (implizit) in den aktuellen Parametern von New zu verwenden. Der folgende Code würde vom Compiler zurückgewiesen:
Function Seiteneffekt() As Short
Return 5
End Function
Sub New()
MyBase.New(Seiteneffekt()) 'Fehler!!
End Sub
Hinweis |
Klassenkonstruktoren (Shared Sub New()) können nicht verkettet werden, da die Laufzeitumgebung die Ladereihenfolge von Datentypen selbst bestimmt. |
Protected-Konstruktoren
Bei der Zusammenfassung gemeinsamer Funktionalität in einer Elternklasse kann diese einen derart hohen Abstraktionsgrad erreichen, dass es nicht sinnvoll ist, von dieser Klasse Objekte zu erzeugen. Die Objekterzeugung kann dann einfach durch einen nichtöffentlichen Objektkonstruktor unterbunden werden. Soll außerdem die Entwicklung von Kindklassen ungehindert möglich sein, wählen Sie Protected als Spezifikation für die Sichtbarkeit. Das folgende Beispiel zeigt eine Basisklasse Ware mit einem Konstruktor, der die Bezeichnung der Ware festlegt. Da diese Klasse so allgemein ist, dass sie selbst Geschenke und zu verkaufende Sachen umfassen soll, ist der Konstruktor durch Protected nicht öffentlich. Die Kindklassen Präsent und Verkaufsartikel haben außer der Warenbezeichnung nichts gemeinsam. Durch die gemeinsame Basisklasse ist aber gewährleistet, dass es keine Ware ohne Bezeichnung gibt.
'...\Klassendesign\Vererbung\ProtectedKonstruktor.vb |
Option Strict On Namespace Klassendesign Class Ware Protected Name As String Protected Sub New(ByVal name As String) Me.Name = name End Sub End Class Class Präsent Inherits Ware Sub New(ByVal name As String) MyBase.New(name) End Sub Sub Angebot() Console.WriteLine("{0} als Präsent", Name) End Sub End Class Class Verkaufsartikel Inherits Ware Private Preis As Double Sub New(ByVal name As String, ByVal preis As Double) MyBase.New(name) : Me.Preis = preis End Sub Sub Kosten() Console.WriteLine("{0} kostet {1}", Name, Preis) End Sub End Class ... End Namespace
Zum Testen werden zwei spezifische Waren erzeugt und wird deren spezifische Methode aufgerufen:
'...\Klassendesign\Vererbung\ProtectedKonstruktor.vb |
Option Strict On Namespace Klassendesign ... Module ProtectedKonstruktor Sub Test() Dim stift As Präsent = New Präsent("Kugelschreiber") Dim drucker As Verkaufsartikel = New Verkaufsartikel("Drucker", 350) stift.Angebot() drucker.Kosten() Console.ReadLine() End Sub End Module End Namespace
Die Beschreibungen der Waren sind recht unterschiedlich, greifen aber beide auf die Bezeichnung der Ware zurück:
Kugelschreiber als Präsent Drucker kostet 350
Ereignisse
Eine Kindklasse kann behandelnde Methoden für Ereignisse einer Elternklasse registrieren, auch wenn die Methoden mit Shared klassengebunden sind. Die Kindklasse kann das Ereignis der Elternklasse aber nicht auslösen. Das folgende Codefragment wird daher vom Compiler zurückgewiesen:
Class Ausbilder Event Zählen() End Class Class Lehrer Inherits Ausbilder Sub Appell() RaiseEvent Zählen() 'Fehler!! End Sub End Class
3.13.4 Modifikation: Shadows und Overloads (Overrides)
Wenn eine Kindklasse das von einer Basisklasse Geerbte modifizieren will, hat sie dazu drei Möglichkeiten. In diesem Abschnitt werden die beiden ersten behandelt, die dritte wird in Abschnitt 3.14, »Polymorphie«, folgen. Die Modifikatoren schließen sich gegenseitig aus.
- Neudefinition mit Shadows: Alle Definitionen der Basisklasse für ein Symbol werden aufgegeben (Verdeckung des Namens).
- Änderung mit Overloads: Zusätzliche Definitionen erweitern die Definitionen der Basisklasse für ein Symbol, oder einzelne Definitionen der Basisklasse für ein Symbol werden ersetzt (Verdeckung der Signatur).
- Ersatz mit Overrides: Eine einzelne Definition für ein Symbol wird effektiv »in« der Basisklasse selbst ersetzt.
Hinweis |
Shadows und Overloads dürfen selbst dann verwendet werden, wenn die Basisklasse keine korrespondierenden Mitglieder hat. Ohne Angaben zur Modifikation wird Shadows angenommen. |
Hinweis |
Alle Signaturen eines Symbols müssen entweder mit Shadows (explizit oder implizit) oder mit Overloads bzw. Overrides gekennzeichnet werden. Eine Mischung der beiden »Gruppen« ist nicht erlaubt. |
Bisher haben wir keine der drei Modifikatoren angegeben, und der Compiler hat implizit eine Spezifikation von Shadows angenommen. Das ist bisher nicht aufgefallen, weil in der Basisklasse für ein Symbol nur eine einzige Definition vorlag.
Das nächste Beispiel zeigt die Unterschiede der Modifikatoren. Dazu wird bei einem allgemeinen Flugzeug bei der Begrüßung der Passagiere zwischen einer mittels des Datentyps Byte auf 255 Plätze beschränkten Maschine und einer allgemeinen Maschine unterschieden, die den Datentyp Short verwendet. Ein Linienflugzeug leitet sich vom allgemeinen Flugzeug mittels Inherits ab und stellt nur eine Art der Begrüßung zur Verfügung, indem es mittels Shadows die Definitionen in Flugzeug verwirft. Demgegenüber wird beim Charterflugzeug mittels Overloads nur die Definition für eine Methodensignatur ersetzt.
'...\Klassendesign\Vererbung\Modifikation.vb |
Option Strict On Namespace Klassendesign Class Flugzeug Sub Einchecken(ByVal anzahl As Byte) Console.Write("{0} per Handschlag ", anzahl) End Sub Sub Einchecken(ByVal anzahl As Short) Console.Write("{0} per Gruß ", anzahl) End Sub End Class Class Linienflugzeug Inherits Flugzeug Shadows Sub Einchecken(ByVal anzahl As Short) Console.Write("{0} Begrüßungen ", anzahl) End Sub End Class Class Charterflugzeug Inherits Flugzeug Overloads Sub Einchecken(ByVal anzahl As Short) Console.Write("{0} per Ansprache ", anzahl) End Sub End Class ... End Namespace
Der Test der Flugzeuge versucht, die verschiedenen Methodensignaturen anzusprechen.
'...\Klassendesign\Vererbung\Modifikation.vb |
Option Strict On Namespace Klassendesign ... Module Modifikation Sub Test() Dim linie As Linienflugzeug = New Linienflugzeug() Dim charter As Charterflugzeug = New Charterflugzeug() linie.Einchecken(4) : linie.Einchecken(300) : Console.WriteLine() charter.Einchecken(4) : charter.Einchecken(300) : Console.WriteLine() Dim allg As Flugzeug = linie allg.Einchecken(4) : allg.Einchecken(300) : Console.WriteLine() Console.ReadLine() End Sub End Module End Namespace
Die erste Zeile der Ausgabe zeigt, dass die Neudefinition mit Shadows die Signatur mit Byte komplett verdrängt hat. Dagegen ist in der zweiten Zeile zu sehen, dass mit Overloads nur die eine Signatur ersetzt wurde. Schließlich führt die Verwendung einer Referenz vom Typ der Basisklasse dazu, dass auch die Definitionen verwendet werden, die in der Basisklasse bekannt sind. Wie Letzteres vermieden werden kann, wird in Abschnitt 3.14, »Polymorphie«, erklärt.
4 Begrüßungen 300 Begrüßungen 4 per Handschlag 300 per Ansprache 4 per Handschlag 300 per Gruß
Klassenbindung mit Shared
Die Modifikation eines Klassenmitglieds ist nicht auf objektgebundene Mitglieder beschränkt. Das folgende Beispiel zeigt den Ausdruck eines Feldes von Zahlen durch Methoden, die mittels Shared an die Klasse gebunden sind. Werden diese in der Klasse ReelleZahl als reelle Zahl interpretiert, wird nur der erste Wert ausgegeben. Ist es hingegen eine komplexe Zahl wie in der Klasse KomplexeZahl, werden die ersten beiden Werte als Summe aus beiden ausgedruckt. Sollten Sie nicht mit komplexen Zahlen vertraut sein: Hier kommt es nur auf das Konzept an, wie die Methoden vererbt werden, und nicht darauf, was der Begriff »komplexe Zahl« bedeutet.
'...\Klassendesign\Vererbung\Shared.vb |
Option Strict On Namespace Klassendesign Class ReelleZahl Shared Sub Druck(ByVal v() As Double) Console.Write("Wert {0} ", v(0)) End Sub End Class Class KomplexeZahl Inherits ReelleZahl Overloads Shared Sub Druck(ByVal v() As Double, ByVal reell As Boolean) If reell Then Druck(v) Else _ Console.Write("Wert {0}+I*{1} ", v(0), v(1)) End Sub End Class Module Klassengebunden Sub Test() Dim zahl() As Double = New Double() {8.1, 5.2} KomplexeZahl.Druck(zahl, True) : KomplexeZahl.Druck(zahl, False) KomplexeZahl.Druck(zahl) : ReelleZahl.Druck(zahl) Console.ReadLine() End Sub End Module End Namespace
Bis auf die erste Ausgabe werden die Werte durch die Funktion der Basisklasse erzeugt. Beim Aufruf über die Kindklasse erfolgt eine Weiterleitung von Druck(v) an die Elternklasse.
Wert 8.1+I*5.2 Wert 8.1 Wert 8.1 Wert 8.1
Komplettersatz mit Shadows
Die Kennzeichnung mit Shadows verdeckt ein Symbol in einer Basisklasse vollständig, inklusive aller mit ihm verbundenen Signaturen. Das geht so weit, dass selbst die Art der Definition geändert werden kann. So kann zum Beispiel aus einer Prozedur ein Feld werden, wie das nächste Codefragment zeigt. Wenn die Referenzvariable vom Typ Ministerpräsident ist, wird in dem Beispiel das Feld angesprochen. Ist sie vom Typ Freund, kommt die Prozedur zum Zuge. Damit wird bereits durch die Klassendefinition sichergestellt, dass der Amtsträger Rau »falsch« angesprochen wird. Jeder Quelltext, der diese Klassen verwendet, kann also diesbezüglich keine »Fehler« machen. Dieses Konzept, Fehler möglichst unmöglich zu machen, schafft insgesamt viel robustere Programme.
'...\Klassendesign\Vererbung\Shadows.vb |
Option Strict On Namespace Klassendesign Class Freund Friend Sub Name(ByVal wer As String) Select Case wer Case "Freund" : Console.WriteLine("Hallo Bruder Johannes") Case Else : Console.WriteLine("Herr Johannes Rau") End Select End Function End Class Class Ministerpräsident Inherits Freund Friend Shadows Name As String = "Johannes Rau" End Class Module Neudefinition Sub Test() Dim präsident As Ministerpräsident = New Ministerpräsident() Dim freund As Freund = präsident Console.WriteLine("Herr {0}", präsident.Name) freund.Name("Freund") Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe gibt das Feld Name der Klasse Ministerpräsident aus, die zweite wird durch die Prozedur Name() der Klasse Freund erzeugt:
Herr Johannes Rau Hallo Bruder Johannes
Hinweis |
Eine Kindklasse hat keine Möglichkeit, einmal gemachte Definitionen in der Elternklasse zu löschen, aber mit Shadows können sie komplett ersetzt werden. Alle von der Kindklasse abgeleiteten Klassen haben dann keinen Zugriff mehr auf die mit Shadows verdeckten Symbole der Elternklasse, wenn die neue Definition für sie sichtbar ist. |
Private und Shadows
Obwohl immer alles komplett vererbt wird, gilt dies nicht für die Sichtbarkeit des Geerbten:
Private-Klassenmitglieder stehen in einer Kindklasse nicht zur Verfügung. |
Das macht sich in einer mehrstufigen Vererbungshierarchie bemerkbar. Wenn ein Symbol mittels Shadows komplett überdeckt wird, hat dies nur dann einen Einfluss auf Kindklassen, wenn das Klassenmitglied nicht privat ist, da alle privaten Mitglieder nur in der Klasse selbst sichtbar sind. Dies führt im folgenden Beispiel dazu, dass der Aufruf Aktion() in der Klasse Aktionen die Methode in der Klasse Button anspricht und nicht die private Methode in Menüeintrag.
'...\Klassendesign\Vererbung\PrivateShadows.vb |
Option Strict On Namespace Klassendesign Class Button Sub Aktion() Console.WriteLine("Aktion") End Sub End Class Class Menüeintrag : Inherits Button Private Shadows Sub Aktion() Console.WriteLine("Menü hervorheben") End Sub End Class Class Aktionen : Inherits Menüeintrag Sub Speichern() Aktion() End Sub End Class Module PrivatVerdeckt Sub Test() Dim akt As Aktionen = New Aktionen() akt.Speichern() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt das Überspringen der privaten Methode.
Aktion
Overloads und ParamArray
Eine Methode mit einem ParamArray-Parameter akzeptiert eine variable Anzahl an aktuellen Parametern. Wenn eine solche Methode mittels Overloads eine Methodensignatur in einer Elternklasse mit einer fixen Anzahl Parameter gleichen Typs ergänzt, überdeckt sie diese nicht, sondern stellt eine zusätzliche Signatur dar, die nur dann aufgerufen wird, wenn die Methode der Elternklasse nicht passt. Kurz formuliert:
ParamArray überschreibt ParamArray, nicht alle repräsentierten Signaturen. |
Das folgende Codefragment überlädt in der Kindklasse MotorradMitBeiwagen die Methode Mitfahrer() der Elternklasse Motorrad. Beide Methoden akzeptieren im Prinzip den Aufruf mit einem einzelnen String-Parameter. Dieser Aufruf wird in der Methode Test() ausprobiert.
'...\Klassendesign\Vererbung\ParamArray.vb |
Option Strict On Namespace Klassendesign Class Motorrad Overloads Sub Mitfahrer(ByVal sozius As String) Console.WriteLine("{0} fährt hinten auf dem Motorrad", sozius) End Sub End Class Class MotorradMitBeiwagen Inherits Motorrad Overloads Sub Mitfahrer(ByVal ParamArray sozius() As String) Console.WriteLine("{0} fährt mit", sozius(0)) End Sub End Class Module Opitionales Sub Test() Dim moto As MotorradMitBeiwagen = New MotorradMitBeiwagen() moto.Mitfahrer("Miss Piggy") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die Methode in der Elternklasse angesprochen wird, da sie spezifischer ist und nicht von der ParamArray-Signatur verdeckt wird.
Miss Piggy fährt hinten auf dem Motorrad
Hinweis |
Parametersignaturen mit Optional sind nicht betroffen. |
3.13.5 Abstrakte Klassen: MustInherit und MustOverride
Bis jetzt waren die Entscheidungen freiwillig, was wie ver- oder geerbt wurde. Es gibt aber häufig Situationen, in denen man einen gewissen Zwang ausüben möchte. Zum Beispiel kann eine Klasse eine Elternklasse für eine ganze Gruppe von Kindklassen sein. Prinzipbedingt erfasst dann die in der Elternklasse formulierte gemeinsame Funktionalität nicht alle Spezialitäten der Kindklasse, auch wenn sie gleich benannt ist. In solchen Situationen ist es nicht möglich, die Elternklasse komplett auszuformulieren. Da eine solche »unvollständige« Klasse auch »unvollständige« Objekte erzeugen würde, unterbindet man mittels des Modifikators MustInherit die Möglichkeit der Objekterzeugung. Dennoch kann, in Bezug auf die Namensgebung, die Elternklasse vervollständigt werden, sodass zumindest die Schnittstelle, die zur Verwendung von Klassenmitgliedern nötig ist, festgelegt ist. Jedes Klassenmitglied in der Elternklasse, das zwar formell, aber nicht inhaltlich festgelegt werden kann, wird nur als Signatur mit dem Modifikator MustOverride in die Klasse geschrieben. Es gibt folgende vier Syntaxvarianten (optionale Teile sind in eckige Klammern gesetzt, kursive Teile müssen Sie Ihren Bedürfnissen anpassen, ein Unterstrich verbindet wie üblich Teile derselben logischen Zeile):
MustInherit Class Klasse MustOverride [<Modifikatoren>] Sub Methode([<Parameter>]) [<Handles>] MustOverride [<Modifikatoren>] Function _ Funktion([<Parameter>]) As Rückgabetyp [<Handles>] MustOverride [ReadOnly|WriteOnly] [<Modifikatoren>] Property _ Eigenschaft([<Parameter>]) As Rückgabetyp MustOverride [<Modifikatoren>] [ReadOnly|WriteOnly] Default Property _ Eigenschaft(<Parameter>) As Rückgabetyp End Class |
Die erlaubten Modifikatoren sind recht beschränkt (siehe Tabelle 3.24).
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (sieheAbschnitt 3.2, »Kapselung«), nicht: Private. |
Redefinition |
Art des Ersatzes (siehe Abschnitt 3.13.4, »Modifikation: Shadows und Overloads (Overrides)«, und Abschnitt 3.14.1, »Virtuelle Methoden: Overridable und Overrides«), nicht: Overridable. |
Dabei ist Folgendes zu beachten:
- Eine MustInherit-Klasse kann keine Objekte erzeugen, aber ist ein gültiger Datentyp.
- Eine Klasse mit MustOverride-Mitgliedern muss als MustInherit gekennzeichnet werden, aber eine MustInherit-Klasse kann ohne MustOverride-Mitglieder sein.
- Felder, Ereignisse, Delegates, Typen, Operatoren und Konstruktoren sind nie abstrakt.
- Abstrakte Klassenmitglieder sind nie mit Shared klassengebunden.
- Es gibt keine abstrakten externen Funktionen.
- Eine Kindklasse darf MustOverride-Mitglieder nur mit Overrides implementieren, eine Verdeckung des Symbols mit Shadows (implizit oder explizit) ist nicht erlaubt.
- Eine Kindklasse muss entweder alle MustOverride-Mitglieder der Elternklasse implementieren oder selbst mit MustInherit gekennzeichnet werden.
- Die Sichtbarkeit von MustOverride-Mitgliedern muss in der Eltern- und der Kindklasse identisch sein (Ausnahme: Protected Friend wird in einer anderen Anwendung zu Protected).
- Private MustOverride ist verboten, ebenso wie MustInherit NotInheritable.
- MustOverride Overrides ist erlaubt, wenn die Elternklasse ein passendes Mitglied hat.
Hinweis |
Eine abstrakte Klasse darf vollständige Mitglieder ohne MustOverride enthalten. |
Das folgende Codefragment zeigt als Beispiel die abstrakte Klasse Luftfahrzeug, die eine Methode Abheben() definiert, denn jedes Fluggerät erhebt sich irgendwie in die Lüfte, unabhängig davon, wie das im Einzelnen passiert. Da man beim Flugdrachen zum Abheben erst Anlauf nehmen muss und ein Hubschrauber sich ohne Muskelkraft senkrecht in die Luft erheben kann, ist es nicht sinnvoll, für beide in der allgemeinen Methode Abheben() ein gemeinsames Startverhalten zu definieren. Da aber dennoch auf anderen Gebieten Gemeinsamkeiten existieren, bietet es sich an, diese in die allgemeine Klasse Luftfahrzeug zu packen. Zum Beispiel haben beide Fluggeräte einen Piloten. Daher greifen die von Luftfahrzeug abgeleiteten Klassen Flugdrachen und Hubschrauber auf diesen »allgemeinen« Piloten zurück.
'...\Klassendesign\Vererbung\Abstrakt.vb |
Option Strict On Namespace Klassendesign MustInherit Class Luftfahrzeug Protected pilot As String Sub New(ByVal pilot As String) Me.pilot = pilot End Sub MustOverride Sub Starten() End Class Class Flugdrachen Inherits Luftfahrzeug Sub New(ByVal pilot As String) MyBase.New(pilot) End Sub Public Overrides Sub Starten() Console.WriteLine("{0} nimmt Anlauf und startet.", pilot) End Sub End Class Class Hubschrauber Inherits Luftfahrzeug Sub New(ByVal pilot As String) MyBase.New(pilot) End Sub Public Overrides Sub Starten() Console.WriteLine("{0} fragt den Tower und startet senkrecht.", pilot) End Sub End Class ... End Namespace
Zum Test werden zwei Fluggeräte erzeugt und gestartet. Bitte beachten Sie, dass beide Variablen als Datentyp das allgemeine Luftfahrzeug haben. Da in diesem bereits die Form des Aufrufs von Starten() steht, kann die Methode aufgerufen werden, auch wenn noch gar kein Methodenrumpf existiert. Welche Methode konkret aufgerufen wird, bestimmt Visual Basic automatisch, da selbst bei einer Referenz auf den Basistyp Luftfahrzeug der reale Typ des Objekts nicht verloren geht und somit alle Informationen vorliegen, um die richtige Methode wählen zu können.
'...\Klassendesign\Vererbung\Abstrakt.vb |
Option Strict On Namespace Klassendesign ... Module Abstrakt Sub Test() Dim drachen As Luftfahrzeug = New Flugdrachen("Otto Lilienthal") Dim hubschrauber As Luftfahrzeug = New Hubschrauber("Igor Sikorsky") drachen.Starten() hubschrauber.Starten() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass die zum Objekt passende Methode gewählt wurde:
Otto Lilienthal nimmt Anlauf und startet. Igor Sikorsky fragt den Tower und startet senkrecht.
Hinweis |
Um den Code besser lesbar zu halten, sollte MustInherit in jedem Teil einer mit Partial aufgeteilten Klasse angegeben werden. |
Handles-Klausel
Eine abstrakte Methode darf sich mittels Handles als eine Methode registrieren, die ein Ereignis behandelt. Dann sollte die implementierende Methode in einer Kindklasse sich nicht registrieren, da das Ereignis sonst doppelt behandelt wird. Das folgende Codefragment zeigt, wie die mit MustOverride als abstrakt markierte Methode Personenschutz() sich als behandelnde Methode für das Ereignis Bremsen registriert, während die Implementation in Auto dies unterlässt.
'...\Klassendesign\Vererbung\AbstraktUndHandles.vb |
Option Strict On Namespace Klassendesign MustInherit Class Fahrzeug Event Bremsen() MustOverride Sub Personenschutz() Handles Me.Bremsen Sub Gefahr() RaiseEvent Bremsen() End Sub End Class Class Auto Inherits Fahrzeug Public Overrides Sub Personenschutz() 'kein Handles Console.WriteLine("Gurt straffen!") End Sub Shared Sub Test() Dim auto As Auto = New Auto() auto.Gefahr() Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass das Ereignis behandelt wurde:
Gurt straffen!
Konstruktoren
Hat eine Klasse keine MustOverride-Mitglieder, ist aber mit MustInherit gekennzeichnet, können als Alternative auch alle Konstruktoren mit Protected der Öffentlichkeit entzogen werden, um eine Objekterzeugung zu unterbinden. Welche der beiden Varianten Sie wählen, ist eher Geschmackssache.
3.13.6 Spezifische Catch-Blöcke
Ein Catch-Block einer Fehlerbehandlung erwartet einen Parameter vom Typ Exception oder dessen Kindklassen. Da außerdem in einem Try/Catch-Block nur der erste passende Catch-Block zum Zuge kommt, bietet sich über die Vererbungshierarchie eine gestaffelte Fehlerbehandlung an. Die ersten Catch-Blöcke kümmern sich um Fehler, die durch ihren Typ genau spezifiziert sind. In den folgenden Blöcken werden dann die Fehler immer unspezifischer. Es bietet sich oft an, den allgemeinsten Fehler, Exception, im letzten Block abzufangen, um damit alle bisher nicht behandelten Ausnahmen zu verarbeiten.
Als Beispiel werden I/O-Ausnahmen gewählt. Die folgende Vererbungshierarchie ist ein Auszug (die Namensräume sind fett gedruckt):
System-Exception +-System-SystemException +System.IO-IOException +System.IO-+DirectoryNotFoundException +DriveNotFoundException +EndOfStreamException +FileLoadException +FileNotFoundException +PathTooLongException
Das folgende Codefragment fängt in der Methode Fehlerreport() pro Stufe der Hierarchie jeweils eine Ausnahme ab. Von oben nach unten werden die Ausnahmen allgemeiner, wir bewegen uns also in der Vererbungshierarchie nach oben. Der komplexe Aufruf mit Invoke ist zu Demonstrationszwecken im Beispiel, weil er eine Ausnahme erzeugen kann, die nicht vom Typ SystemException ist.
'...\Klassendesign\Vererbung\Catch.vb |
Option Strict On Namespace Klassendesign Module Auffangen Sub Aus(ByVal name As String, ByVal ex As Exception, ByVal wo As String) Console.WriteLine("{0,14} => {1} ({2})", name, ex.GetType().Name, wo) End Sub Sub Fehlerreport(ByVal name As String) Try Dim f As System.IO.FileInfo = New System.IO.FileInfo(name) Dim len As Long = f.Length GetType(Auffangen).GetMethod("Aus").Invoke(Nothing, New Object() {}) Catch ex As System.IO.FileNotFoundException Aus(name, ex, "FileNotFoundException") Catch ex As System.IO.IOException Aus(name.Substring(0, 10) & "...", ex, "IOException") Catch ex As SystemException Aus(name, ex, "SystemException") Catch ex As Exception Aus(name.Substring(0, 10) & "...", ex, "Exception") End Try End Sub Sub Test() Fehlerreport("C:\\GibtsNicht") Fehlerreport("C:\\" & New String("x"c, 300)) Fehlerreport("C:\\?.txt") Fehlerreport(My.Application.Info.LoadedAssemblies(0).Location) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt jeweils in den Klammern, in welchem Catch-Zweig eine Ausnahme abgefangen wurde.
C:\\GibtsNicht => FileNotFoundException (FileNotFoundException) C:\\xxxxxx... => PathTooLongException (IOException) C:\\?.txt => ArgumentException (SystemException) C:\WINDOWS... => TargetParameterCountException (Exception)
3.13.7 Eigene Ausnahmen
Es lohnt sich, von Anfang der Programmentwicklung an die Fehlerbehandlung mit zu berücksichtigen. Nach meiner Erfahrung ist es dazu hilfreich, eine eigene Vererbungshierarchie für Ausnahmen aufzubauen. Damit ist es möglich, im Fehlerfall bereits am Datentyp der Ausnahme zu erkennen, welchem Quelltextzusammenhang der Fehler zuzuordnen ist. Außerdem können dann in Try/Catch-Strukturen gezielt die Fehler gefiltert werden, ohne andere Fehlerquellen gleich mit berücksichtigen zu müssen. Alle eigenen Ausnahmen müssen von Exception oder dessen Kindklassen abgeleitet sein, wie die folgende Syntax zeigt:
Class Ausnahme Inherits Exception ... End Class Class SpezifischeAusnahme Inherits Ausnahme ... End Class |
Hinweis |
Alle Ausnahmen haben Exception als Vorfahren. |
Als Beispiel wählen wir einen Mitarbeiter, der auch mal in Urlaub oder krank ist (Feld zustand). Das folgende Codefragment enthält auch die möglichen Ausnahmeklassen. NichtDa ist auf der obersten Ebene und von Exception abgeleitet. Die Klasse ist abstrakt, um zu verhindern, dass eine grundlose Abwesenheit vorkommen kann. Der Aufruf MyBase.New(grund) sorgt dafür, dass das Feld Message von Exception initialisiert wird. Die beiden Klassen Urlaub und Krank spezialisieren die allgemeinere Ausnahmeklasse NichtDa.
'...\Klassendesign\Vererbung\Exception.vb |
Option Strict On Namespace Klassendesign Class Mitarbeiter Friend zustand As String = "OK" Friend name As String = "Werner" End Class MustInherit Class NichtDa : Inherits Exception Sub New(ByVal grund As String) MyBase.New(grund) 'sonst ist Feld Message nicht sinnvoll belegt End Sub End Class Class Urlaub : Inherits NichtDa Sub New(ByVal arb As Mitarbeiter) MyBase.New(arb.name & " hat Urlaub!") End Sub End Class Class Krank : Inherits NichtDa Sub New(ByVal arb As Mitarbeiter) MyBase.New(arb.name & " ist krank!") End Sub End Class ... End Namespace
Bei der Ausführung der Aufgaben wird der Zustand des Mitarbeiters geprüft, und gegebenenfalls wird mit Throw eine Ausnahme ausgelöst.
'...\Klassendesign\Vererbung\Exception.vb |
Option Strict On Namespace Klassendesign ... Module Aufgaben Sub NeueHeizung(ByVal arb As Mitarbeiter) Select Case arb.zustand Case "Urlaub" : Throw New Urlaub(arb) Case Else : Console.WriteLine("{0} erledigt den Job.", arb.name) End Select End Sub Sub Rohrbruch(ByVal arb As Mitarbeiter) Select Case arb.zustand Case "krank" : Throw New Krank(arb) Case Else : Console.WriteLine("{0} erledigt den Job.", arb.name) End Select End Sub End Module ... End Namespace
Die Aufgabenverteilung kann, je nach Zustand des Mitarbeiters, zu einer Ausnahme führen. Diese wird mit Catch aufgefangen. Da die Ausnahme Urlaub nur während der Methode NeueHeizung ausgelöst werden kann, ist beim ersten Catch auch direkt klar, dass die Ausnahme nur von dort stammen kann, denn eine Ausnahme Krank würde von diesem Catch schlicht ignoriert. Durch den allgemeineren Ausnahmetyp im zweiten Catch kann nicht direkt auf die Methode geschlossen werden, die die Ausnahme verursacht hat. Durch eine genauere Analyse des Parameters ex oder des Meldungstextes geht das.
'...\Klassendesign\Vererbung\Exception.vb |
Option Strict On Namespace Klassendesign ... Module Ausnahme Sub Test() Dim werner As Mitarbeiter = New Mitarbeiter() NeueHeizung(werner) werner.zustand = "Urlaub" Try Rohrbruch(werner) NeueHeizung(werner) Catch ex As Urlaub Console.WriteLine("Röhrich macht das selbst, denn {0}", ex.Message) End Try werner.zustand = "krank" Try Rohrbruch(werner) NeueHeizung(werner) Catch ex As NichtDa Console.WriteLine("Röhrich macht das selbst, denn {0}", ex.Message) End Try Console.ReadLine() End Sub End Module End Namespace
Hinweis |
Der Compiler gibt keine Hinweise, ob eventuell Ausnahmen nicht behandelt werden. |
3.13.8 Arrays
Wenn sich die Elemente zweier Arrays von Referenztypen implizit ineinander umwandeln lassen, gilt dies auch für das ganze Array.
a As A = New B() erlaubt => a() As A = New B(){…} erlaubt |
Hinweis |
Diese sogenannte Kovarianz von Arrays gilt nicht für Werttypen. |
Bei Methodenparametern, die mit ByRef deklariert sind, gibt es eine kleine Stolperfalle in Kombination mit Arrays. Diese müssen homogen sein, das heißt, alle Datentypen der Elemente müssen identisch sein. Eine Methode mit einem ByRef-Parameter gibt ein Objekt vom deklarierten Typ zurück. Wenn der aktuelle Parameter ein Feldelement mit einem Datentyp ist, der sich nicht implizit aus dem ByRef-Parameter umwandeln lässt, wird eine Ausnahme ausgelöst, da die eben erwähnte Rückgabe das Feld inhomogen machen würde. Das folgende Codefragment nutzt eine Methode Zuwendung() mit einem ByRef-Parameter, die in Test() einmal mit einer »normalen« Referenz und dann mit einem Feldelement aufgerufen wird.
'...\Klassendesign\Vererbung\ByRef.vb |
Option Strict On Namespace Klassendesign Module ByReference Sub Zuwendung(ByRef tier As Object) Console.WriteLine("Streichle {0}.", tier) tier = tier.ToString() & " ist gestreichelt" End Sub Sub Test() Dim Tier As Object = "Katze" Dim Zoo() As Object = New String(0) {"Löwe"} Zuwendung(Tier) Try Zuwendung(Zoo(0)) Catch ex As Exception 'System.ArrayTypeMismatchException Console.WriteLine("Ausnahme {0}: {1}", ex.GetType(), ex.Message) End Try Console.WriteLine(Tier) : Console.WriteLine(Zoo(0)) Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe zeigt, wie in der Methode Zuwendung() der Parameter vom Object das Tier vom Typ String akzeptiert. Demgegenüber erzeugt der Aufruf mit einem Feldelement gleichen Typs eine Ausnahme. Das Fehlen der Ausgabe zeigt, dass die Prüfung auf Datenkonsistenz eines Feldes schon während der Parameterübergabe erfolgt. Analog zeigen die letzten beiden Ausgaben, dass der erste Aufruf das Objekt geändert hat und der zweite nicht:
Streichle Katze. Ausnahme System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array. Katze ist gestreichelt Löwe
Hinweis |
Ein Objekt einer Kindklasse kann ein Stellvertreter für einen Wert vom Typ der Elternklasse sein. Für einen Speicherplatz (zum Beispiel ByRef) gilt dies nicht immer. |
3.13.9 Grafikbibliothek: neue Vielecke
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.11.6, »Grafikbibliothek: Addition und Umwandlung von Rechtecken«, erweiterte Grafikanwendung. Hier werden nur einige der Möglichkeiten der Vererbung genutzt. Zuerst wird die Idee eines Rechtecks in der Klasse Vieleck verallgemeinert. Diese abstrakte Klasse fordert das Vorhandensein einer schreibgeschützten Flächeneigenschaft. Dann wird das Rechteck von diesem Vieleck abgeleitet und diese Eigenschaft implementiert.
'...\Klassendesign\Graphik\Vererbung.vb |
Option Explicit On Namespace Klassendesign Public MustInherit Class Vieleck MustOverride ReadOnly Property Fläche() As Double End Class Partial Public Class Rechteck : Inherits Vieleck Overrides ReadOnly Property Fläche() As Double Get Return a * b End Get End Property End Class ... End Namespace
Damit sichergestellt ist, dass alle Seiten des Quadrats gleiche Längen haben, müssen einige Klassenmitglieder der Klasse Rechteck überschrieben werden. Sowohl der Konstruktor als auch die Größenänderungen müssen sich der neuen Situation anpassen. Um es einfach zu halten, wird ein Breitenparameter ignoriert und die Definition der Breite ohne Funktionalität überdeckt. Der Sichtbarkeitsmodifikator ist so gewählt, dass nicht die Funktionalität des Rechtecks »durchscheint« (er könnte auch Friend sein).
'...\Klassendesign\Graphik\Vererbung.vb |
Option Explicit On Namespace Klassendesign ... Partial Public Class Quadrat : Inherits Rechteck Sub New(Optional ByVal a As Double = 1) MyBase.New(a, a) Größe(a, a) End Sub Overloads Sub Größe(ByVal a As Double, Optional ByVal b As Double = 1) MyBase.Größe(a, a) End Sub Shadows Property Länge() As Double Get Return MyBase.Länge End Get Set(ByVal a As Double) Größe(a, a) End Set End Property Public Shadows Breite As Double End Class End Namespace
Zum Test wird ein Quadrat erzeugt, dessen Abmessungen geändert werden, was zusätzlich protokolliert wird.
'...\Klassendesign\Zeichner\Vererbung.vb |
Option Explicit On Namespace Klassendesign Partial Class Zeichner Sub Vererbung() Dim q As New Quadrat(7) : q.Größe() 'Quadrat(7,7) nicht möglich q.Größe(8, 9) : q.Größe() 'ignoriert zweiten Parameter q.Länge = 4 : q.Größe() q.Breite = 3 : q.Größe() 'durch Überdeckung ohne Einfluss Console.Write("Fläche {0}", q.Fläche) End Sub End Class End Namespace
Zur Kontrolle sehen Sie hier die Ausgabe der Methode Vererbung():
Dimension des Rechtecks: 7x7 Dimension des Rechtecks: 8x8 Dimension des Rechtecks: 4x4 Dimension des Rechtecks: 4x4 Fläche 16
Die nächste Erweiterung erfolgt in Abschnitt 3.14.7, »Grafikbibliothek: Objektsammlungen«.
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.