arrow-left

All pages
gitbookPowered by GitBook
1 of 5

Loading...

Loading...

Loading...

Loading...

Loading...

H9: Geheugenmanagement bij klassen

Null en NullReferenceException

Zoals nu duidelijk is bevatten variabelen steeds een referentie naar een object. Maar wat als we dit schrijven:

Dit zal een fout geven. stud1 bevat namelijk nog geen referentie. Maar wat dan wel?

Deze variabele bevat de waarde null . Net zoals bij value types die een default waarde hebben (bv 0 bij een int ) als je er geen geeft, zo bevat reference types altijd null.

hashtag
NullReferenceException

Een veel voorkomende foutboodschap tijdens de uitvoer van je applicatie is de zogenaamde NullReferenceException . Deze zal optreden wanneer je code een object probeert te benaderen wiens waarde null is.

Laten we dit eens simuleren:

Dit zal resulteren in volgende foutboodschap:

We moeten in dit voorbeeld expliciet =null plaatsen daar Visual Studio slim genoeg is om je te waarschuwen voor eenvoudige potentiele NullReference fouten en je code anders niet zal compileren.

hashtag
NullReferenceException voorkomen

Objecten die niet bestaan zullen altijd null. Uiteraard kan je niet altijd al je code uitvlooien waar je misschien =new SomeObject(); bent vergeten.

Voorts kan het ook soms by design zijn dat een object voorlopig null is.

Gelukkig kan je controleren of een object null is als volgt:

hashtag
Verkorte null controle notatie

Vaak moet je dit soort code schrijven:

Op die manier voorkom je NullReferenceException. Het is uiteraard omslachtig om steeds die check te doen. Je mag daarom ook schrijven:

Het vraagteken direct na het object geeft aan: "de code na dit vraagteken enkel uitvoeren indien het object voor het vraagteken niét null is".

Bovenstaande code zal dus gewoon een lege lijn op scherm plaatsen indien stud1 effetief null is, anders komt de naam op het scherm.

hashtag
Return null

Uiteraard mag je dus ook expliciet soms null teruggeven als resultaat van een methode. Stel dat je een methode hebt die in een array een bepaald object moet zoeken. Wat moet de methode teruggeven als deze niet gevonden wordt? Inderdaad, we geven dan null terug.

Volgende methode zoekt in een array van studenten naar een student met een specifieke naam en geeft deze terug als resultaat. Enkel als de hele array werd doorlopen en er geen match is wordt er null teruggegeven (de werking van arrays van objecten worden later besproken):

Student stud1;
stud1.Naam= "Test";
NullReferenceException error in VS

Stack en Heap

hashtag
Geheugenmanagement in C-Sharp

Tot nog toe lagen we er niet van wakker wat er achter de schermen van een C# programma gebeurde. We duiken nu dieper in wat er juist gebeurt wanneer we variabelen aanmaken.

hashtag
Twee soorten geheugen

Wanneer een C# applicatie wordt uitgevoerd krijgt het twee soorten geheugen toegewezen dat het 'naar hartelust' kan gebruiken:

  1. Het kleine, maar snelle stack geheugen

  2. Het grote, maar tragere heap geheugen

Afhankelijk van het soort variabele wordt ofwel de stack, ofwel de heap gebruikt. Het is uitermate belangrijk dat je weet in welk geheugen de variabele zal bewaard worden!

Er zijn namelijk twee soorten variabelen:

  1. Value types

  2. Reference types

Als je volgende tabel begrijpt dan beheers je geheugenmanagement in C#:

hashtag
Waarom twee geheugens?

