Wat leer je in dit hoofdstuk?

Na dit hoofdstuk kan je:

uitleggen dat alles erft van System.Object

de ingebouwde methoden ToString, Equals en GetHashCode overriden

een abstracte klasse en abstracte methoden maken

het verschil tussen abstract en virtual/sealed uitleggen

een eigen exception ontwerpen en opwerpen

Hoe het allemaal begon

Volgens Darwin stamt alle leven af van een gemeenschappelijke voorouder. En voor de niet-darwinisten onder ons: in den beginne schiep .NET System.Object.

Hoe je het ook bekijkt: elke klasse in C# stamt af van System.Object.

Het is de stamvader van alles.

System.Object

De oer-klasse: System.Object

Alles in C# erft van System.Object: je eigen klassen, Random, Console, zelfs int en bool.

internal class Student : System.Object
{ }

Schrijf je geen parent, dan is System.Object automatisch de parent.

Alles stamt af van System.Object.

Vier ingebouwde methoden

Elke klasse heeft er meteen vier, van System.Object:

ToString()

een string die het object voorstelt

Equals()

zijn twee objecten gelijk?

GetHashCode()

unieke hash van het object

GetType()

het datatype van het object

Opmerking

Drie ervan zijn virtual (ToString, Equals, GetHashCode): die kan je overriden. GetType() niet: dat moet altijd het echte type teruggeven.

GetType()

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

GetType() geeft een Type-object terug, met o.a. een Name-property.

StudentManager.Student
Student

ToString(): het werkpaardje

Console.WriteLine(stud1) roept stiekem stud1.ToString() aan. Standaard geeft dat de klassenaam. Override het voor iets nuttigs:

internal class Student
{
    public string Voornaam { get; set; }
    public int Geboortejaar { get; set; }

    public override string ToString()
    {
        return $"{Voornaam} ({Geboortejaar})";
    }
}

Nu toont Console.WriteLine(stud1) netjes:

Tim Dams (1981)

Equals() overriden

Jij bepaalt wanneer twee objecten “gelijk” zijn.

public override bool Equals(object o)
{
    if (o is not Student temp) // null of geen Student? Niet gelijk.
        return false;
    return Geboortejaar == temp.Geboortejaar && Voornaam == temp.Voornaam;
}

Opmerking

o is not Student temp checkt het type én steekt o meteen als Student in temp. Dat is-patroon zie je voluit in hoofdstuk 18.

GetHashCode() overriden

Override je Equals, override dan ook GetHashCode.

gelijke objecten moeten dezelfde hash geven, belangrijk voor Dictionary en HashSet

public override int GetHashCode()
{
    return HashCode.Combine(Voornaam, Geboortejaar);
}

Tip

Geef dezelfde properties mee die je in Equals vergelijkt. HashCode.Combine doet het rekenwerk voor je.

Kopiëren: shallow vs deep

System.Object geeft ook MemberwiseClone(): een shallow copy (ondiepe kopie).

public Student MaakKopie()
{
    return (Student)MemberwiseClone(); // shallow copy
}

Shallow

instantievariabelen van een valuetype worden gekopieerd, maar geneste objecten worden gedeeld (enkel de referentie gaat mee)

Deep

ook de geneste objecten zelf kopiëren, dat vraagt handwerk

Waarschuwing

Pas je in de kopie een gedeeld nested object aan, dan verandert het origineel mee. Voor een echte, losse kopie heb je een deep copy nodig.

Even moed verzamelen

“Ok, wow? Vier methoden en wat beloofde compatibiliteit? Call me unimpressed.”

Begrijpelijk. We leggen puzzelstukjes klaar die straks samenkomen. Polymorfisme wordt onze doelpuntenmaker, en System.Object geeft telkens de perfecte voorzet.

Abstracte klassen

Abstracte klassen in C#

Geef mij de blauwdruk van een “meubel”. Een tafel? Een kast? Een bed?

Sommige concepten zijn te abstract om er een object van te maken.

internal abstract class Dier
{
    public string Naam { get; set; }
}

Je kan niet new Dier() doen

Je kan er wel van overerven: Paard : Dier, Wolf : Dier

Tip

abstract en sealed zijn tegenpolen: bij abstract moet je overerven, bij sealed mag het net niet.

Abstracte methoden

Welk geluid maakt “een dier”? Onbekend. Geef enkel de signatuur:

internal abstract class Dier
{
    public abstract string MaakGeluid();
}

Child-klassen moeten dit overriden:

public override string MaakGeluid()
{
    return "Hinnikhinnik";
}

De child vult het gat in.

Belangrijk

virtual = override mag, abstract = override moet. Een abstracte methode kan enkel in een abstracte klasse.

Abstracte properties

