Zie Scherp Scherper - Hoofdstuk 16
Na dit hoofdstuk kan je:
uitleggen wat polymorfisme is en hoe late binding werkt
child-objecten in een variabele van het parent-type bewaren (upcasting)
lijsten van een basistype vullen met allerlei child-objecten
polymorfisme inzetten om code eenvoudiger te maken
de is- en as-keywords en pattern matching gebruiken
Equals netjes overriden
Polymorfisme = poly (meerdere) + morfisme (vormen): meerdere vormen.
Het laat toe om child-objecten te behandelen als objecten van hun parent-klasse
Het zorgt dat virtual/override echt werkt
Ik ben Tim. Maar ook: een Docent is een Werknemer, en een Werknemer is een Mens.
Bij de bakker ben ik gewoon een mens
Bij AP een werknemer
In de les een docent
Steeds dezelfde Tim, maar in welke rol je me ziet, hangt af van de context. Dat is polymorfisme.
De variabele is van het type Dier, maar elk roept zijn eigen MaakGeluid op.
Waarom voert a.MaakGeluid() toch de Varken-versie uit?
C# kiest pas tijdens de uitvoer welke override draait, op basis van het echte object in de heap
Dat heet late binding (dynamic dispatch)
Het werkt enkel omdat de methode virtual of abstract is
Een child toewijzen aan een parent-variabele (upcasting) gaat automatisch, tot zelfs:
Belangrijk
De compiler kijkt naar het type van de variabele, niet naar het echte object. Via mijnObject kan je dus enkel wat System.Object kent. Terug naar Varken (downcasting) doe je veilig met is en as.
Een hierarchie kan diep gaan:
Dennenboom
Naaldboom
Boom
Plant
LevendWezen
Een Dennenboom kan je dus zien als een Naaldboom, een Boom, een Plant of een LevendWezen: upcasting werkt naar elke voorouder.
Een lijst van het basistype, gevuld met child-objecten:
Objecten als één geheel behandelen, zonder dat ze exact hetzelfde type hoeven te zijn.
Polymorfisme werkt enkel mooi als een child zich echt kan gedragen als zijn parent (het Liskov substitution principle).
Het klassieke tegenvoorbeeld: een Vierkant die overerft van Rechthoek klopt taalkundig, maar geeft brokkelige code.
Kan elk child overal de plaats van zijn parent innemen?
internal class EersteMinister
{
public MinisterVanMilieu Jansens { get; set; } = new MinisterVanMilieu();
public MinisterBZ Ganzeweel { get; set; } = new MinisterBZ();
public void Regeer()
{
Jansens.VerhoogBosSubsidies();
Jansens.OpenOnderzoek();
Ganzeweel.VervangAmbassadeur();
// ... de premier moet ELK departement van binnen kennen
}
}De EersteMinister weet veel te veel: dat vloekt tegen abstractie.
Elke minister moet Adviseer overriden.
De premier hoeft geen enkel departement nog van binnen te kennen. Wie zei dat regeren moeilijk was?
is en as keywordsis test of een object van een bepaald type is, en geeft een bool:
as probeert te converteren; lukt het niet, dan krijg je null in plaats van een crash:
Belangrijk
as werkt enkel op reference types (objecten en interfaces), niet op value types als int.
Tip
Vuistregel: wil je het object daarna gebruiken, kies as; wil je enkel weten of iets van een type is, kies is.
Sinds C# 7 doe je de check én de omzetting in één stap:
Tip
In dezelfde geest: if (jos is not null) is de modernere variant van != null. Je ziet deze vorm overal in moderne C#.
Met is schrijf je een veilige Equals (geen crash bij een verkeerd type):
Belangrijk
Wie Equals overridet, override ook GetHashCode (met HashCode.Combine), anders haperen Dictionary en HashSet.
== vergelijkt bij objecten standaard de referenties: twee aparte objecten met dezelfde inhoud zijn dus niet gelijk
Equals Door Equals te overriden bepaal jij gelijkheid op basis van de inhoud
Tip
Sinds C# 9 regelen record-types dit automatisch: public record Student(string Voornaam, int Geboortejaar); genereert zelf Equals, GetHashCode én ToString. We doen het hier met de hand om te begrijpen wat eronder zit.
De wapens uit H15 worden nu polymorf: elk type onderhoudt zich anders.
Tip
De Held roept gewoon wapen.Maintain() aan voor elk wapen in zijn handen. Late binding kiest de juiste versie (Zwaard, Boog of Staf die mana verdubbelt).
Polymorfisme behandelt child-objecten als hun parent; late binding kiest de juiste override
Upcasting gaat vanzelf; voor downcasting gebruik je is en as
Een List<Basistype> met child-objecten maakt code flexibel en kort (de premier!)
is toetst een type, as zet veilig om (null bij mislukking), pattern matching doet beide
Override Equals voor vergelijking op inhoud, en vergeet GetHashCode niet
Tip
Volgende halte: interfaces, contracten die klassen beloven na te komen.
Kennisclips
Hoe doen andere programmeertalen hetzelfde? En een korte zoek-de-fout, om zo kritisch mogelijk naar code te leren kijken.
C# heeft een gemeenschappelijke parent plus virtual/override nodig. Python doet het via duck typing: heeft het object de methode, dan werkt het gewoon, zonder gedeelde basisklasse.
Bij Python merk je pas tijdens het uitvoeren of een object de methode echt heeft; C# controleert dat al bij het compileren.
Dezelfde vraag, andere notatie. In Python is het een functie:
In JavaScript een operator:
Zelf bepalen wanneer objecten gelijk zijn, doe je via een vaste methode. Python override __eq__ (zit achter ==):
Java lijkt op C#: override equals (en hashCode):
In JavaScript kan === niet overschreven worden: daar schrijf je zelf een functie zijnGelijk(a, b).
Onderstaande C#-code wil dat elk dier zijn eigen geluid maakt, maar bij de uitvoer roept élk dier “Een dier maakt geluid”. Wat loopt er mis?
Opmerking
De methode MaakGeluid in Dier is niet virtual (of abstract), dus de override in Varken kan niet kicken in. Eigenlijk zou deze code zelfs niet compileren: zonder virtual mag je niet override’n. Maak MaakGeluid in de parent public virtual string MaakGeluid() (of abstract), dan zorgt late binding ervoor dat de Varken-versie draait.
Een handige samenvattende poster (Opgelet: gemaakt m.b.v. ChatGPT Image Gen 2).