Waarom plaatsen we niet alles in de stack? De reden hiervoor is dat bij het compileren van je applicatie er reeds zal berekend worden hoeveel geheugen de stack zal nodig hebben. Wanneer je programma dus later wordt uitgevoerd weet het OS perfect hoeveel geheugen het minstens moet reserveren. Er is echter een probleem: we kunnen niet alles perfect berekenen/voorspellen. Een variabele van het type int is perfect geweten hoe groot die zal zijn (32 bit).Maar wat met een string? Of met een array waarvan we pas tijdens de uitvoer de lengte aan de gebruiker misschien vragen? Het zou nutteloos (en zonde) zijn om reeds bij aanvang een bepaalde hoeveelheid voor een array te reserveren als we niet weten hoe groot die zal worden. Beeld je maar eens in dat we 2k byte reserveren om dan te ontdekken dat we maar 5byte ervan nodig hebben. RAM is goedkoop, maar toch... De heap laat ons dus toe om geheugen op een wat minder gestructureerde manier in te palmen. Tijdens de uitvoer van het programma zal de heap als het ware dienst doen als een grote zandbak waar eender welke plek kan ingepalmd worden om zaken te bewaren. De stack daarentegen is het kleine bankje naast de zandbak: handig, snel, en perfect geweten hoe groot.

hashtag
Value types

Value types worden in de stack bewaard. De effectieve waarde van de variabele wordt in de stack bewaard. Dit zijn alle gekende, 'eenvoudige' datatypes die we totnogtoe gezien hebben, inclusief enums en structs (zie later):

  • sbyte, byte

  • short, ushort

= operator bij value types

Wanneer we een value-type willen kopieren dan kopieren de echte waarde:

Vanaf nu zal anderGetal de waarde 3 hebben. Als we nu een van beide variabelen aanpassen dan zal dit geen effect hebben op de andere variabelen.

We zien het zelfde effect wanneer we een methode maken die een parameter van het value type aanvaardt- we geven een kopie van de variabele mee:

De parameter a zal de waarde 5 gekopieerd krijgen. Maar wanneer we nu zaken aanpassen in a zal dit geen effect hebben op de waarde van getal. De output van bovenstaand programma zal zijn:

hashtag
Reference types

Reference types worden in de heap bewaard. De effectieve waarde wordt in de heap bewaard, en in de stack zal enkel een referentie of pointer naar de data in de heap bewaard worden. Een referentie (of pointer) is niet meer dan het geheugenadres naar waar verwezen wordt (bv 0xA3B3163) Concreet zijn dit alle zaken die vaak redelijk groot zullen zijn:

  • objecten, interfaces en delegates

  • arrays

= operator bij reference types

Wanneer we de = operator gebruiken bij een reference type dan kopieren we de referentie naar de waarde, niet de waarde zelf.

Bij objecten We zien dit gedrag bij alle reference types, zoals objecten:

Wat gebeurt er hier?

  1. new Student() : new roept de constructor van Student aan. Deze zal een constructor in de heap aanmaken en vervolgens de geheugenlocatie teruggeven.

  2. Een variabele stud wordt in de stack aangemaakt.

Bij arrays

Maar ook bij arrays:

In dit voorbeeld zal andereNummers dus nu ook verwijzen naar de array in de heap waar de actuele waarden staan.

Als we dus volgende code uitvoeren dan ontdekken we dat beide variabele naar dezelfde array verwijzen:

We zullen dus als output krijgen:

Hetzelfde gedrag zien we bij objecten:

We zullen in dit geval dus Queen op het scherm zien omdat zowel b als a naar het zelfde object in de heap verwijzen. Het originele "abba"-object zijn we kwijt en zal verdwijnen (zie Garbage collector verderop).

Methoden en reference parameters

Ook bij methoden geven we de dus de referentie naar de waarde mee. In de methode kunnen we dus zaken aanpassen van de parameter en dan passen we eigenlijk de originele variabele aan:

We krijgen als uitvoer:

Opgelet: Wanneer we een methode hebben die een value type aanvaardt en we geven één element van de array met dan geven dus een kopie van de actuele waarde mee!

De output bewijst dit:

