Gevorderde overervingsconcepten

C# houdt van objecten. De hele taal is letterlijk opgebouwd om maximaal het object georiënteerd programmeren te omarmen. Van zodra je een nieuw project aanmaakt kan je niet naast de internal class Program zien. Hoe meer je C# en de bestaande bibliotheken bekijkt, hoe duidelijker dit wordt. Alles is een klasse.

Maar dan stelt zich natuurlijk de vraag: staat er nog iets boven alle klassen die wij aan het maken zijn? Is er misschien een soort oer-klasse waar alle klassen van overerven?

De vraag stellen is ze beantwoorden! Er is effectief een oer-klasse, genaamd de System.Object-klasse, waar alles en iedereen in C# van moet overerven.

System.Object

Alle klassen in C# zijn afstammelingen van de System.Object klasse. Zowel de bestaande ingebouwde klassen zoals Random en Console. Maar ook klassen die je zelf maakt erven over van System.Object. En ja, zelfs de bestaande valuetype datatypes zoals int en bool zijn verre afstammelingen van System.Object.

Enkele voorbeelden. Merk op dat er véél meer ingebouwde klassen in .NET zitten dan degene die ik hier toon.

Indien je een klasse schrijft zonder een expliciete parent dan zal deze steeds System.Object als rechtstreekse parent hebben. Ook afgeleide klassen stammen dus uiteindelijk af van System.Object. Concreet wil dit zeggen dat alle klassen System.Object-klassen zijn en dus ook de bijhorende functionaliteit ervan hebben.

Om de klasse Object niet te verwarren met het concept "object" zullen we hier steeds praten over System.Object.

Impliciete overerving

Wanneer je een klasse Student aanmaakt als volgt: class Student{ }. Dan gebeurt er een zogenaamde impliciete overerving van System.Object. Er staat dus eigenlijk:

internal class Student: System.Object
{  }

Wat je trouwens ook expliciet zelf mag schrijven, dat maakt niet uit. Maar van zodra je een klasse schrijft die nergens expliciet van overerft, dan zal deze automatisch van System.Object overerven.1:

1

Je kan in de .NET documentatie altijd opzoeken waar een klasse van overerft. De Type klasse bijvoorbeeld erft finaal ook van System.Object over. Eerst erft Type over van de MemberInfo klasse, die op zijn beurt overerft van de oer-klasse.

Hoe ziet System.Object er uit?

Wanneer je een lege klasse maakt dan zal je misschien al gezien hebben dat instanties van deze nieuwe klasse reeds 4 methoden ingebouwd hebben, dit zijn uiteraard de methoden die in de System.Object klasse staan gedefiniëerd:

MethodeBeschrijving
Equals()Gebruikt om te ontdekken of twee instanties gelijk zijn.
GetHashCode()Geeft een unieke hash terug van het object; nuttig om o.a. te sorteren.
GetType()Geeft het datatype (de klasse) van het object terug.
ToString()Geeft een string terug die het object voorstelt.

Deze methoden zijn redelijk nutteloos in het begin. Enkel door ze zelf te overriden zullen ze hun nut bewijzen. Uiteraard kan je de de methoden testen om te zien wat er gebeurt.

GetType()

Stel dat je een klasse Student hebt gemaakt in je project. Je kan dan op een object van deze klasse de GetType()-methode aanroepen om te weten wat het type van dit object is:

Student stud1 = new Student();
Console.WriteLine(stud1.GetType());

Dit zal als uitvoer de namespace gevolgd door het type van het object op het scherm geven . Als je project bijvoorbeeld StudentManager heet (en je namespace dus vermoedelijk ook) dan zal er op het scherm verschijnen: StudentManager.Student.

Wil je enkel het type zonder namespace dan is het nuttig te beseffen dat GetType() eigenlijk een object teruggeeft van het type Type met meerdere eigenschappen, waaronder Name. Volgende code zal enkel Student op het scherm tonen:

Student stud1 = new Student();
Console.WriteLine(stud1.GetType().Name);

ToString(): het werkpaardje van System.Object

Deze methode vind ik het nuttigst. Wanneer je schrijft:

Console.WriteLine(stud1);

Wordt er eigenlijk een impliciete aanroep naar ToString gedaan. Er staat dus eigenlijk:

Console.WriteLine(stud1.ToString());

Op het scherm verschijnt dan StudentManager.Student. Waarom? Wel, de methode ToString() wordt in System.Object() ongeveer als volgt beschreven:

public virtual string ToString()
{ 
    return GetType(); 
}

Merk twee zaken op:

  1. GetType() wordt aangeroepen en die output krijg je dus terug.
  2. De methode is virtual gedefinieerd.

Alle 4 methoden in System.Object zijn virtual, en je kan deze dus override'n!

Nu komen we tot het hart van deze methoden. Aangezien ze alle 4 virtual zijn, kunnen we de werking ervan naar onze hand zetten in onze eigen klassen. Aardig wat .NET bibliotheken rekenen er namelijk op dat je deze methoden op de juiste manier hebt aangepast, zodat ook jouw nieuwe klassen perfect kunnen samenwerken met deze bibliotheken. Een eerste voorbeeld hiervan toonde ik net: de Console.WriteLine methode gebruikt van iedere parameter dat je er aan meegeeft de ToString-methode om de parameter op het scherm als string te tonen.

