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 [, ...]) |
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) |
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 | _ |
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.