hashtag
De Garbage Collector (GC)

Een essentieel onderdeel van .NET is de zogenaamde GC, de Garbage Collector. Dit is een geautomatiseerd onderdeel van ieder C# programma dat ervoor zorgt dat we geen geheugen voor niets gereserveerd houden. De GC zal geregeld het geheugen doorlopen en kijken of er in de heap data staat waar geen references naar verwijzen. Indien er geen references naar wijzen zal dit stuk data verwijderd worden.

In dit voorbeeld zien we dit in actie:

Vanaf de laatste lijn zal er geen referentie meer naar {3,4,5} in de heap zijn, daar we deze hebben overschreven met een referentie naar {1,2,3}.De GC zal dus deze data verwijderen.

Wil je dat niet dan zal je dus minstens 1 variabele moeten hebben dat naar de data verwijst. Volgende voorbeeld toont dit:

De variabele bewaarArray houdt dus een referentie naar {3,4,5} bij en we kunnen dus later via deze variabele alsnog aan de originele data.

hashtag
Meer weten?

Meer info, lees zeker volgende artikels:

Student stud1 = null;

Console.WriteLine(stud1.Name);
if(stud1 == null)
    Console.WriteLine("oei. object bestaat niet")
if(stud1 != null)
{
    Console.WriteLine(stud1.Name)
}
Console.WriteLine(stud1?.Name)
static Student ZoekStudent(Student[] array, string naam)
{
    for (int i = 0; i < array.Length; i++)
    {
        if (array[i].Name == naam)
            return array[i];
    }

    return null;
}

Kopieert het adres naar de actuele waarde

