Loading...
Loading...
Loading...
Loading...
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:
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:
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.
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:
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.
Wanneer een C# applicatie wordt uitgevoerd krijgt het twee soorten geheugen toegewezen dat het 'naar hartelust' kan gebruiken:
Het kleine, maar snelle stack geheugen
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:
Value types
Reference types
Als je volgende tabel begrijpt dan beheers je geheugenmanagement in C#:
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.
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
int
, uint
long
, ulong
char
float
, double
, decimal
bool
structs (zien we niet in deze cursus)
enums
= 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:
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?
new Student()
: new
roept de constructor van Student
aan. Deze zal een constructor in de heap aanmaken en vervolgens de geheugenlocatie teruggeven.
Een variabele stud
wordt in de stack aangemaakt.
De geheugenlocatie uit de eerste stap wordt vervolgens in stud
opgeslagen in de stack.
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:
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.
Meer info, lees zeker volgende artikels:
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
.
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.
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:
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.
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):
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.
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.
Maak een consoleapplicatie met daarin een klasse Pokémon die de werking zoals hierboven beschreven heeft:
De base-stats worden als ints bewaard. Maak voor al deze basis-eigenschappen full properties, namelijk:
HP_Base
Attack_Base
Defense_Base
SpecialAttack_Base
SpecialDefense_Base
Speed_Base
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.
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.
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.
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
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).
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
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":
Maak enkele Pokémon objecten aan en stel hun base stats in.
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
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
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.
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)
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?!
Kies enkele Pokémon uit en maak in je Main enkele Pokémon-objecten aan met de juiste eigenschappen.
vergelijk je oplossing uit het vorige deel .
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 )
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
Kopieert de actuele waarde
Kopieert het adres naar de actuele waarde