Ook properties kunnen abstract; de child vult de implementatie in:

internal abstract class Dier
{
    public abstract int MaxLeeftijd { get; }
}
internal class Olifant : Dier
{
    public override int MaxLeeftijd
    {
        get { return 100; }
    }
}

Een abstracte { get; } is dus geen auto-property: er staat bewust geen implementatie.

Pong met abstract

Een gemeenschappelijke SpelObject voor alles op het scherm, zonder te weten hoe het getekend wordt:

internal abstract class SpelObject
{
    public int X { get; set; }
    public int Y { get; set; }
    public abstract void TekenOpScherm();
}

Balletje

erft hiervan en implementeert zijn eigen TekenOpScherm()

een nieuw ScoreBoard

erft hiervan en implementeert zijn eigen TekenOpScherm()

Zelf exceptions maken

Zelf een exception opwerpen

Exceptions zijn ook maar klassen die erven van Exception.

Werp er zelf een op met throw:

static int Bereken(int getal)
{
    if (getal != 0)
        return 100 / getal;
    else
        throw new DivideByZeroException("BOEM. ZWART GAT!");
}

Elders vang je die op met catch (DivideByZeroException e).

Een eigen exception ontwerpen

Erf van Exception en voorzie de drie standaard-constructors:

internal class Timception : Exception
{
    public Timception() { }
    public Timception(string message) : base(message) { }
    public Timception(string message, Exception inner)
        : base(message, inner) { }
}
throw new Timception("Een wilde exception verschijnt!");

throw versus throw ex

Waarschuwing

Gooi je een opgevangen exception opnieuw door, gebruik dan throw; (zonder variabele):

catch (Exception e)
{
    // ...loggen...
    throw;      // GOED: behoudt de stack trace
    // throw e; // SLECHT: wist de stack trace
}

throw e; wist de stack trace, waardoor de echte oorzaak veel moeilijker te vinden is.

Conclusie

Alles erft van System.Object, met o.a. ToString, Equals, GetHashCode (allemaal virtual)

Override ToString voor leesbare objecten; override Equals + GetHashCode samen

Een abstract klasse kan je niet instantiëren, maar dient als basis voor children

Een abstracte methode zonder body moet de child overriden

Eigen exceptions erven van Exception; gebruik throw; om de stack trace te bewaren

Tip

Volgende halte: compositie en aggregatie: de “heeft-een”-relatie.

Meer weten

Kennisclips

Oefeningen H14 · Quizlet flashcards

Zie verder

Hoe doen andere programmeertalen hetzelfde? En een korte zoek-de-fout, om zo kritisch mogelijk naar code te leren kijken.

Zie verder: zoek de fout

Onderstaande C#-code wil twee studenten vergelijken op hun naam. De Equals-override doet z’n werk, maar bij het gebruik van Student-objecten als sleutel in een Dictionary of HashSet lopen dingen mis. Wat ontbreekt er?

internal class Student
{
    public string Voornaam { get; set; }
    public int Geboortejaar { get; set; }

    public override bool Equals(object o)
    {
        if (o is not Student temp)
            return false;
        return Geboortejaar == temp.Geboortejaar && Voornaam == temp.Voornaam;
    }
}

Wie Equals override, moet ook GetHashCode override. Twee gelijke objecten horen dezelfde hashcode terug te geven, en daar rekenen Dictionary en HashSet op.

Zie verder: één oer-klasse

Ook Java kent dezelfde oer-klasse, daar gewoon Object genoemd. Elke klasse erft er impliciet van, met methoden als toString(), equals() en hashCode():

class Student {
    // erft automatisch van java.lang.Object
}

Student stud = new Student();
System.out.println(stud.getClass().getName()); // Student

Heel gelijkaardig dus. Ook Python doet dit met object.

Zie verder: abstract zonder keyword

C++ kent het keyword abstract niet, maar wel exact hetzelfde idee: een “pure virtual” methode. Je zet = 0 achter de signatuur, en zo’n klasse kan je niet instantiëren:

class Dier {
public:
    virtual std::string MaakGeluid() = 0; // pure virtual
};

class Paard : public Dier {
public:
    std::string MaakGeluid() override { return "Hinnikhinnik"; }
};

Python doet dit met de ABC-module en @abstractmethod.

Zie verder: eigen exceptions in Python

In Python werkt dit verrassend gelijkaardig: erf over van Exception en gooi op met raise (het equivalent van throw):

class Timception(Exception):
    pass

def tims_methode():
    raise Timception("Een wilde exception verschijnt!")

try:
    tims_methode()
except Timception as e:
    print(e)
Een wilde exception verschijnt!

Samenvattende poster

Een handige samenvattende poster (Opgelet: gemaakt m.b.v. ChatGPT Image Gen 2).