4.4 Generisches
Das Rad neu erfinden … oder besser doch nicht?
Die Tätigkeit eines Programmierers ist, neben den grandiosen Momenten des Erfolgs, durch ein gerüttelt Maß an Routine gekennzeichnet. Im Alltag tauchen immer wieder Problemstellungen auf, die sehr ähnlich sind, aber eben doch nicht identisch. Häufig ist der einzige Unterschied, dass Daten unterschiedlichen Typs vorliegen, die Operationen auf den Daten aber im Wesentlichen gleich bleiben. Ein Beispiel ist die Berechnung eines Maximums aus Werten, so dass der richtige Datentyp zurückgegeben wird und nicht einfach der »maximal mögliche« Double. Da Visual Basic mit der dringend zu empfehlenden Compileroption Strict eine strenge Typisierung aller Variablen erzwingt, sind Programme nicht einfach für andere Datentypen zu übernehmen. Die Vorgehensweise, ein Programmteil zu kopieren und die Datentypen anzupassen, ist keinesfalls eine gute Idee. Zum einen ist es zeitaufwändig, die ganzen Kopien immer auf dem neuesten Stand zu halten, zum anderen passiert es sehr leicht, dass man die Korrektur in einer der Kopien vergisst. Daher bietet Visual Basic die Möglichkeit, Definitionen so zu formulieren, dass die Datentypen »austauschbar« sind und erst bei Verwendung einer Klasse oder Methode festgelegt werden. Da dies im Code erfolgt, hat der Compiler weiterhin die Möglichkeit, eine strenge Typisierung zu erzwingen und Ihnen so bei der Fehlersuche zu helfen.
Syntaktisch werden ein oder mehr Typparameter hinter einem Bezeichner eingeführt. Diese Typparameter können dann im selben Codeblock wie jeder andere Typ verwendet werden. Bei der Nutzung wird dann für jeden Typparameter ein konkreter Datentyp angegeben. Die folgenden Syntaxvarianten enthalten alle Arten von Definitionen, die Typparameter einführen dürfen (die letzten vier sind die einzigen Typmitglieder). Optionale Teile sind in eckige Klammern gesetzt, und die kursiv gesetzten Namen müssen Sie Ihren Bedürfnissen entsprechend anpassen, insbesondere besteht Wahlfreiheit für voneinander unabhängige Typparameter. Wie üblich verbindet der Unterstrich zwei Codezeilen zu einer logischen Zeile.
[<Modifikatoren>] Class Name(Of Typparameter [, ...]) [<Definitionen>] End Class [<Modifikatoren>] Structure Name(Of Typparameter [, ...]) Variablen/Ereignisdeklaration [<Definitionen>] End Structure [<Modifikatoren>] Interface IName(Of Typparameter [, ...]) [<Definitionen>] End Interface [<Sichtbarkeit>] Delegate Sub Name(Of Typparameter [, ...])([<Parameter>]) [<Sichtbarkeit>] Delegate Function _ Name(Of Typparameter [, ...])([<Parameter>]) As Typ [<Modifikatoren>] Sub name[(Of TypparameterX [, ...])]([<Parameter>]) _ [Implements Schnittstelle(Of TypparameterY [, ...]).mitglied [, ...]] [<Anweisungen>] End Sub [<Mod.>] Function name[(Of TypparameterX [, ...])]([<Parameter>]) As Typ _ [Implements Schnittstelle(Of TypparameterY [, ...]).mitglied [, ...]] [<Anweisungen>] End Function |
Hinweis |
Definitionen mit Typparametern werden generisch genannt. |
Auf einige Besonderheiten möchte ich gezielt hinweisen:
- Module (Abschnitt 4.1, »Module«), Enumerationen (Abschnitt 4.3, »Enumerationen«) und anonyme Klassen (Abschnitt 4.6, »Anonyme Klassen«) können keine Typparameter einführen, wohl aber solche aus umliegenden Quelltextblöcken benutzen.
- Felder, Eigenschaften, Ereignisse sowie Operatoren und externe Funktionen können keine Typparameter einführen, wohl aber solche aus umliegenden Quelltextblöcken benutzen.
- Typmitglieder und Typparameter teilen sich einen Namensraum und müssen zusammengenommen eindeutig sein.
- Die Anzahl der Typparameter ist ein Teil der Signatur von Datentypen und Methoden. Dies ermöglicht die Überladung von Typen und Methoden nur auf Basis der Anzahl der Typparameter. Definitionen, die sich nur durch die Existenz von Typparametern unterscheiden, haben dennoch nichts miteinander zu tun und dürfen daher nebeneinander existieren (Methodensignaturen sind in Abschnitt 3.3.1, »Prinzip«, beschrieben).
- Typparameter sind bezüglich der Deklaration ein Quelltextkonstrukt und gelten nur innerhalb desselben Codeblocks im Quelltext bis zur korrespondierenden End-Anweisung bzw. bis zum Zeilenende bei Delegates.
- Typparameter können nicht vererbt und nicht mit Typnamen qualifiziert werden.
- Typparameter von mit Partial aufgeteilten Definitionen müssen identisch übereinstimmen, selbst die Bezeichner der Typparameter müssen übereinstimmen.
- Generische Methoden können keine Ereignisbehandlung mit Handles deklarieren.
- Ein namensgleicher Typparameter verdeckt den Typparameter des umschließenden Codeblocks.
- Typparameter können eingeschränkt werden, siehe Abschnitt 4.4.1, »Einschränkungen«.
Hinweis |
Generische Typen, in denen zumindest ein Teil der Typparameter durch konkrete Typen festgelegt sind, werden als konstruiert bezeichnet. Nicht vollständig festgelegte Typen heißen offen. |
Die Nutzung der Typparameter im Code ist denkbar einfach. Statt eines konkreten Typs geben Sie einfach einen Typparameter an. Das folgende Fragment skizziert das Vorgehen:
Class Generisch(Of Typ) Public Variable As Typ ... End Class |
Hinweis |
Der Typ jeder Variablen liegt zur Laufzeit komplett fest, er ist nie offen. |
Schauen wir uns als erstes Beispiel eine generische Methode mit einem Typparameter an. Sie tauscht die Werte zweier Variablen gegeneinander aus. Um die Datentypen flexibel zu halten, wird überall in der Methode der Typparameter Typ verwendet. Zum Vergleich ist auch eine Variante mit »normalen« Datentypen definiert. Es ist also möglich, generische und normale Definitionen parallel zu halten. Ob das wirklich sinnvoll ist und nicht eher Verwirrung stiftet, entscheiden Sie bitte im Einzelfall selbst. In der Methode Test() wird dreimal getauscht, wobei im letzten Aufruf der Compiler den Datentyp automatisch aus den übergebenen Werten ermittelt. In diesem letzten Fall ist es reine Geschmackssache, ob Sie die Formulierung mit Of bevorzugen oder lieber den Compiler die Typen von Typparametern ermitteln lassen. Manchmal jedoch scheitert der Compiler, und Sie müssen die Variante mit Of verwenden. Oder es liegt eine konkurrierende nichtgenerische Definition wie im zweiten Aufruf vor, die bevorzugt wird, da sie »spezifischer« ist. Ohne generische Datentypen müssten Sie für jeden Datentyp eine neue Methode schreiben, hier hingegen kommen Sie durch den Typparameter mit einer einzigen Methode hin.
'...\Datentypen\Generisch\Methode.vb |
Option Strict On Namespace Datentypen Module Methode Sub Tausch(Of Typ)(ByRef links As Typ, ByRef rechts As Typ) Dim temp As Typ = links links = rechts rechts = temp End Sub Sub Tausch(ByRef links As Integer, ByRef rechts As Integer) Console.WriteLine("Nichtgenerischer Aufruf:") Dim temp As Integer = links links = rechts rechts = temp End Sub Sub Test() Dim w1 As Integer = 5, w2 As Integer = 17 Console.WriteLine("Vorher : {0,3} {1,3}", w1, w2) Tausch(Of Integer)(w1, w2) Console.WriteLine("Nachher : {0,3} {1,3}", w1, w2) Tausch(w1, w2) Console.WriteLine("Original: {0,3} {1,3}", w1, w2) Dim d1 As Date = Now, d2 As Date = Now.AddDays(1.234) Console.WriteLine("Vorher : {0,22} {1,22}", d1, d2) Tausch(d1, d2) Console.WriteLine("Nachher : {0,22} {1,22}", d1, d2) Console.ReadLine() End Sub End Module End Namespace
Die Werte werden in allen Fällen korrekt getauscht:
Vorher : 5 17 Nachher : 17 5 Nichtgenerischer Aufruf: Original: 5 17 Vorher : 3/10/2008 12:41:09 PM 3/11/2008 6:18:06 PM Nachher : 3/11/2008 6:18:06 PM 3/10/2008 12:41:09 PM
Häufiger als Methoden kommen generische Datentypen zum Einsatz. Als Beispiel folgt ein sogenannter Stapel, der Werte aufnimmt und in umgekehrter Reihenfolge wieder abgibt (englisch LIFO: last in first out). Bei Warteschlangen behält man eher die Reihenfolge bei (englisch FIFO: first in first out). Der Typ der gespeicherten Werte wird durch einen Typparameter auf Klassenebene spezifiziert. Die Methoden zur Änderung des Stapels verwenden denselben Typparameter (ein Typparameter auf Methodenebene kann zu inkonsistenten Datentypen führen).
'...\Datentypen\Generisch\Typ.vb |
Option Strict On Namespace Datentypen Class Stapel(Of Typ) Private werte() As Typ = New Typ() {} Private ende As Integer = –1 Sub Hinzufügen(ByVal wert As Typ) If ende >= werte.Length – 1 Then ReDim Preserve werte(werte.Length) ende += 1 werte(ende) = wert End Sub Function Wegnehmen() As Typ If ende < 0 Then Throw New InvalidOperationException("Stapel leer!") ende -= 1 Return werte(ende + 1) End Function End Class ... End Namespace
In Test() verwenden wir einen Stapel aus Zeichen. Die Buchstaben einer Zeichenkette werden dem Stapel hinzugefügt und dann wieder entnommen. Die Beispielzeichenkette hört sich rückwärts gelesen genauso an wie vorwärts gelesen. So etwas nennt man Palindrom.
'...\Datentypen\Generisch\Typ.vb |
Option Strict On Namespace Datentypen ... Module Typ Sub Test() Dim st As Stapel(Of Char) = New Stapel(Of Char) Dim palindrom As String = "nie leg Raps neben Spargel ein" For Each c As Char In palindrom : st.Hinzufügen(c) : Next Try For no As Integer = 0 To 50 : Console.Write(st.Wegnehmen()) : Next Catch ex As Exception Console.WriteLine(Environment.NewLine & ex.Message) End Try Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt die umgekehrte Entnahme der Werte und die Ausnahme, die durch den Entnahmeversuch aus einem leeren Stapel ausgelöst wird.
nie legrapS neben spaR gel ein Stapel leer!
Auch Funktionszeiger dürfen Typparameter enthalten. Mit einem generischen Delegate lassen sich viele Typen von Ereignissen gleichzeitig beschreiben. Das folgende Beispiel definiert ein Delegate, das von zwei Typparametern abhängt. Dadurch lassen sich in den beiden folgenden Klassen die Ereignisse durch dasselbe generische Delegate mit unterschiedlichen konkreten Datentypen typisieren. Das Modul Delegates enthält eine Methode zur Behandlung der Ereignisse, die in Test() ausgelöst werden.
'...\Datentypen\Generisch\Delegate.vb |
Option Strict On Namespace Datentypen Delegate Sub Handler(Of S, A)(ByVal sender As S, ByVal argumente As A) Class Button Event Click As Handler(Of Button, Date) Sub Drücken() RaiseEvent Click(Me, Now) End Sub End Class Class Liste Event Click As Handler(Of Liste, Integer) Sub Drücken(ByVal eintrag As Integer) RaiseEvent Click(Me, eintrag) End Sub End Class Module Delegates Sub Element(ByVal sender As Object, ByVal daten As Object) Console.WriteLine("{0} gedrückt: {1}", sender.GetType().Name, daten) End Sub Sub Test() Dim b As New Button(), l As New Liste() AddHandler b.Click, AddressOf Element AddHandler l.Click, AddressOf Element b.Drücken() : l.Drücken(27) Console.ReadLine() End Sub End Module End Namespace
Die Behandlungsmethode wird beide Male korrekt aufgerufen:
Button gedrückt: 3/10/2008 3:23:05 PM Liste gedrückt: 27
Hinweis |
Typparameter, die bei der Nutzung eines generischen Typs fehlen, versucht der Compiler automatisch zu ermitteln. |
4.4.1 Einschränkungen
Es ist nicht möglich, die vollständige Allgemeinheit eines Datentyps immer aufrechtzuerhalten. Das Einzige, was alle Objekte gemeinsam haben, sind die Methoden der Klasse Object. Damit allein lässt sich nicht viel machen. Daher ist es sehr oft sinnvoll, den Typparameter ein wenig festzulegen, zum Beispiel durch die Angabe einer Schnittstelle, die er implementiert. Das hat zwei Effekte:
- Es ist leichter, konkreten Code für eingeschränkte Typparameter zu formulieren, da Zugriff auf die Funktionalität all der Typen besteht, die die Einschränkung erfüllen.
- Nur Datentypen, die die Einschränkungen erfüllen, können bei der Nutzung des generischen Typs oder der generischen Methode verwendet werden.
Die Spezifikation der Einschränkung lehnt sich an die Deklaration von Variablen an. In den folgenden beiden Syntaxvarianten sind Alternativen durch einen senkrechten Strich getrennt und optionale Teile in eckige Klammern gesetzt. Kursiv gesetzte Teile müssen Sie Ihren Bedürfnissen anpassen. Wie üblich verbindet der Unterstrich zwei Codezeilen zu einer logischen Zeile.
Typparameter As Klasse | Schnittstelle | anderer Typparameter | _ New | Class | Structure Typparameter As {Einschränkung [, ...]} |
Auch hier möchte ich auf einige Besonderheiten hinweisen:
- Module, Strukturen und Enumerationen können nicht in der As-Klausel verwendet werden.
- Typeinschränkungen ergänzen sich, jede zusätzliche erweitert die Zugriffsmöglichkeiten. Die anderen (New, Class, Structure) engen die erlaubten konkreten Typen ein.
- Datentypen in der As-Klausel dürfen generisch sein und den Typparameter selbst enthalten.
- Einschränkungen müssen mindestens so sichtbar sein wie die Definition, die sie enthält.
- Es darf nur eine Klasse gleichzeitig als Einschränkung deklariert werden, mehrere Schnittstellen gleichzeitig sind erlaubt.
- Die Kombination eines konkreten Typs mit Class oder Structure ist nicht erlaubt.
- Typparameter mit Structure-Einschränkung sind in der As-Klausel verboten.
- Signaturen werden als gleich angesehen, wenn sie sich nur durch die Einschränkungen der Typparameter unterscheiden.
- Datentypen in der As-Klausel dürfen nicht mit NotInheritable gekennzeichnet sein, nullbare Typen (siehe Abschnitt 4.5, »Werttypen mit dem Wert Nothing«) sind durch Structure ausgeschlossen.
- Array, Delegate, MulticastDelegate, Enum, Object und ValueType im Namensraum System sind nicht als Datentypen in der As-Klausel zugelassen.
- Bei Vererbung sind durch Überschreiben eines Elternklassenmitglieds oder Implementieren eines Schnittstellenmitglieds die beiden letzten Punkte verletzbar (bezüglich des Typparameters sind dann nur Typumwandlungen möglich, die DirectCast erlaubt).
Den Einsatz einer Einschränkung zeigt das nächste Beispiel, das das Maximum einer beliebigen Anzahl von Werten ermittelt, indem es die Methode CompareTo() der Schnittstelle IComparable verwendet. In der As-Klausel wird der Typparameter selbst verwendet, um die Methodenparameter typrichtig miteinander zu vergleichen. Bitte beachten Sie, dass in Test() der Compiler den Datentyp des Typparameters T von Max() automatisch ermitteln kann (sonst müssten Sie Max(Of Single)(17, 9, 102.7F, 89, –281, 2) schreiben).
'...\Datentypen\Generisch\Einschränkung.vb |
Option Strict On Namespace Datentypen Module Einschränkung Function Max(Of T As IComparable(Of T))(ByVal ParamArray w() As T) As T Dim m As T For Each ww As T In w : m = If(ww.CompareTo(m) > 0, ww, m) : Next Return m End Function Sub Test() Dim maximum As Single = Max(17, 9, 102.7F, 89, –281, 2) Console.WriteLine("Maximum: {0}", maximum) Console.ReadLine() End Sub End Module End Namespace
Das Maximum wird korrekt bestimmt:
Maximum: 102.7
Ein generischer Typ kann auch mehrere Einschränkungen haben, die in geschweiften Klammern stehen. Im Code kann auf die Mitglieder aller Typen zugegriffen werden, die in der Einschränkung spezifiziert worden sind. Nicht alle Kombinationen sind dabei erlaubt (siehe oben). Im folgenden Beispiel werden die Klasse Kämpfer und die Schnittstelle IPrüfling als Typparameter in der Methode Info() zugelassen. Daher kann auf Mitglieder beider Typen zugegriffen werden: auf Prüfling aus Kämpfer und auf Name aus IPrüfling.
'...\Datentypen\Generisch\MehrfachEinschraenkung.vb |
Option Strict On Namespace Datentypen Interface IPrüfling Function Name() As String End Interface Class Kämpfer : Implements IPrüfling Private wer As String Sub New(ByVal name As String) wer = name End Sub Function Prüfling() As String Implements IPrüfling.Name Return wer End Function End Class Module MehrfachEinschraenkungen Sub Info(Of T As {Kämpfer, IPrüfling})(ByVal wer As T) Console.WriteLine("Prüfling: {0}", wer.Prüfling()) Console.WriteLine("Prüfling: {0}", wer.Name()) End Sub Sub Test() Dim kämpfer As New Kämpfer("Liu Yu Te") Info(kämpfer) Console.ReadLine() End Sub End Module End Namespace
Beide Zugriffe in Info() erzeugen die gleiche Ausgabe:
Prüfling: Liu Yu Te Prüfling: Liu Yu Te
Structure
Anders als es der Name vermuten lässt, beschränkt man sich mit der Einschränkung Structure nicht auf Strukturen, sondern auf beliebige von ValueType abgeleitete Typen. Es werden zum Beispiel auch Primitive und Enumerationen erfasst. Nicht erlaubt sind jedoch nullbare Typen (siehe Abschnitt 4.5, »Werttypen mit dem Wert Nothing«). Im folgenden Codefragment wird in der Methode Test() die Methode Ausgabe() mit einer Struktur, einer Fließkommazahl sowie einer Enumeration als Parameter aufgerufen. Alle sind Werttypen und erfüllen die Structure-Einschränkung der Methode. Die auskommentierte Zeile dagegen würde zu einem Compilerfehler führen.
'...\Datentypen\Generisch\Structure.vb |
Option Strict On Namespace Datentypen Structure Zahl Friend Wert As Integer Public Overrides Function ToString() As String Return Wert.ToString() End Function End Structure Enum Schalter : Aus : An : End Enum Module StructureEinschränkung Sub Ausgabe(Of Typ As Structure)(ByVal wert As Typ) Console.WriteLine("Wert {0}", wert) End Sub Sub Test() Dim z As Zahl : z.Wert = 99 : Ausgabe(z) Dim w As Double = 77.2 : Ausgabe(w) Dim s As Schalter = Schalter.Aus : Ausgabe(s) 'Ausgabe(New Object()) 'Compilerfehler!! Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Arbeitsweise:
Wert 99 Wert 77.2 Wert Aus
Hinweis |
Ohne Structure-Einschränkung können Typen mit Is und IsNot verglichen werden, auch wenn die konkreten Typen Werttypen sind (über den Sinn entscheiden Sie bitte selbst). |
Class
Die Einschränkung Class reduziert die möglichen Datentypen für den Typparameter auf Referenztypen und verbietet alle Werttypen. Es stellt damit fast das Pendant zur Structure-Einschränkung dar. Das »fast« bezieht sich darauf, dass auch hier die nullbaren Typen (siehe Abschnitt 4.5, »Werttypen mit dem Wert Nothing«) ausgeschlossen sind. So eine Art Ausschluss ist sinnvoll, wenn zum Beispiel sichergestellt werden soll, dass der Kopiermechanismus der Werttypen nicht stattfindet oder Operatoren wie Is verwendet werden, die nur für Referenztypen erlaubt sind. Das folgende Codefragment beschränkt den Typparameter der Methode Ausgabe() mit Class und ruft sie in Test() mit einem Objekt und einer Schnittstellenreferenz auf. Die auskommentierten Zeilen würden zu einem Compilerfehler führen, da Ausgabe() mit einem Werttyp aufgerufen wird.
'...\Datentypen\Generisch\Class.vb |
Option Strict On Namespace Datentypen Structure Wert : Friend Zahl As Integer : End Structure Enum Antwort : Ja : Nein : End Enum Interface IMarkierung : End Interface Class Implementierung : Implements IMarkierung : End Class Module ClassEinschränkung Sub Ausgabe(Of Typ As Class)(ByVal wert As Typ) Console.WriteLine("Wert initialisiert: {0}", wert IsNot Nothing) End Sub Sub Test() Ausgabe(New Object()) Dim mark As IMarkierung = New Implementierung() : Ausgabe(mark) mark = Nothing : Ausgabe(mark) 'Ausgabe(New Wert()) 'Compilerfehler!! 'Ausgabe(77.2) 'Compilerfehler!! 'Ausgabe(Antwort.Nein) 'Compilerfehler!! Console.ReadLine() End Sub End Module End Namespace
Der Is-Operator arbeitet erwartungsgemäß:
Wert initialisiert: True Wert initialisiert: True Wert initialisiert: False
New
Soll ein Typparameter zur Erzeugung von Objekten mit dem New-Operator herangezogen werden, muss er New als Einschränkung spezifizieren. Damit sind Typen erlaubt, die nicht mit MustInherit gekennzeichnet sind und einen sichtbaren parameterlosen Konstruktor zur Verfügung stellen. Dieser ist auch der einzige, mit dem ein Typparameter ein Objekt erzeugen kann. Die folgenden Codefragmente erzeugen daher auch einen Compilerfehler: Dem ersten fehlt die New-Einschränkung, und das zweite verwendet einen Konstruktorparameter.
Function Neu(Of Typ)() As Typ Return New Typ() 'Compilerfehler!! End Function Function Neu(Of Typ As New)() As Typ Return New Typ(1) 'Compilerfehler!! End Function
Das folgende Beispiel protokolliert in der Methode Neu() die Objekterzeugung. Durch die Einschränkung New für den Typparameter können Objekte über den Typparameter erzeugt werden. In Test() werden verschiedene Objekte erzeugt. Die auskommentierten Zeilen würden zu einem Compilerfehler führen, da die verwendeten Datentypen nicht die New-Einschränkung erfüllen – der erste, weil er durch MustInherit abstrakt ist, und der zweite, weil er keinen parameterlosen Konstruktor mit ausreichender Sichtbarkeit zur Verfügung stellt.
'...\Datentypen\Generisch\New.vb |
Option Strict On Namespace Datentypen MustInherit Class Abstrakt : End Class Class Privat Private Sub New() End Sub End Class Module NewEinschränkung Function Neu(Of Typ As New)() As Typ Console.WriteLine("Erzeuge Objekt vom Typ {0}.", GetType(Typ).Name) Return New Typ() End Function Sub Test() Neu(Of Object)() Dim rnd As Random = Neu(Of Random)() 'Dim abs As Abstrakt = Neu(abs) 'Compilerfehler!! 'Dim prv As Privat = Neu(prv) 'Compilerfehler!! Console.ReadLine() End Sub End Module End Namespace
Alle mit der Methode erzeugten Objekte werden protokolliert.
Erzeuge Objekt vom Typ Object. Erzeuge Objekt vom Typ Random.
Stolperfallen
Leider entspricht der Compiler nicht in allen Punkten der Sprachspezifikation von Visual Basic. Das folgende Codefragment sollte eigentlich gar nicht übersetzt werden, da die New-Einschränkung vom Rennauto nicht erfüllt wird, weil kein parameterloser Konstruktor existiert. Besonders ärgerlich ist, dass der erste Fahren()-Aufruf gänzlich ignoriert wird und der zweite komplett durchläuft, so dass kein Anhaltspunkt für eine Fehlersuche existiert.
'...\Datentypen\Generisch\Stolperfallen.vb |
Option Strict On Namespace Datentypen Class Auto Sub Fahren() Console.WriteLine("{0} fährt.", Me.GetType().Name) End Sub End Class Class Rennauto : Inherits Auto Sub New(ByVal fahrer As String) End Sub End Class Module Stolperfallen Sub Fahren(Of T As {New, Auto})(ByVal ParamArray w() As T) For Each a As Auto In w : a.Fahren() : Next End Sub Sub Test() Fahren(New Rennauto("Laie")) Fahren(New Rennauto("Laie"), New Auto()) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass der erste Fahren()-Aufruf fehlt:
Rennauto fährt. Auto fährt.
Hinweis |
Ohne ParamArray tritt das Problem nicht auf. |
4.4.2 Innere generische Typen
Ist ein Datentyp in einem anderen generisch, verhält er sich genauso wie ein nichtgenerischer. Er hat, wie jedes Mitglied des äußeren Typs, Zugriff auf dessen Typparameter. Bei der Verwendung sind alle Typparameter, ob des äußeren oder des inneren Typs, mit konkreten Typen oder Typparametern des umschließenden Typs zu belegen. Das nächste Beispiel definiert die generische Klasse Motor als inneren Typ der ebenfalls generischen Klasse Airbus. Im Konstruktor der inneren Klasse werden die Typparameter der äußeren und der inneren Klasse verwendet. Die Verwendung des Typs zeigt, dass jeder generische Typ seine eigenen Typen mittels Of festlegt.
'...\Datentypen\Generisch\InnereTypen.vb |
Option Strict On Namespace Datentypen Class Jet : End Class Class Rakete : End Class Class Airbus(Of Typ) Class Motor(Of Antrieb) Sub New(ByVal flugzeug As Typ, ByVal motor As Antrieb) Console.WriteLine("Flugzeug {0} mit Motor {1}", flugzeug, motor) End Sub End Class End Class Module InnereTypen Sub Test() Dim m As New Airbus(Of Jet).Motor(Of Rakete)(New Jet(), New Rakete()) Console.ReadLine() End Sub End Module End Namespace
Für die Verwendung der Typparameter spielt es keine Rolle, woher sie kommen. Die Ausgabe bereitet daher keine Probleme (auch wenn in der Realität Airbus so etwas noch nicht in der zivilen Luftfahrt verwendet).
Flugzeug Generisch.Datentypen.Jet mit Motor Generisch.Datentypen.Rakete
4.4.3 Vererbung und Überladung
Wenn eine generische Klasse beerbt wird, muss für jeden Typparameter in der Implements-Klausel ein Typ angegeben werden. Da dies wiederum ein Typparameter sein darf, aber nicht sein muss, kann eine Kindklasse wieder generisch sein oder aber konkret (geschlossen). Diese Flexibilität führt dazu, dass ein und dieselbe Methode in einer Klasse oder Schnittstelle mehrfach überschrieben bzw. implementiert werden kann. Bei der Kindklasse ist es wichtig, dass sie genauso viele generische Typen (englisch arity) verwendet wie die Elternklasse und Einschränkungen für Typparameter in der Elternklasse in der Kindklasse nicht verletzt werden. Dies gilt für alle Arten generischer Typen und Methoden.
Hinweis |
Generische Methoden, die mit Overrides oder Implements durch verschiedene aktuelle Typargumente mehrfach definiert werden, dürfen sich nicht überlappen. |
Typen
Eine Kindklasse spezialisiert immer die Elternklasse. Dies bezieht sich insofern auch auf die Typparameter, als dass sie mindestens so speziell sein müssen wie in der Elternklasse. Im folgenden Beispiel wird die generische Klasse Box etwas spezieller in der Klasse ElternBox durch die Beschränkung des Typparameters auf Kind. Typparametern können in einer Kindklasse also zusätzliche Beschränkungen auferlegt werden. Einmal gemachte Einschränkungen in der Elternklasse dagegen können in der Kindklasse nicht zurückgenommen werden. Da die Klasse ElternBox einen Typparameter enthält, ist sie auch weiterhin generisch. In der dritten Stufe wird daraus eine einfache Klasse durch die Festlegung des Typparameters auf Kind. Die auskommentierte Zeile würde einen Compilerfehler erzeugen, da Date nicht die Einschränkung des Typparameters in ElternBox erfüllt. In der Methode Test() ist zu sehen, dass Objekte von jeder der Stufen generiert werden können.
'...\Datentypen\Generisch\Kindtyp.vb |
Option Strict On Namespace Datentypen Class Eltern : End Class Class Kind : Inherits Eltern : End Class Class Box(Of Was) : End Class Class ElternBox(Of W As Eltern) : Inherits Box(Of W) : End Class Class KindBox : Inherits ElternBox(Of Kind) : End Class 'Class Termin : Inherits ElternBox(Of Date) : End Class 'Compilerfehler!! Module Kindtyp Sub Test() Dim direkt As New Box(Of Kind) Dim eltern As New ElternBox(Of Kind) Dim kind As New KindBox() Console.WriteLine("Typen: {0} {1} {2}", direkt.GetType().Name, _ eltern.GetType().Name, kind.GetType().Name) Console.WriteLine("Vererbt: {0}", TypeOf kind Is Box(Of Kind)) Console.WriteLine("Umwandelbar: {0}", _ eltern.GetType() Is GetType(ElternBox(Of Eltern)())) Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe zeigt etwas ungewohnte Namen von Klassen. Der Akzent kennzeichnet generische Typen, und die Zahl gibt die Anzahl Typparameter an:
Typen: Box`1 ElternBox`1 KindBox Vererbt: True Umwandelbar: False
Die beiden letzten Ausgaben machen deutlich:
Es gibt keine Vererbung bezüglich der konkreten Typen von Typparametern. |
Anders formuliert:
Eine Vererbungshierarchie generischer Typen hat identische konkrete Typen für alle Typparameter. |
Damit ist das folgende Codefragment fehlerhaft:
Dim b As ElternBox(Of Eltern) = New ElternBox(Of Kind) 'Compilerfehler!!
Hinweis |
Die Ableitung einer generischen Klasse mit einem Typparameter statt mit einem konkreten Typ erzeugt wieder eine generische Klasse. |
Überschreiben und Überladen
Definiert eine Elternklasse virtuelle Methoden mit Overridable oder MustOverride, werden diese in Kindklassen mit derselben Signatur und dem Schlüsselwort Overrides überschrieben. Dabei müssen alle Einschränkungen der Typparameter erhalten werden: Eine Kindklasse kann keine neue Einschränkung einführen oder eine vorhandene abändern. Da nur die Anzahl der Typparameter Teil der Signatur ist, ist durch eine Einschränkungsänderung auch kein Überladen möglich. Das folgende Codefragment zeigt, dass eine Umbenennung der Typparameter von Argumentausgabe in der ersten Methode von Konsolenausgabe erlaubt ist. Durch eine geänderte Anzahl der Typparameter stellt die zweite Methode eine Überladung dar. Die auskommentierte Zeile würde einen Compilerfehler auslösen, da eine Einschränkung eines Typparameters die Signatur der darüber stehenden Methode nicht ändert und aus Sicht des Compilers die Methode doppelt vorhanden ist.
'...\Datentypen\Generisch\Kindmethode.vb |
Option Strict On Namespace Datentypen MustInherit Class Argumentausgabe MustOverride Sub Druck(Of T, P)(ByVal arg As T) End Class Class Konsolenausgabe : Inherits Argumentausgabe Overrides Sub Druck(Of W, F)(ByVal zahl As W) Console.WriteLine("Start: " & zahl.ToString(0)) End Sub Overloads Sub Druck(Of W)(ByVal zahl As W) Console.WriteLine("Argument: {0}", zahl) End Sub 'Overloads Sub Druck(Of W As Class)(ByVal z As W) 'Compilerfehler!! End Class Module Kindmethode Sub Test() Dim aus As New Konsolenausgabe() aus.Druck(Of Double, Argumentausgabe)(9.8) aus.Druck(9.8) Console.ReadLine() End Sub End Module End Namespace
Die erste Ausgabe wird von der überschriebenen Methode erzeugt, die zweite Ausgabe von der überladenen Methode:
Start: 9 Argument: 9.8
Hinweis |
Die Überladung auf Basis der Anzahl der Typparameter gilt genauso für Datentypen. |
Ist ein Typparameter durch einen umschließenden Block festgelegt, wird er behandelt wie jeder andere Datentyp und kann auch zur Überladung genutzt werden. Das folgende Codefragment definiert eine Methode, die den Typparameter der Klasse verwendet, und eine Methode mit einem konkreten Datentyp. Auch wenn der Typparameter Art »zufällig« auch String sein kann, liegen zwei verschiedene Definitionen vor. In Test() werden die Methoden mit Objekten verschiedenen Typs aufgerufen.
'...\Datentypen\Generisch\Ueberladung.vb |
Option Strict On Namespace Datentypen Class Daten(Of Art) Overloads Shared Sub Druck(ByVal daten As Daten(Of Art)) Console.WriteLine("Allgemein") End Sub Overloads Shared Sub Druck(ByVal daten As Daten(Of String)) Console.WriteLine("Speziell") End Sub End Class Module Überladung Sub Test() Daten(Of String).Druck(New Daten(Of String)()) Daten(Of Integer).Druck(New Daten(Of Integer)()) Console.ReadLine() End Sub End Module End Namespace
Die genauer passende Methode wird jeweils gewählt. Es liegt eine Überladung für den konkreten Typ String vor, die, wie die erste Ausgabe zeigt, für String-Daten verwendet wird. Für alle anderen Typen wird die Methode mit dem Typparameter verwendet.
Speziell Allgemein
Verdecken
Da Typparameter ein Quelltextkonstrukt sind, kann ein Typparameter durch einen anderen nur im gleichen Quelltextblock abgeschattet werden, nicht durch Vererbung. Das folgende Codefragment zeigt die zweifache Verwendung des Typparameters Art: auf Klassenebene und für die in der Klasse enthaltene Methode. In Test() erfolgt ein Zugriff auf die Methode.
'...\Datentypen\Generisch\Verdecken.vb |
Option Strict On Namespace Datentypen Class Kontakt(Of Art As Structure) Shared Sub Start(Of Art)(ByVal wie As Art) Console.WriteLine("Kontaktart: {0}", wie.GetType().Name) End Sub End Class Module Verdecken Sub Test() Kontakt(Of Date).Start("Anruf") Console.ReadLine() End Sub End Module End Namespace
Der Typparameter der Klasse wird ignoriert, da er vom gleichnamigen Typparameter der Methode verdeckt wird. Wäre dem nicht so, würde die Structure-Einschränkung der Klasse nicht vom Typparameter der Methode erfüllt und der Compiler würde einen Fehler melden.
Kontaktart: String
Protected-Mitglieder
Wie bereits in obigem Unterabschnitt Typen erläutert wurde, sind Vererbungshierarchien generischer Klassen mit unterschiedlichen Typparametern komplett getrennt. Folgerichtig sind Zugriffe auf Mitglieder, die mit Protected auf die Vererbungshierarchie beschränkt sind, nur möglich, wenn identisch dieselben Typparameter in Eltern- und Kindklasse vorliegen. Dabei ist ein Typparameter grundsätzlich etwas anderes als ein konkreter Typ, auch wenn beide in einem Spezialfall gleich sein können.
Im folgenden Beispiel wird auf das mit Protected geschützte Feld der Elternklasse in der Kindklasse mit je einer Methode mit konkretem Typ Date und mit Typparameter Art zugegriffen. In der ersten Methode liegt der im vorigen Absatz beschriebene Fall vor, und sie ist auskommentiert, um eine Fehlermeldung des Compilers zu vermeiden. Die Methoden sind unterschiedlich benannt, um die Methode mit dem Typparameter auch mit einem Objekt vom Typ Date aufrufen zu können und nicht immer in der Methode mit Date zu landen. In Test() werden beide Methoden mit den gleichen Datentypen aufgerufen.
'...\Datentypen\Generisch\Verdecken.vb |
Option Strict On Namespace Datentypen Class Verarbeitung(Of Art) Protected daten As Art End Class Class Ausgabe(Of Art) : Inherits Verarbeitung(Of Art) Shared Sub Druck(ByVal eingabe As Ausgabe(Of Date)) Console.WriteLine("???") 'Console.WriteLine(eingabe.daten) 'Compilerfehler!! End Sub Shared Sub Ausdruck(ByVal eingabe As Ausgabe(Of Art)) Console.WriteLine(eingabe.daten) End Sub End Class Module Vererbungshierarchie Sub Test() Ausgabe(Of Date).Druck(New Ausgabe(Of Date)()) Ausgabe(Of Date).Ausdruck(New Ausgabe(Of Date)()) Console.ReadLine() End Sub End Module End Namespace
Wie die Ausgabe zeigt, werden beide Methoden korrekt aufgerufen:
??? 1/1/0001 12:00:00 AM
Im Rahmen von Typparametern ergibt sich noch eine kleine Besonderheit. Ein generischer Datentyp kombiniert die Sichtbarkeiten der Elemente, aus denen er sich zusammensetzt. Daher kann die Klasse Schnittmenge des folgenden Codefragments nur in Klassen verwendet werden, die sowohl ProtectedFriend ableiten als auch in derselben Anwendung liegen. Diese doppelte Einschränkung ist mit anderen Sprachmitteln von Visual Basic nicht definierbar, da Protected Friend bedeutet, dass der Zugriff entweder in einer Kindklasse oder von innerhalb der Anwendung erfolgen kann.
Class ProtectedFriend Friend Class Anwendung : End Class Protected Class Schnittmenge(Of T As Anwendung) : End Class End Class
Hinweis |
C# erlaubt Zugriffe auf Protected-Mitglieder unabhängig vom Typparameter. |
Schnittstellen (Interface)
Eine generische Schnittstelle stellt im Grunde eine ganze Anzahl von Schnittstellen zur Implementierung zur Verfügung. Jeder bei der Implementation angegebene konkrete Typ erfüllt einen Vertrag mit der Schnittstelle. Das geht so weit, dass selbst in einer Klasse dieselbe generische Schnittstelle durch verschiedene Signaturen implementiert werden kann.
Das folgende Beispiel definiert die generische Schnittstelle IFläche, die in der Klasse Mauer mit zwei verschiedenen Typen implementiert wird. In Test() werden zwei Schnittstellenreferenzen auf dasselbe Objekt definiert und in den Schreibbefehlen zur Ausgabe genutzt.
'...\Datentypen\Generisch\Schnittstellen.vb |
Option Strict On Namespace Datentypen Enum Farbe : Rot = 1 : Blau = 2 : Grün = 4 : End Enum Enum Form : Viereckig : Rund : End Enum Interface IFläche(Of Typ) Function Art() As Typ End Interface Class Mauer : Implements IFläche(Of Farbe), IFläche(Of Form) Function Ansicht() As Farbe Implements IFläche(Of Farbe).Art Return Farbe.Rot End Function Function Oberfläche() As Form Implements IFläche(Of Form).Art Return Form.Viereckig End Function End Class Module Schnittstellen Sub Test() Dim mauer As New Mauer() Dim farbe As IFläche(Of Farbe) = mauer Dim form As IFläche(Of Form) = mauer 'Dim fläche As IFläche(Of Object) = mauer 'Compilerfehler!! Console.WriteLine("Farbe {0}={1}", mauer.Ansicht(), farbe.Art()) Console.WriteLine("Form {0}={1}", mauer.Gestalt(), form.Art()) Console.ReadLine() End Sub End Module End Namespace
Die Zugriffe über die Objekt- und Schnittstellenreferenzen sind gleichwertig:
Farbe Rot=Rot Form Viereckig=Viereckig
Die Möglichkeiten der Vielfachimplementierung führen zu ein paar Stolperfallen, von denen das folgende Codefragment zwei enthält. Der erste Fehler entsteht dadurch, dass die Implementierung nicht mehr eindeutig ist, wenn der Datentyp Werkstatt(Of Meister) verwendet wird, da er mit dem konkreten Typ IHandwerker(Of Stift, Meister) konkurriert und die beiden nicht mehr zu trennen sind. Um diese Möglichkeit eines Problems zu vermeiden, verbietet der Compiler diese Art der Definition. Ähnlich gelagert ist der zweite Fall, in dem durch zwei gleiche konkrete Typen die Methode Auftrag der Schnittstelle doppelt vorhanden ist, da sowohl Aufsicht als auch Arbeiter vom Typ Geselle sind.
'...\Datentypen\Generisch\Überlappung.vb |
Option Strict On Namespace Datentypen Class Stift : End Class Class Geselle : Inherits Stift : End Class Class Meister : Inherits Geselle : End Class Interface IHandwerker(Of Arbeiter As Stift, Aufsicht As Geselle) Sub Auftrag(ByVal auf As Aufsicht) Sub Auftrag(ByVal arb As Arbeiter) Sub Reparatur(ByVal arb As Arbeiter, ByVal auf As Aufsicht) End Interface Class Werkstatt(Of Aufsicht As Geselle) Implements IHandwerker(Of Stift, Meister) 'Compilerfehler!! Implements IHandwerker(Of Stift, Aufsicht) ... End Class Class Werkstatt Inherits Werkstatt(Of Geselle) Implements IHandwerker(Of Geselle, Geselle) 'Compilerfehler!! ... End Class ... End Namespace
Analog zur Klassenvererbung muss auch die Schnittstellenvererbung die Einschränkungen in der Elternschnittstelle für Typparameter in der Kindklasse erfüllen. Anders als bei Klassen führt ein Abschatten mit Shadows nicht dazu, dass verdeckte Schnittstellenmitglieder der Elternklasse abgeschnitten sind, sondern sie müssen implementiert werden, wenn eine Klasse mit Implements die Schnittstellenimplementierung deklariert. Das folgende Beispiel zeigt diese beiden letzten Punkte, den ersten in den As-Klauseln von IAufräumen sowie ISäubern und den zweiten in der doppelten Implements-Deklaration der Methode Tun() in der Klasse Butler. Die Verwendung der Datentypen in der Methode Test() macht noch auf ein Problem aufmerksam. Analog zu den im Unterabschnitt Typen weiter oben beschriebenen Klassen legt jeder konkrete Typ eine neue Vererbungshierarchie fest, so dass die Zeile im Try-Block zu einem Laufzeitfehler führt, obwohl Profi sich von Laie ableitet.
'...\Datentypen\Generisch\Vererbung.vb |
Option Strict On Namespace Datentypen Class Laie : End Class Class Profi : Inherits Laie : End Class Interface IAufräumen(Of T As Laie) Sub Tun() End Interface Interface ISäubern(Of T As Profi) : Inherits IAufräumen(Of T) Shadows Sub Tun() End Interface Class Butler : Implements ISäubern(Of Profi) Sub Tun() Implements ISäubern(Of Profi).Tun, IAufräumen(Of Profi).Tun Console.Write("sauber+rein ") End Sub End Class Module Vererbung Sub Test() Dim b As New Butler() Dim s As ISäubern(Of Profi) = b Try Dim al As IAufräumen(Of Laie) = b Catch ex As Exception Console.WriteLine("Ausnahme: " & ex.Message) End Try Dim ap As IAufräumen(Of Profi) = b b.Tun() : s.Tun() : ap.Tun() Console.ReadLine() End Sub End Module End Namespace
Hinweis |
Die generischen und normalen Schnittstellen gemeinsamen Aspekte, wie zum Beispiel die Notwendigkeit, Standardwerte optionaler Parameter beizubehalten, werden hier nicht behandelt, sondern sind in Abschnitt 3.15, »Schnittstellen: Interface und Implements«, beschrieben. |
Funktionszeiger (Delegate)
Sowohl normale als auch generische Delegates können auf generische oder normale Methoden zeigen. Wichtig ist, dass die Datentypen der Delegates zu denen der Methoden passen. Wenn für generische Methoden bei der Verknüpfung des Delegates mit einer generischen Methode keine Typparameter angegeben werden, kann der Compiler die Typen meist automatisch ermitteln, vorausgesetzt, die Compileroption Infer ist auf On gesetzt. Dies geht so weit, dass selbst der Typ der Rückgabe betrachtet wird, auch wenn dieser gar nicht zur Signatur der Methode gehört.
Im folgenden Codefragment werden ein nichtgenerisches Delegate GeldVerdienen und ein generische Delegate Lehren definiert. In der Methode Test() wird das nichtgenerische Delegate GeldVerdienen mit der generischen Methode Arbeiten() verknüpft, wobei der Compiler die Typisierung aufgrund des Rückgabetyps der Methode selbst ermittelt. Als Zweites zeigt das generische Delegate Lehren auf die nichtgenerische Methode Mathe(). Dann folgt der »normale« Fall einer Verbindung des generischen Delegates Lehren mit der generischen Methode Vertretung(). Schließlich werden die Delegates zur Kontrolle aufgerufen.
'...\Datentypen\Generisch\Infer.vb |
Option Strict On Namespace Datentypen Delegate Function GeldVerdienen() As Double Delegate Sub Lehren(Of Stoff)(ByVal was As Stoff) Module Ableiten Function Arbeiten(Of Ergebnis)() As Ergebnis Return Arbeiten 'Standardwert "Null" End Function Sub Mathe(ByVal was As Integer) Console.WriteLine("Diskussion der Zahl {0}", was) End Sub Sub Vertretung(Of Wann)(ByVal w As Wann) Console.WriteLine("Vertretung um {0}", w) End Sub Sub Test() Dim schuften As GeldVerdienen = AddressOf Arbeiten Dim reden As Lehren(Of Integer) = AddressOf Mathe Dim beschäftigen As Lehren(Of Date) = AddressOf Vertretung(Of Date) Console.WriteLine("Verdienst: {0}", schuften.Invoke()) reden.Invoke(Integer.MinValue) beschäftigen.Invoke(Now) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Verwendung der Methoden durch die Delegates:
Verdienst: 0 Diskussion der Zahl –2147483648 Vertretung um 3/14/2008 3:21:00 PM
4.4.4 Sichtbarkeit
Welches Konstrukt auch immer einen Datentyp verwendet, es muss mindestens so sichtbar sein wie der verwendete Datentyp. Generische Datentypen bilden da keine Ausnahme. Ungewohnt ist lediglich, dass mehr Datentypen als sonst im Einsatz sind, so dass es leichter passieren kann, ein Problem zu übersehen. Das folgende Codefragment zeigt zwei Methoden, die beide eine Fehlermeldung des Compilers verursachen, und zwar durch einen Datentyp für einen Typparameter, der weniger sichtbar ist als die öffentliche Methode.
'...\Datentypen\Generisch\Sichtbarkeit.vb |
Option Strict On Namespace Datentypen Public Class Ware(Of Typ, Menge) Public Was As Typ Public Wieviel As Menge End Class Friend Class Boot : End Class Public Module Sichtbarkeit Friend Class Anwendung : End Class Sub Umgehung(Of P As Anwendung)(ByVal arg As P) 'Compilerfehler!! End Sub Function Umgehung(Of W As Ware(Of Boot, Short))() As W 'Compilerfehler!! End Function End Module End Namespace
Hinweis |
Bitte beachten Sie, dass Datentypen auch ohne explizite Angabe immer eine Sichtbarkeit haben. |
4.4.5 Bindung
Typparameter generischer Datentypen legen nicht nur eine Vererbungshierarchie fest, sondern auch den Gültigkeitsbereich von mit Shared klassengebundenen Typmitgliedern. Verschiedene konkrete Typen für einen Typparameter erzeugen Mitglieder, die unabhängig voneinander sind. Das nächste Codefragment definiert einen Konstruktor Shared Sub New() für die Klasse und ein Feld Anzahl, das im Objektkonstruktor Sub New() inkrementiert wird. In Test() werden mehrere Objekte verschiedener konkreter Typen erzeugt und wird die Variable Anzahl für jeden der Typen ausgegeben.
'...\Datentypen\Generisch\Bindung.vb |
Option Strict On Namespace Datentypen Class Hund : End Class Class Katze : End Class Class Tier(Of Typ) Shared Sub New() Console.WriteLine("Klasseninitialisierer {0}", GetType(Typ).Name) End Sub Friend Shared Anzahl As Short Sub New() Anzahl += 1 End Sub End Class Module Bindung Sub Test() Dim hunde() As Tier(Of Hund) = New Tier(Of Hund)() _ {New Tier(Of Hund), New Tier(Of Hund)} Dim katzen() As Tier(Of Katze) = New Tier(Of Katze)() _ {New Tier(Of Katze), New Tier(Of Katze), New Tier(Of Katze)} Console.WriteLine("{0} Hunde", Tier(Of Hund).Anzahl) Console.WriteLine("{0} Katzen", Tier(Of Katze).Anzahl) Console.ReadLine() End Sub End Module End Namespace
Sowohl der Klassenkonstruktor als auch die klassengebundene Variable sind für jeden der Typen unterschiedlich.
Klasseninitialisierer Hund Klasseninitialisierer Katze 2 Hunde 3 Katzen
4.4.6 Rekursive Typen
Typparameter können in ihren Einschränkungen auch sich selbst enthalten. Dies ist in solchen Fällen sinnvoll, wenn Methoden nur dann sinnvoll arbeiten können, wenn ihre Parameter zum selben Typ gehören. Durch die Formulierung der Typisierung im Typparameter kann der Compiler bereits eine typfremde Verwendung unterbinden. Das nächste Codefragment zeigt eine Schnittstelle zum Vergleich von Objekten desselben Typs. Der Schnittstellentyp inklusive Typparameter taucht in der As-Klausel auf. Die Implementation der Schnittstelle in der Klasse Wort verwendet den Vergleich von Zeichenkettenlängen als Kriterium der Gleichheit. In Test() wird die Methode über eine Schnittstellenreferenz auf IGleich(Of Wort) getestet.
'...\Datentypen\Generisch\Rekursiv.vb |
Option Strict On Namespace Datentypen Interface IGleich(Of Typ As IGleich(Of Typ)) Function Gleich(ByVal objekt As Typ) As Boolean End Interface Class Wort : Implements IGleich(Of Wort) Private zeichen As String Sub New(ByVal zeichen As String) Me.zeichen = zeichen End Sub Public Function Gleichartig(ByVal w As Wort) As Boolean _ Implements IGleich(Of Wort).Gleich Return zeichen.Length = w.zeichen.Length End Function End Class Module Rekursiv Sub Test() Dim r As IGleich(Of Wort) = New Wort("irgendetwas") Console.WriteLine("""Gleich"" {0}", r.Gleich(New Wort("gleich lang"))) Console.WriteLine("""Gleich"" {0}", r.Gleich(New Wort("anders"))) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Arbeitsweise:
"Gleich" True "Gleich" False
4.4.7 Generische Typen in .NET
In .NET begegnen Ihnen generische Typen wahrscheinlich als Erstes im Gewand von Datensammlungen, englisch Collections. Aufgrund des großen Umfangs werden sie in Kapitel 6, »Collections und LINQ«, behandelt. Es gibt noch sehr viel mehr Stellen, an denen generische Typen zum Einsatz kommen. Exemplarisch greife ich hier nur zwei Arten heraus: Vergleiche und Funktionsobjekte.
Vergleiche
Die Vielfalt in der Objektorientierung macht es unmöglich, ohne weitere Kenntnisse Objekte zu vergleichen. Daher stützen sich Vergleichsfunktionen auf typspezifische Schnittstellen. Die Allgemeinheit bleibt durch die Verwendung generischer Typen gewahrt.
System-+IComparable(Of T) +IEquatable(Of T) System.Collections.Generic-+IComparer(Of T) +IEqualityComparer(Of T)
Die Nutzung der Schnittstellen ist sehr einfach. Dazu müssen Sie in Ihren Klassen Zustände identifizieren, die vom Charakter her vergleichbar sind. In der Rhetorik wird dies als tertium comparationis bezeichnet, übersetzt »ein Drittes zum Vergleich«. Hier sind die ersten beiden Zwei die verschiedenen Klassen, das Dritte der oder die zu vergleichenden Zustände. Damit lassen sich beliebige Objekte miteinander vergleichen. Was Sie für den Vergleich heranziehen, hängt von der Problemstellung ab. Das folgende Beispiel zeigt den Vergleich eines Apfeldurchmessers mit einem Birnengewicht. Inhaltlich ist das Blödsinn, es zeigt aber die Tragweite des Konzepts. Laut der Dokumentation zur Schnittstelle IComparable muss die Methode CompareTo() einen negativen Wert liefern, wenn das Objekt »kleiner« als das Vergleichsobjekt ist, und einen positiven, wenn es »größer« ist. Bei Gleichheit wird die Zahl 0 zurückgegeben.
'...\Datentypen\Generisch\Vergleich.vb |
Option Strict On Namespace Datentypen Class Apfel : Implements IComparable(Of Birne) Friend Durchmesser As Double Sub New(ByVal d As Double) Durchmesser = d End Sub Function CompareTo(ByVal b As Birne) As Integer _ Implements IComparable(Of Birne).CompareTo If(b Is Nothing, 1, Durchmesser.CompareTo(b.Gewicht)) End Function End Class Class Birne : Implements IComparable(Of Apfel) Friend Gewicht As Double Sub New(ByVal g As Double) Gewicht = g End Sub Function CompareTo(ByVal a As Apfel) As Integer _ Implements IComparable(Of Apfel).CompareTo Return If(a Is Nothing, 1, Gewicht.CompareTo(a.Durchmesser)) End Function End Class Module Vergleich Sub Test() Dim a As New Apfel(80), b As New Birne(75) Console.WriteLine("Apfel "">"" Birne: {0}", a.CompareTo(b) > 0) Console.WriteLine("Birne "">"" Apfel: {0}", b.CompareTo(a) > 0) Console.ReadLine() End Sub End Module End Namespace
Nun lassen sich sogar Äpfel und Birnen vergleichen:
Apfel ">" Birne: True Birne ">" Apfel: False
Auf die Gleichheit zweier Objekte gehe ich in Abschnitt 10.1, »Object«, ein.
Funktionsobjekte
Die Vererbungslinie Object R ActivationContext R Delegate R MulticastDelegate im Namensraum System hat einige interessante Delegates. Prozeduren ohne und Funktionen mit Rückgabe werden mit null bis vier Parametern durch folgende Typen repräsentiert (optionale Teile stehen in eckigen Klammern):
Action[(Of T1[,T2[,T3[,T4]]])] Func(Of [T1,[T2,[T3,[T4,]]]] TErgebnis)
Dadurch müssen Sie dafür nicht selbst Delegates definieren. Für Ereignisse steht das Delegate EventHandler(Of TEventArgs As {EventArgs}) zur Verfügung, zum Vergleich Comparison (Of T) und Predicate(Of T) sowie Converter(Of TInput,TOutput) zur Typumwandlung.
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.