Generics in .NET Deel II: Co- en contravariantie

Dit is een reeds eerder gepubliceerde post van de vorige VBcentral website door Sander Rossel.

In mijn vorige blog over Generics hebben we gezien hoe we zelf een klasse of functie kunnen maken die gebruik maakt van Generics. Er is echter nog een aspect van Generics dat ik nog niet heb besproken, co- en contravariantie. Deze techniek is beschikbaar in VB sinds .NET 4.0. 
Covariantie en contravariantie hebben te maken met overerving, of Inheritance. Inheritance houdt in dat een klasse een andere klasse 'overerft' en als het ware een verlengstuk van die klasse wordt. 
Een voorbeeldje:

 
Public Class Persoon 
 
   Public Property Naam As String 
 
End Class 
 
Public Class Werknemer 
 
   Inherits Persoon 
 
   Public Property Salaris As Decimal 
 
End Class 
 
Dim werknemer As New Werknemer 
werknemer.Naam = "Sander" 
werknemer.Salaris = 125000 ' Was het maar zo'n feest! ;) 


U ziet dat de Property Naam niet in Werknemer staat gedefiniëerd, maar omdat Werknemer 'Inherit' van Persoon heeft de Werknemer automatisch toch de Property Naam. Dit geldt voor alle functies en properties van een afgeleide klasse (afgeleide wil hier zeggen 'Werknemer is afgeleid van Persoon'. 
Omdat Inheritance een 'is een'-relatie beschrijft kunnen wij zeggen "een Werknemer is een Persoon". Zodoende kunnen wij de volgende code schrijven.

 
' Een Werknemer is een Persoon. 
' Het volgende is dus toegestaan. 
Dim persoon As Persoon = New Werknemer 
 
' Een Persoon is niet automatisch een Werknemer. 
' Het volgende mag dus niet. 
Dim werknemer As Werknemer = New Persoon 


Nu zult u denken dat het volgende dan ook wel toegestaan zal zijn.

 
Dim personen As List(Of Persoon) = New List(Of Werknemer) 


Het zou toch niet uit moeten maken of u een lijst met Werknemers heeft waar de compiler een lijstje met Personen verwacht? Een Werknemer is immers een Persoon! 
Helaas werkt dit niet zo makkelijk met Generics. Het 'casten' of 'converten' van Generics werkt namelijk enkel met Interfaces (hierover zo meer). Bovendien is het van belang dat een bepaalde Generic enkel als input voor functies óf enkel als output voor functies en properties wordt gebruikt. 
In het geval van een List(Of T) is T de output van Property Item, maar de input van, bijvoorbeeld Sub Add. In het geval van de List(Of T) het dus niet mogelijk. 
Waarom? Stelt u zich eens voor dat u als output van een functie een Persoon verwacht. Zou het dan een ramp zijn als u een Werknemer terug krijgt? Nee, want een Werknemer is een Persoon. Andersom zou het echter wel heel vervelend zijn, want u verwacht dan een Werknemer met een Salaris, maar u krijgt een Persoon zonder Salaris! Dit kan natuurlijk niet. Daarom kunt u voor output parameters enkel types van Persoon of hoger in de Inheritance hiërarchie gebruiken, maar nooit lager. 
Stelt u zich nu eens voor dat u een functie heeft die een Werknemer als input heeft. De Werknemer wordt in de klasse zelf gebruikt, maar nooit als output gepresenteerd. Vergeet niet dat een generieke klasse geen idee heeft van het type van T. Werknemer.Salaris zal dus nooit in deze functie gebruikt kunnen worden, tenzij u een restrictie op T heeft zitten waarbij T altijd van het type Werknemer moet zijn. Wij gaan er echter van uit dat dit niet het geval is. Het maakt nu dus niet meer uit of u een Werknemer aan de functie geeft of een Persoon. Intern zal de klasse de Werknemer dan namelijk gewoon als een Person behandelen (wat mogelijk is omdat een Werknemer een Persoon is). 
Nogmaals, variantie werkt enkel met Interfaces. List(Of T) Implementeert de Interface IList(Of T). In IList(Of T) is de T helaas zowel in- als output. Deze Interface komt dus niet in aanmerking voor variantie. De IList(Of T) is uiteindelijk een afgeleide van IEnumerable(Of T). IEnumerable(Of T) kent geen input. Dit is dus wél een geschikte kandidaat voor variantie. Als een Interface T enkel gebruikt als output én variantie ondersteunt dan kan hier covariantie toegepast worden. 
Dit klinkt allemaal heel ingewikkeld, dus laten we naar wat code gaan kijken.

 
' Het volgende mag niet, al zou u het wel verwachten. 
' Vergeet niet dat Persoon hier zowel een input als output parameter is. 
Dim personen As IList(Of Persoon) = New List(Of Werknemer) 
 
' IEnumerable(Of T) is de basis voor iedere collectie. 
' IEnumerable(Of T) gebruikt T enkel als output. 
Dim personen As IEnumerable(Of Persoon) = New List(Of Werknemer) 
 
' Helaas kent IEnumerable(Of T) geen Add functionaliteit. 
' Om een Werknemer aan de lijst toe te voegen zullen we dus moeten casten. 
' Let op, we casten naar List(Of Werknemer), niet List(Of Persoon)! 
DirectCast(personen, List(Of Werknemer)).Add(New Werknemer) 
 
For Each p As Persoon in personen 
 
   ' p heeft dus geen salaris, hiervoor zou weer gecast moeten worden naar Werknemer. 
   Console.WriteLine(p.Naam) 
 
Next 
 
' Het volgende mag niet! 
Dim werknemers As IEnumerable(Of Werknemer) = New List(Of Persoon) 


U ziet dat dit redelijk intuïtief aanvoelt. U kent een lijstje met Werknemers toe aan een collectie met Personen. Aangezien een Werknemer een Persoon is voelt dit goed. 
Het omgekeerde, contravariantie, voelt een stuk minder intuïtief aan. Eigenlijk zegt het woord 'contra' het al. Dit is niet logisch. 
Bij contravariantie is T enkel input. We kijken hier dus naar een omgekeerde situatie. 
De Interface IComparer(Of T) gebruikt T enkel als input en ondersteunt contravariantie. De Comparer(Of T) implementeert deze Interface. Met deze klasse is echter iets geks aan de hand. We kunnen deze namelijk niet op normale manier instantiëren. Dit gebeurt via de Shared Property Default. Dit maakt voor het voorbeeld echter niks uit.

 
' Declareer een Comparer(Of Persoon) en ken de variabele een waarde toe. 
Dim persoonComparer As Comparer(Of Persoon) = Comparer(Of Persoon).Default 
 
' Let op, hier maken we gebruik van de Interface! 
Dim werknemerComparer As IComparer(Of Werknemer) = persoonComparer 
 
' Het volgende mag dus niet! 
Dim werknemerComparer As Comparer(Of Werknemer) = Comparer(Of Werknemer).Default 
Dim persoonComparer As IComparer(Of Persoon) = werknemerComparer 


U ziet dat dit stukje code niet lijkt te kloppen. We hebben een Comparer(Of Persoon), die geen Salaris heeft, en we kennen het toe aan een Comparer(Of Werknemer), die wel een Salaris heeft! Wat gebeurt er dan nu als in de klasse het Salaris wordt opgevraagd? Dat zal niet gebeuren. De Comparer klasse heeft namelijk geen weet van de Salaris Property (T heeft geen Salaris Property). Wat de klasse wel weet is dat T een Werknemer is. Als we T veranderen van Werknemer naar Persoon dan weten we dus dat dit voor T niks uitmaakt want een Werknemer is een Persoon. Het zal voortaan al zijn Werknemers simpelweg behandelen als een Persoon in plaats van Werknemer. 
Nog steeds een vaag verhaal? Geen nood. We zullen zelf eens twee Interfaces maken en implementeren die ook gebruik maken van co- en contravariantie. 
Allereerst covariantie. We gaan een Interface maken die een ReadOnly Property heeft die een T terug geeft. T is dus enkel output. Om de Interface covariant te maken geven we aan dat T output is met het keyword Out.

 
Public Interface ITest(Of Out T) 
 
   ReadOnly Property EenObject As T 
 
End Interface 
 
' Een simpele implementatie. 
Public Class Test(Of T) 
 
   Implements ITest(Of T) 
 
   Private _eenObject As T 
 
   ' Hier is T input, maar dit komt niet terug in de Interface. 
   Public Sub New(ByVal eenObject As T) 
 
      _eenObject = eenObject 
 
   End Sub 
 
   Public ReadOnly Property EenObject As T Implements ITest(Of T).EenObject 
      Get 
         Return _eenObject 
      End Get 
   End Property 
 
End Class 
 
' Maak persoon Sander aan. 
Dim persoon As New Persoon 
persoon.Naam = "Sander" 
 
' Maak een nieuw Test object aan en geef persoon Sander mee aan de constructor. 
Dim test1 As New Test(Of Persoon)(persoon) 
 
' Dit geeft de persoon Sander weer terug. 
Dim nogEenPersoon As Persoon = test1.EenObject 
 
' Maak nu werknemer Sander aan. 
Dim werknemer As New Werknemer 
werknemer.Naam = "Sander" 
werknemer.Salaris = 150000 
 
' Maak een ITest(Of Persoon), maar ken het toe aan een Werknemer. 
Dim test2 As ITest(Of Persoon) = New Test(Of Werknemer)(werknemer) 
 
' Dit geeft de werknemer Sander terug, maar dat is geen probleem! 
Dim werknemerPersoon As Persoon = test2.EenObject 


Allicht begint het u nu te dagen. We zullen nog een voorbeeld bekijken, maar dit keer met contravariantie. In plaats van het keyword Out gebruiken we nu In om aan te geven dat T enkel als input gebruikt wordt.

 
Public Interface ITest(Of In T) 
 
   Sub DoeIets(ByVal eenObject As T) 
 
End Interface 
 
' Een simpele implementatie. 
' We houden hier een lijstje bij met alle input. 
Public Class Test(Of T) 
 
   Implements ITest(Of T) 
 
   Private _eenLijstje As New List(Of T) 
 
   ' Hier is T wederom output. Dit komt niet terug in de Interface. 
   Public ReadOnly Property EenLijstje As List(Of T) 
      Get 
         Return _eenLijstje 
      End Get 
   End Property 
 
   Public Sub DoeIets(ByVal eenObject As T) Implements ITest(Of T).DoeIets 
 
      _eenLijstje.Add(eenObject) 
 
   End Sub 
 
End Class 
 
' Maak een persoon Sander. 
Dim persoon As New Persoon 
persoon.Naam = "Sander" 
 
' Maak een nieuw Test object aan en geef persoon Sander als input aan de functie. 
Dim test1 As New Test(Of Persoon) 
 
test1.DoeIets(persoon) 
 
' Het volgende gaat altijd goed omdat alle werknemers die voortaan in test2 gaan ook personen zijn. 
Dim test2 As ITest(Of Werknemer) = test1 
 
' Maak nu een werknemer Sander. 
Dim werknemer As New Werknemer 
werknemer.Naam = "Sander" 
werknemer.Salaris = 150000 
 
test2.DoeIets(werknemer) 
 
' Het volgende gaat mis omdat test2 Werknemers verwacht, maar nu dus ook niet-werknemers als input kan krijgen! 
' Het feit dat test2 al een persoon bevat is op dit punt irrelevant. 
Dim test3 As ITest(Of Persoon) = test2 
 
' Wat wel mogelijk is omdat we een List(Of Persoon) hebben toegekend aan de IList(Of Werknemer) 
Dim test4 As Test(Of Persoon) = DirectCast(test2, Test(Of Persoon)) 


Wat we hier zien bij contravariantie is dus exact het tegenovergestelde van covariantie! We kennen een Persoon toe aan de afgeleide Werknemer, of zo lijkt het ten minste, want uiteindelijk is deze aanpak helemaal type-safe omdat intern alle Werknemer input als Persoon behandelt blijft worden. Er kan dus niks mis gaan!

De materie die hier besproken is is niet makkelijk. Laat het bezinken en experimenteer er wat mee. Als het kwartje valt is het best logisch. Succes!

Nieuwsbrief

Blijf op de hoogte van alles wat op VBcentral gebeurd. Meld je nu aan voor onze nieuwsbrief! »

Over ons

Wij zijn gek op het Microsoft .NET ontwikkelplatform en haar ontwikkeltalen, maar we hebben een sterke voorkeur voor Visual Basic .NET! »

Neem contact op

VBcentral.nl
Bataafseweg 20
7101 PA Winterswijk
Nederland
+31 (543) 538 388
info@vbcentral.nl