int, uint
  • long, ulong

  • char

  • float, double, decimal

  • bool

  • structs (zien we niet in deze cursus)

  • enums

  • De geheugenlocatie uit de eerste stap wordt vervolgens in stud opgeslagen in de stack.

    Value types

    Reference types

    Inhoud van de variabele

    De eigenlijke data

    Een referentie naar de eigenlijke data

    Locatie

    (Data) Stack

    Heap (globaal)geheugen

    Beginwaarde

    0,0.0, "",false, etc.

    null

    Effect van = operator

    Reference en value typesarrow-up-right
    Stack vs heaparrow-up-right

    Kopieert de actuele waarde

    Oefeningen

    We gaan een programma schrijven dat ons toelaat enkele basis-eigenschappen van specifieke Pokémon te berekenen terwijl ze levellen. Nadruk van deze oefening is het juist gebruiken van properties. Bekijk de cheat sheet bij twijfel.

    Disclaimer: de informatie in deze tekst is een vereenvoudigde versie van de echte Pokémon-stats in de mate dat ik het allemaal een beetje kon begrijpen en juist interpreteren.

    hashtag
    Hoe Pokémon werken

    Korte uitleg over Pokémon en hun interne werking: Iedere Pokémon wordt uniek gemaakt door z’n base-stats, deze zijn voor iedere Pokémon anders. Deze base-stats (punt 3) zijn onveranderlijk en blijven dus doorheen het hele leven van een Pokémon dezelfde. Je kan de base-stats als het dna van een Pokémon beschouwen.

    De full-stats (punt 9) zijn echter de stats die de effectieve ‘krachten’ van een Pokémon bepalen in een gevecht. Deze stats worden berekend gebaseerd op de vaste base-stats en het huidige level van de Pokémon. Hoe hoger het level van de Pokémon, hoe hoger dus zijn full-stats.

    hashtag
    De Pokémonopdracht

    Maak een consoleapplicatie met daarin een klasse Pokémon die de werking zoals hierboven beschreven heeft:

    hashtag
    Base-stats

    De base-stats worden als ints bewaard. Maak voor al deze basis-eigenschappen full properties, namelijk:

    • HP_Base

    • Attack_Base

    • Defense_Base

    hashtag
    Extra stats

    Voorts wordt een Pokémon ook gedefinieerd door z’n naam (string) ,type (string, bv "grass & poison") en nummer (int), maak hiervoor auto properties aan.

    hashtag
    Level

    Voeg een fullproperty Level toe(type int). Deze heeft een public get, maar een private setter.

    Voeg een publieke methode "VerhoogLevel" toe. Deze methode zal , via de private setter van Level (zie vorig punt), de level van de Pokémon met 1 verhogen. Deze methode heeft géén parameters nodig en return niets.

    hashtag
    Statistieken

    Voeg 2 read-only properties toe (enkel get, géén set) genaamd "Average" en "Total":

    • De Average-property geeft het gemiddelde van de 6 base-stats terug , dus (HP_Base + Attack_Base + Defense_Base + SpAttack_Base + SpDefense_Base +Speed_Base)/6.

    • De Total-property geeft de som terug van de 6 basestats. Daar de base stats niet evolueren met het level veranderen dus Average en Total ook niet van zodra de base-stats werden ingesteld, toch mag je beide statistieken steeds herberekenen in de get.

    hashtag
    Level-gebaseerde stats

    De eigenschappen van de Pokémon die mee evolueren met het leven gaan we steeds als read-only property implementeren:

    • Voeg een read-only HP_Full property (int) toe om de maximum health voor te stellen. Deze wordt berekend als volgt: (( (HP_Base + 50 ) * Level ) / 50 ) + 10 (noot: dit is een benadering van hoe het bij "echte" Pokémon is ).

    • Voeg voor iedere base-stat een full-stat toe (int). Dus Defense_Full, Speed_Full, etc. Ook deze properties zijn readonly. Deze stats worden berekend als volgt: ( (stat_Base*Level) / 50 ) + 5. Attack_Full bijvoorbeeld wordt dus berekend als: ((Attack_Base*Level)/50)+5

    hashtag
    Maak enkele Pokémon

    Kies enkele Pokémon uit en maak in je Main enkele Pokémon-objecten aan met de juiste eigenschappen.

    Opgelet: Je dient dus enkel de base stats in te stellen. Alle andere zaken zijn op deze stats en het huidige level van de Pokémon gebaseerd.

    Toon aan dat de Average, Total , HP en andere stats correct berekend worden (controleer in de tabel op de voorgaande url).

    hashtag
    Level-up tester

    Maak een kleine loop die je toelaat om per loop een bepaalde Pokémon z’n level met 1 te verhogen en vervolgens toon je dan z’n nieuwe stats.

    1Test eens hoe de stats na bv 100 levels evolueren. Je zal zien dat bepaalde stats pas na een paar keer levelen ook effectief beginnen stijgen.

    Voeg extra functionaliteit naar keuze toe

    hashtag
    Deel 2: De Pokémontester

    vergelijk je oplossing uit het vorige deel .

    Het is een heel gedoe om telkens manueel de informatie van een Pokémon op het scherm te outputen. Voeg een methode public void ShowInfo() toe aan je Pokemon klasse. Deze methode zal alle relevante informatie (alle properties!) in een mooie vorm op het scherm tonen, bv:

    Maak nu een nieuwe console-applicatie genaamd "Pokémon Tester":

    1. Voeg je Pokemon-klasse-bestand toe aan dit project. Verander de "namespace" van dit bestand naar de namespace van je nieuwe console-applicatie (zie "Aanpassen van klasse" in )

    2. Maak enkele Pokémon objecten aan en stel hun base stats in.

    3. Schrijf een applicatie die aan de gebruiker eerst de 6 base-stats vraagt. Vervolgens wordt de Pokémon aangemaakt met die stats en worden de full-stats aan de gebruiker getoond

    hashtag
    Deel 3: Pokémon-battler

    hashtag
    Pokémon generator

    Maak een methode met volgende signatuur: static Pokemon GeneratorPokemon(). Plaats deze methode niét in je Pokémon-klasse, maar in Program.cs.

    Deze methode zal telkens een Pokémon aanmaken met willekeurige base-stats. Bepaal zelf hoe je dit gaat doen.

    hashtag
    Battle tester

    Voeg een methode met volgende signatuur toe aan je hoofdprogramma (dus ook weer in Program.cs): static int Battle(Pokemon poke1, Pokemon poke2).

    De methode zal een getal teruggeven dat aangeeft welke van de twee Pokémons een gevecht zou winnen. 1= poke1, 2 = poke2, 0 = gelijke stand.

    Controleer steeds of 1 of beide van de meegegeven Pokémons niet null zijn. Indien er 1 null is dan wint uiteraard de andere. Indien allebei null wint niemand (dus return je 0). Test of dit werkt!

    Bepaal zelf hoe Pokémons vechten (bv degene met de hoogste average van full-stats). Werk niet enkel met de base-stats, daar deze constant zijn. Het is leuker dat het level ook een invloed heeft (maar ga niet gewoon het level vergelijken)

    hashtag
    Alles samen

    Genereer 2 willekeurige Pokémons met je generator en laat ze vechten met je battle-methode. Toon wat output aan de gebruiker zodat hij ziet wat er allemaal gebeurt (en gebruik zeker de ShowInfo methode om dit snel te doen). Kan je dit in een loop zetten en wat leuker maken met Pokémons die telkens levelen als ze een gevecht winnen?!

    int getal=3;
    int anderGetal= getal;
    void DoeIets(int a)
    {
        a++;
        Console.WriteLine($"In methode {a}");
    }
    
    //Elders:
    int getal= 5;
    DoeIets(getal);
    Console.WriteLine($"Na methode {getal}");
    In methode 6
    Na methode 5
    Student stud= new Student();
    int[] nummers= {4,5,10};
    int[] andereNummers= nummers;
    andereNummers[0]=999;
    Console.WriteLine(andereNummers[0]);
    Console.WriteLine(nummers[0]);
    999
    999
    Student a= new Student("Abba");
    Student b= new Student("Queen");
    a=b;
    Console.WriteLine(a.Naam);
    void DoeIets(int[] a)
    {
       a[0]++;
       Console.WriteLine($"In methode {a[0]}");
    }
    
    //Elders:
    int[] getallen= {5,3,2};
    DoeIets(getallen);
    Console.WriteLine($"Na methode {getallen[0]}");
    In methode 6
    Na methode 6
    void DoeIets(int a)
    {
        a++;
        Console.WriteLine($"In methode {a}");
    }
    
    //Elders:
    int[] getallen= {5,3,2};
    DoeIets(getallen[0]); //<= VALUE TYPE!
    Console.WriteLine($"Na methode {getallen[0]}");
    In methode 6
    Na methode 5
    int[] array1= {1,2,3};
    int[] array2= {3,4,5};
    array2=array1;
    int[] array1= {1,2,3};
    int[] array2= {3,4,5};
    int[] bewaarArray= array2;
    array2=array;
    SpecialAttack_Base
  • SpecialDefense_Base

  • Speed_Base

  • Vraag nu aan de gebruiker tot welke level de Pokémon moet gelevelled worden. Roep zoveel keer de LevelUp-methode aan van de Pokémon. (of kan je dit via een parameter doorgeven aan LevelUp?!)

  • Toon terug de full-stats van de nu ge-levelde Pokémon

  • deze lijstarrow-up-right
    met volgende oplossingarrow-up-right
    volgende uitlegarrow-up-right
    Pokémon
    Pikachu (level 5)
    Base stats:
        * Health = 56
        * Speed = 30
        etc
    Full stats:
        * Health = 100
        etc.

    Objecten en methoden

    hashtag
    Objecten als argumenten

    Klassen zijn "gewoon" nieuwe types. Alle regels die we dus al kenden in verband met het doorgeven van variabelen als parameters in een methoden blijven gelden. Het enige verschil is dat we objecten by reference meegeven aan een methode. Aanpassingen aan het object in de methode zal dus betekenen dat je het originele object aanpast dat aan de methode werd meegegeven. Hier moet je dus zeker rekening mee houden.

    Een voorbeeld. Stel dat we volgende klasse hebben waarin we metingen willen opslaan, alsook wie de meting heeft gedaan:

    In ons hoofdprogramma schrijven we een methode ToonMetingInKleur die ons toelaat om deze meting op het scherm te tonen in een bepaalde kleur. Het gebruik en de methode zelf zouden er zo kunnen uitzien:

    hashtag
    Objecten in methoden aanpassen

    Je kan dus ook methoden schrijven die meegegeven objecten aanpassen daar we deze by reference doorsturen. Een voorbeeld:

    Als we deze methode als volgt aanroepen

    Da zullen we zien dat de temperatuur in m1 effectief met 1 werd verhoogd.

    Dit gedrag zouden we NIET zien bij volgende methode daar int by value wordt doorgegeven:

    hashtag
    Delen van objecten als parameter

    Stel dat we volgende methode hebben

    Je mag deze methode dus ook oproepen als volgt (we gebruiken de Meting objecten m1 en m2 uit vorige paragraaf):

    Het type van de property Temperatuur is int en mag je dus als parameter aan deze methoden meegeven.

    hashtag
    Objecten als resultaat

    Weer hetzelfde verhaal: ook klassen mogen het resultaat van een methoden zijn.

    Deze methode kan je als volgt dan gebruiken:

    Merk op dat het dus kan zijn dat een methode null teruggeeft. Het kan dus zeker geen kwaad om steeds in je code te controleren of je effectief iets hebt terug gekregen:

    class Meting
    {
        public int Temperatuur { get; set; }
        public string OpgemetenDoor { get; set; }
    }
    static void Main(string[] args)
    {
        Meting m1 = new Meting();
        m1.Temperatuur = 26; 
        m1.OpgemetenDoor = "Elon Musk";
        Meting m2 = new Meting();
        m2.Temperatuur = 34; 
        m2.OpgemetenDoor = "Dennis Rodman";
    
        ToonMetingInKleur(m1, ConsoleColor.Red);
        ToonMetingInKleur(m2, ConsoleColor.Gray);
    }
    
    static void ToonMetingInKleur (Meting inmeting, ConsoleColor kleur)
    {
        Console.ForegroundColor = kleur;
        Console.WriteLine($"Temperatuur {inmeting.Temperatuur}°C werd opgemeten door {inmeting.OpgemetenDoor}");
        Console.ResetColor();
    }
    static void ToonMetingEnVerhoog(Meting inmeting)
    {
        ToonMetingInKleur(inmeting, ConsoleColor.Green);
    
        inmeting.Temperatuur++;
    }
    Meting m1 = new Meting();
    m1.Temperatuur = 26; m1.OpgemetenDoor = "Elon Musk";
    
    ToonMetingEnVerhoog(m1);
    
    Console.WriteLine(m1.Temperatuur);
    static void VerhoogGetal(int inmeting)
    {
        inmeting++;
    }
    static double Gemiddelde(double getal1, double getal2)
    {
        return (getal1 + getal2) / 2;
    }
    double result= Gemiddelde(m1.Temperatuur, m2.Temperatuur);
    static Meting GenereerRandomMeting()
    {
        Meting result = new Meting();
        Random r = new Random();
        result.Temperatuur = r.Next(-100, 200);
        result.OpgemetenDoor = "Onbekend";
    
        return result;
    }
    Meting m3 = GenereerRandomMeting();
    Meting m3 = GenereerRandomMeting();
    if(m3 != null)
    {
        ToonMetingInKleur(m3, ConsoleColor.Red);
    }