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:
Dan 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 dan als volgt 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:
Objecten als parameter of returnwaarde (opname uit hoorcollege 4/3/20)
Pokemon
(h9-pokeattack)herhalen aanmaken klassen
herhalen aanmaken instantiemethoden
herhalen aanmaken full properties
We willen een bootleg Pokémon spel maken. We starten met een klasse om Pokémon voor te stellen.
Schrijf een klasse Pokemon
met volgende onderdelen:
een full property MaxHP
deze stelt een getal voor dat altijd minstens 20 en maximum 1000 bedraagt
als je een lagere of hogere waarde probeert in te stellen, wordt de dichtstbijzijnde waarde ingesteld
een full property HP
deze stelt een getal voor dat altijd groter dan of gelijk aan 0 is; verder kan de waarde ook nooit groter gemaakt worden dan MaxHP
; elke poging om het getal kleiner dan 0 te maken, maakt het gelijk aan 0 en elke poging om boven MaxHP
te gaan, maakt het gelijk aan MaxHP
.
een autoproperty PokeSpecies
om aan te geven over welk soort Pokémon het gaat; maak hiervoor een enum PokeSpecies
met waarden Bulbasaur
, Charmander
, Squirtle
, Pikachu
een autoproperty PokeType
om aan te geven wat het element van de Pokémon is; maak hiervoor een enum PokeTypes
met waarden Grass
, Fire
, Water
, Electric
een methode Attack()
: deze zorgt ervoor dat de naam van het soort Pokémon in hoofdletters en in kleur wordt geprint. Je kan de methode ToString()
van een enum gebruiken. De kleur die je gebruikt is als volgt:
groen voor type Grass
rood voor type Fire
blauw voor Water
geel voor Electric
Schrijf dan een statische MakePokemon
-methode in de klasse Pokemon
die één Pokemon
van elke soort maakt (je mag de soort en het type invullen na het aanmaken van de objecten) en elk van deze Pokemon
één keer hun methode Attack
laat uitvoeren. Elke Pokemon
start bovendien met 20 hit points als huidige waarde en als maximumwaarde.
(In groen, rood, blauw en geel.)
arrays van objecten
null
In een gevecht begin je met je eerste Pokémon die nog bij bewustzijn is. Bewusteloze Pokémon kunnen immers niet vechten. Schrijf een statische methode om de eerste bewuste Pokémon te vinden.
Schrijf een statische methode FirstConsciousPokemon
met één parameter: een array van Pokemon
. Deze methode loopt met een for
-lus door de array en geeft als antwoord de eerste Pokemon
terug met minstens 1 HP. Je moet zorgen dat de methode aanvaard wordt door de compiler, ook als er geen enkele bewuste Pokémon in de rij is bijgehouden.
Schrijf ook een statische methode TestConsciousPokemon()
die een array van dezelfde vier Pokémon als hierboven maakt, waarbij Bulbasaur en Charmander 0 HP hebben en Squirtle 2 HP. Toon wat gebeurt als de eerste wakkere Pokémon aanvaalt. Dit is de methode die je vanuit je keuzemenu zal oproepen.
arrays van objecten
null
Je moet ook het geval afhandelen waarbij al je Pokémon KO zijn.
Breid je methode TestConsciousPokemon
uit zodat ze niet crasht wanneer al je Pokémon KO zijn. Doe dit in een nieuwe versie, TestConsciousPokemonSafe
.
call by value vs. call by reference
Een beginnend programmeur bij Game Freak heeft volgende statische methode geschreven in je klasse:
Hij gaat ervan uit dat dit werkt:
Maar dit klopt niet. Los zijn bug op.
Je moet RestoreHP
anders schrijven en ook het gebruik ervan aanpassen. Je mag de parameters van RestoreHP
volledig aanpassen en ook de eerste for
-lus veranderen. De tweede for
-lus en het aanmaken van de array van Pokemon
moeten exact gebeuren zoals ze geschreven zijn.
Roep DemoRestoreHP()
op uit je keuzemenu.
gebruik van Random
null
guard
call by reference
Hoe wreed het ook is, Pokémon zijn bestemd om tegen elkaar te vechten. Schrijf een simulatie van een gevecht met een willekeurig element.
Schrijf eerst een enumeratie FightOutcome
met drie mogelijkheden: WIN
, LOSS
en UNDECIDED
("onbeslist").
Schrijf dan een statische methode FightOutcome
in de klasse Pokemon
. Deze heeft drie parameters, twee Pokemon
-objecten en één Random
-object.
FightOutcome()
werkt als volgt:
Een van de twee Pokémon mag eerst aan de beurt; welke van de twee wordt willekeurig beslist met behulp van het Random
-object.
Wanneer een Pokémon aan de beurt is, voert hij zijn Attack()
methode uit.
Hierna verlaagt de HP van de andere Pokémon met een getal tussen 0 en 20.
Hierna is de andere van de twee Pokémon aan de beurt, maar alleen als hij nog bij bewustzijn is.
De match is voorbij wanneer één van de twee Pokémon 0 HP heeft bereikt. Dan wordt het resultaat teruggegeven:
WIN
als de eerste Pokémon die je als parameter hebt meegegeven nog bij bewustzijn is.
LOSS
als de tweede nog bij bewustzijn is.
Handel ook situaties af waarbij minstens één van de twee Pokémon null
is of al KO is bij het begin van de match. Dan wint de andere vanzelf, tenzij ze allebei ontbreken of KO zijn. Dan is de uitkomst UNDECIDED
.
Schrijf ten slotte een methode DemoFightOutcome()
die twee Pokémon naar keuze aanmaakt, hen tegen elkaar laat vechten tot er een resultaat is en dat resultaat dan op het scherm toont.
Test je methode met alle combinaties:
twee gezonde Pokémon
één bewusteloos
twee bewusteloos
één null
twee null
één bewusteloos en één null
Ga na dat al je code van de eerste oefeningen nog werkt nadat je de laatste hebt afgerond en plaats alles op Bitbucket.
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:
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):
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
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
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:
Value types | Reference types |
Inhoud van de variabele | De eigenlijke data | Een referentie naar de eigenlijke data |
Locatie | (Data) Stack | Heap (globaal)geheugen |
Beginwaarde |
|
|
Effect van = operator | Kopieert de actuele waarde | Kopieert het adres naar de actuele waarde |