ToString() overriden

Het zou natuurlijk fijner zijn dat de ToString()-methode van onze student nuttigere info teruggeeft.

Stel dat we de Voornaam gevolgd door de Geboortejaar (ook een autoprop) willen terugkrijgen. We kunnen dat eenvoudig verkrijgen door ToString() te overriden:

internal class Student
{
    public int Geboortejaar {get;set;}
    public string Voornaam {get;set;}
    public override string ToString()
    {
        return $"{Voornaam} ({Geboortejaar})";
    }
}

Wanneer je nu Console.WriteLine(stud1); - gelet dat hij de properties Voornaam en Geboortejaar heeft - zou schrijven dan wordt je output: Tim Dams (1981).

Een extra handigheidje van ToString is dat deze methode wordt gebruikt tijdens het debuggen om je objecten samen te vatten in het watch-venster.

De Equals() methode

Ook deze methode kan je overriden om twee objecten met elkaar te vergelijken:

if(stud1.Equals(stud2))

De Equals()-methode heeft als signatuur: public virtual bool Equals(Object o) Twee objecten zijn gelijk voor .NET als aan volgende afspraken wordt voldaan:

  • Het moet false teruggeven indien de parameter o null is.
  • Het moet true teruggeven indien je het object met zichzelf vergelijkt (bv. stud1.Equals(stud1)).
  • Het mag enkel true teruggeven als zowel stud1.Equals(stud2); als stud2.Equals(stud1); waar zijn.
  • Indien stud1.Equals(stud2) true teruggeeft en stud1.Equals(stud3) ook true is, dan moet stud2.Equals(stud3) ook true zijn.

Equals() overriden

Het is echter aan de maker van de klasse om te beslissen wanneer 2 objecten van een zelfde type gelijk zijn. Het is dus niet zo dat iedere waarde van een instantievariabele bijvoorbeeld gelijk moet zijn opdat 2 objecten gelijk zijn. Alles hangt af van de wijze waarop de klasse dienst moet doen.

Stel dat we vinden dat een student gelijk is aan een andere student indien z'n Voornaam en Geboortejaar dezelfde is, we kunnen dan de Equals-methode overriden als volgt in de Student klasse:

public override bool Equals(Object o)
{  
    Student temp = (Student)o; //Zie opmerking na code!
    return (Geboortejaar == temp.Geboortejaar && Voornaam == temp.Voornaam);
}

De lijn Student temp = (Student)o; zal het object o casten naar een Student. Doe je dit niet dan kan je niet aan de interne Student-variabelen van het object o. Dit concept, polymorfisme (zie nog steeds hoofdstuk 16....We komen dichter!).

GetHashcode() overriden

Indien je Equals override dan moet je eigenlijk ook GetHashCode overriden, daar er wordt verondersteld dat twee gelijke objecten ook dezelfde unieke hashcode teruggeven. Wil je dit dus implementeren dan zal je dus een (bestaand) algoritme moeten schrijven dat een uniek nummer genereert voor ieder niet-gelijke object. Algoritmes bespreken om zelf een hash te genereren liggen niet in de scope van dit boek.

ReferenceEquals()

Als je nog wat dieper zou graven in de documentatie van System.Object zou je ontdekken dat er ook een static methode met als signatuur static bool ReferenceEquals(object obj1, object obj2) bestaat. Deze handige methode laat je toe om te controleren of 2 variabelen dezelfde referentie hebben. Je kan hiermee dus kijken of 2 variabelen naar hetzelfde object in de heap verwijzen. Het gebruik ervan is eenvoudig:

if(ReferenceEquals(student1,student3))
{
    Console.WriteLine("Beide bevatten zelfde referentie!");
}

Nu stelt zich de vraag: waarom deze controle niet met de == doen? Alhoewel dit perfect toegestaan is, moet je je ervan bewust zijn dat de werking van == kan overschreven worden.Ik leg dit niet uit, maar kijk zeker eens in de appendix naar het hoofdstuk Operator overloading.

Je hebt dus geen garantie dat in alle projecten de == werkt zoals je zou verwachten. Prefereer daarom om ReferenceEquals() te gebruiken. Merk op dat we System. voor de methodenaam mogen weglaten, net zoals we Object in plaats van System.Object mogen schrijven.

"Ik ben nog niet helemaal mee..."

Niet getreurd, je bent niet de enige: het is allemaal een hoop nieuwe kennis om te verwerken. En ik vermoed dat je nu niet bepaald overweldigd bent van de nieuwe kennis. Mogelijk heb je nu zoiets van? "Ok..wow?! Wat krijg ik nu juist extra wetende dat al mijn klassen overerven van een oer-klasse? 4 methoden en wat beloofde compatibiliteit met andere .NET bibliotheken? Call me ...unimpressed".Begrijpelijke reactie.

Hou vol, we zijn een hoop puzzelstukjes aan het opnemen die finaal zullen samenkomen om een gigantisch knappe OOPuzzel te maken (see what I did there?) In die puzzel zal polymorfisme onze sterspeler worden. Het zal ons toelaten erg krachtige code te schrijven.

Polymorfisme wordt onze doelpuntenmaker, maar System.Object zal steeds de perfecte voorzet geven!