Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Schrijf een programma om een boodschappenlijstje samen te stellen en af te werken aan de hand van List<string>
. Maak deze versie deel van de klasse Datastructuren
.
Vraag eerst om items toe te voegen, totdat er een lege regel wordt ingegeven. Toon telkens om het hoeveelste item het gaat, zoals in de voorbeeldinteractie.
Sorteer vervolgens de lijst.
Vraag dan, zo lang er nog items op de lijst staan en zo lang de gebruiker nog wenst te winkelen, welk item gekocht is. Wanneer er een item wordt ingegeven dat op het lijstje staat, verwijder je dat van het lijstje.
Als er op het einde nog niet-gekochte items over zijn, laat je zien welke items de gebruiker is vergeten te kopen.
Tip: voor lijsten is Sort
een instantiemethode
We wensen een simpel telefoonboek bij te houden, waarin je namen en nummers plaatst.
maak eerst een blanco Dictionary van string naar string aan
vraag in een lus telkens of de gebruiker nog wil doorgaan en, zo ja, vraag om een naam en een nummer
hou de koppeling van de naam en dat nummer bij
dit mag geen fout leveren als de naam al in het woordenboek staat - overschrijf in dat geval de waarde
je kan controleren met de instantiemethode ContainsKey
toon tenslotte de inhoud van heel je telefoonboek
noem je methode TelefoonboekNaamNummer
Zie boven, maar we willen nu telefoonnummers ook groeperen per gemeente
per gemeente heb je een Dictionary dat werkt zoals in de vorige oefening
om aan het Dictionary van een gemeente te komen, gebruik je een "groter" Dictionary met de naam van de gemeente als opzoekingssleutel
achteraf print je de gegevens per gemeente, zoals dat ook in een fysiek telefoonboek ongeveer het geval is
noem je methode TelefoonboekGemeenteNaamNummer
We willen graag dat ons Dictionary (zonder gemeente) veilig doorgegeven kan worden aan methodes enz. Daarom zullen we er een ImmutableDictionary van maken.
start met aanmaak van een builder voor een ImmutableDictionary
vraag de gegevens zoals in de eerdere oefening
plaats deze stap voor stap in de builder (ook hier kan je ContainsKey gebruiken)
zet, voor je alle gegevens print, om naar een ImmutableDictionary en pas daar een foreach lus op toe
In de interactie zie je geen verschil met de eerdere oefening.
Als je alles eerder mee hebt kunnen volgen, werk dan vanaf je recentste commit.
StudieProgramma.ToonOverzicht()
, Cursus.ToonOverzicht()
en Student.ToonOverzicht()
met foreach
Pas je ToonOverzicht-methodes aan zodat er geen gebruik wordt gemaakt van een klassieke for
, maar wel van een foreach
.
Voorzie de klasse Student van een statische read-only property AlleStudenten
. Deze is van het type List<Student>
en bevat altijd elke student die in het systeem aanwezig is. Dit gebeurt door bij de constructie van elk Student
-object de lijst uit te breiden.
AlleStudenten
beveiligenMaak van AlleStudenten
een ImmutableList<T>
in plaats van een gewone List<T>
. Merk op dat je dit niet hoeft te doen voor het achterliggend attribuut.
Vervang alle properties van StudieProgramma
, Cursus
en Student
van een arraytype naar een List
type. AlleCursussen maak je immutable.
Vervang hierbij ook for
-lussen door foreach
-lussen waar je kan. Je hoeft geen rekening te houden met capaciteiten die eerder zijn vastgelegd voor arrays. Je mag er ook van uitgaan dat er geen null
waarden in lijsten worden geplaatst als dat niet zinvol is. Dit kan je code wat korter maken.
In sommige situaties wil je dat een element geen twee keer in een datastructuur terecht kan komen. Je wil bijvoorbeeld dat de lijst met cursussen die deel uitmaakt van een studieprogramma geen tweemaal dezelfde cursus kan bevatten.
In dit geval gebruik je geen List<T>
, maar een HashSet<T>
. Elementen toevoegen doe je met de methode Add
en elementen verwijderen doe je met Remove
. Ook hier beperken we ons voorlopig tot voorgedefinieerde soorten objecten. Wanneer we System.Object
hebben bestudeerd, kunnen we ook HashSet
s van onze eigen types maken.
De immutable variant is ImmutableHashSet.
Een Queue is een collectie van elementen die in een welbepaalde volgorde behandeld moeten worden: van voor naar achter. Vergelijk met een wachtrij bij de bakker: de klant die eerst in de rij staat wordt eerst geholpen, dan steeds de volgende klant tot we aankomen bij de klant die het laatst in de rij is aangesloten. We noemen dit soort van collectie ook wel een First In, First Out oftewel FIFO-collectie: het item dat eerst in de rij is gezet, is ook het eerste dat behandeld wordt.
Een Queue is dus een speciaal soort lijst, waarbij het toevoegen en verwijderen van elementen op de lijst niet op gelijk welke plaats mag gebeuren. Een queue biedt daarom geen Add() of RemoveAt() methode aan. In plaats daarvan gebruik je:
Enqueue(T item)
om een item aan de rij toe te voegen
Dequeue()
om een item uit de rij te halen. Deze methode geeft als returnwaarde het weggehaalde item terug, zodat je er iets mee kan doen.
Peek()
geeft je het eerstvolgende item terug, maar verwijdert het nog niet uit de rij.
Op lijn 18 wordt de volgende klant uit de rij gehaald. Deze klant gebruiken we nog snel om zijn naam te tonen aan de gebruiker, maar na lijn 29 zal deze klant verdwijnen. Wil je deze klant in meer dan één statement gebruiken, zal je hem dus moeten opslaan in een lokale variabele:
Op lijn 20 wordt er eerst 'gespiekt' wie de volgende klant is: Piet. Met Peek()
wordt hij echter nog niet uit de rij gehaald, zoals je in onderstaande output kan zien.
De immutablevariant van Queue is ImmutableQueue.
Het omgekeerde van een Queue is een Stack. Dit is een lijst van items waarbij je steeds het laatst toegevoegde item eerst wilt behandelen. Vergelijk dit met een stapel borden aan de afwas: het eerstvolgende bord dat je afwast, is het bovenste bord op de stapel, dus het laatst toegevoegde. Of met een rij wagens in een lange, smalle garage met maar één toegangspoort: de eerste wagen die kan buitenrijden, is degene die laatst is binnengereden.
Dit noemen we een LIFO-collectie, oftewel Last In, First Out. Waar Queue Enqueue(T item)
en Dequeue()
gebruikte om items toe te voegen en uit de rij te halen, gebruikt Stack
Push(T item)
om een item op de stapel te leggen.
Pop()
om een item van de stapel te nemen.
Peek()
om het bovenste item op de stapel te bekijken, zonder het er af te nemen.
Dit voorbeeld demonstreert de werking van de 'Maak ongedaan' functionaliteit die je hebt in de meeste programma's op je computer. Als je op 'Maak ongedaan' (Engels: undo, commando: Ctrl+Z) klikt, wordt enkel dat wat je als laatste gedaan hebt, teruggedraaid.
Volgend filmpje demonstreert de acties die de gebruiker uitvoert in een tekstbewerkingsprogramma:
De gebruiker neemt volgende stappen, vertrekkende vanaf een wit blad:
Voeg paragraaf toe
Zet tekst in vet
Haal stuk tekst weg
Maak laatste actie ongedaan
Maak tekst groter.
Maak laatste actie ongedaan
Maak tekst kleiner.
Voeg tekst toe.
De code om deze acties bij te houden in een actiehistoriek zou kunnen zijn:
Dit geeft volgende output:
De immutablevariant van Stack is ImmutableStack.
In onderstaande kennisclip wordt er een andere syntax gebruikt om objecten aan te maken dan wat wij gewoon zijn (namelijk constructors met parameters). Dat maakt geen verschil voor de werking van de datastructuur. Je zou net zo goed een constructor kunnen definiëren die hetzelfde doet.
Een List<>
collectie is de meest standaard collectie die je kan beschouwen als een flexibelere variant op een een doodnormale array.
De Generieke List<>
klasse bevindt zich in de System.Collections.Generic
namespace. Je dient deze namespace dus als using
bovenaan toe te voegen wil je deze klasse kunnen gebruiken.
De klasse List<T>
is een zogenaamde generieke klasse. Tussen de < >
tekens plaatsen we het type dat de lijst zal moeten gaan bevatten. Vaak wordt dit genoteerd als T
voor "type". Bijvoorbeeld:
List<int> alleGetallen= new List<int>();
List<bool> binaryList = new List<bool>();
List<Pokemon> pokeDex = new List<Pokemon>();
List<string[]> listOfStringarrays = new List<string[]>();
Zoals je ziet hoeven we bij het aanmaken van een List
geen begingrootte mee te geven, wat we wel bij arrays moeten doen. Dit is een van de voordelen van List
: ze groeien mee. Als we toch een begingrootte meegeven (zoals in de kennisclip even getoond wordt) is dat enkel om de performantie van de code wat te verhogen in bepaalde scenario's. Wij gaan dit nooit doen.
Via de Add()
methode kan je elementen toevoegen aan de lijst. Je dient als parameter aan de methode mee te geven wat je aan de lijst wenst toe te voegen. Deze parameter moet uiteraard van het type zijn dat de List
verwacht.
In volgende voorbeeld maken we een List aan die objecten van het type string mag bevatten en vervolgens plaatsen we er twee elementen in.
Het leuke van een List is dat je deze ook kan gebruiken als een gewone array, waarbij je met de indexer elementen kan aanspreken. Stel bijvoorbeeld dat we een lijst hebben met minstens 4 strings in. Volgende code toont hoe we de string op positie 3 kunnen uitlezen en hoe we die op positie 2 overschrijven:
Ook de klassieke werking met for
blijft gelden. De enige aanpassing is dat List<T>
niet met Length
werkt maar met Count
.
Interessante methoden en properties voorts zijn:
Clear()
:methode die de volledige lijst leegmaakt
Insert()
: methode om element op specifieke plaats in lijst toe te voegen, bijvoorbeeld:
voegt de string toe op de tweede plek en schuift de rest naar achter
Contains()
: geef als parameter een specifiek object mee (van het type T
dat de List<T>
bevat) om te weten te komen of dat specifieke object in de List<>
terug te vinden is. Indien ja dan zal true worden teruggeven.
IndexOf()
: geeft de index terug van het element item in de rij. Indien deze niet in de lijst aanwezig is dan wordt -1 teruggegeven.
RemoveAt()
: verwijder een element op de index die je als parameter meegeeft.
Remove():
verwijder het gegeven element
Contains, Remove
en IndexOf
zullen zich met jouw eigen klassen niet noodzakelijk gedragen zoals je verwacht. De verklaring hierachter komt later aan bod, wanneer we Equals en GetHashCode bekijken. Ze zullen wel werken zoals verwacht voor voorgedefinieerde types, inclusief DateTime
.
Je kan met een eenvoudige for
of while-loop over een lijst itereren, maar het gebruik van een foreach-loop is toch handiger.
Dit is dan ook de meestgebruikte operatie om eenvoudig en snel een bepaald stuk code toe te passen op ieder element van de lijst:
Naast de generieke List
collectie, zijn er nog enkele andere nuttige generieke 'collectie-klassen' die je geregeld in je projecten kan gebruiken.
In een dictionary wordt ieder element voorgesteld door een sleutel (key) en de waarde (value) van het element. Het idee is dat je de sleutel kan gebruiken om de waarde snel op te zoeken. De sleutel moet dan ook uniek zijn. Dictionaries stellen geen reeks met een volgorde voor, maar geven je de mogelijkheid data met elkaar in verband te brengen.
Enkele voorbeeldjes die het idee achter Dictionary kunnen verduidelijken:
een papieren telefoonboek is als een Dictionary met gecombineerde namen en adressen als keys en telefoonnummers als values
een echt woordenboek is als een Dictionary met woorden als keys en omschrijvingen als values
een array is als een Dictionary met getallen als keys en waarden van het type van de array als values
Bij de declaratie van de Dictionary<K,V>
dien je op te geven wat het datatype van de key zal zijn , alsook het type van de waarde (value). Met andere woorden, K
en V
komen niet letterlijk voor, maar je vervangt ze door types die je al kent.
Voor bovenstaande voorbeelden:
een echt woordenboek stel je best voor met string
als type van de key en string
als type van de value, want een woord stel je voor als string en een omschrijving ook
een array van double
kan je nabootsen door uint
te gebruiken als type van de key en double
als type van de value
het telefoonboek moeten we wat vereenvoudigen, maar als we op basis van naam meteen een telefoonnummer konden opzoeken (zonder adresgegevens,...), dan zou string
(de naam) het type van de key zijn en string
(telefoonnummer) het type van de value. Het telefoonnummer is geen getal omwille van zaken die je niet met een getaltype kan voorstellen zoals +32 of 0473.
In het volgende voorbeeld maken we een Dictionary
van klanten aan. Iedere klant heeft een unieke ID (de key, die we als int
gebruiken) alsook een naam (die niet noodzakelijk uniek is en de waarde voorstelt):
Bij de declaratie van customers
plaatsen we dus tussen de < >
twee datatypes: het eerste duidt het datatype van de key aan, het tweede dat van de values.
Merk op dat je niet verplicht bent om een int
als type van de key (of value) te gebruiken, dit mag eender wat zijn, zelfs een klasse.
Bij dit laatste horen wel enkele nuances. Deze worden pas behandeld in een later hoofdstuk. Voorlopig zullen we alleen voorgedefinieerde types opnemen in dictionaries.
We kunnen nu met behulp van bijvoorbeeld een foreach
-loop alle elementen tonen. Hier kunnen we de key met de .Key
-property uitlezen en het achterliggende object of waarde met .Value
. Value
en Key
hebben daarbij ieder het type dat we hebben gedefinieerd toen we het Dictionary
-object aanmaakten, in het volgende geval is de Key
dus van het type int
en Value
van het type string
:
We kunnen echter ook een specifiek element opvragen aan de hand van de key. Stel dat we de waarde van de klant met key 123 willen tonen:
De key werkt dus net als de index bij gewone arrays, alleen heeft de key nu geen relatie meer met de positie van het element in de collectie.
Je kan de syntax met rechte haakjes ook gebruiken om een element toe te voegen. In tegenstelling tot Add, geeft deze syntax geen fout als de key al bestaat, maar vervangt hij het bestaande verband:
Als je wil weten of een bepaalde key voorkomt in een Dictionary, gebruik je de instantiemethode ContainsKey
.
Wanneer je geen indexering nodig hebt, maar toch snel over alle elementen in een array wenst te gaan, dan is het foreach statement een zeer nuttig is. Een foreach loop zal ieder element in de array een voor een in een tijdelijke variabele plaatsen (de iteration variable). Volgende code toont de werking waarbij we een array van string
s hebben en alle elementen er in op het scherm willen tonen:
De eerste keer dat we in de loop gaan zal het element boodschappen[0]
aan boodschap
toegewezen worden voor gebruik in de loop-body, vervolgens wordt boodschappen[1]
toegewezen, enz.
Het voordeel is dat je dus geen teller/index nodig hebt en dat foreach zelf de lengte van de array zal bepalen.
De foreach iteration variable is read-only: je kan dus geen waarden in de array aanpassen, enkel uitlezen.
De foreach gebruik je enkel als je alle elementen van een array wenst te benaderen. In alle andere gevallen zal je een ander soort loop (for, while, etc.) moeten gebruiken.
C# heeft een var
keyword. Je mag dit keyword gebruiken ter vervanging van het type (bv int) op voorwaarde dat de compiler kan achterhalen wat het type moet zijn.
Opgelet: het var
keyword is in deze cursus nooit nodig. Het vergemakkelijkt het schrijfwerk, want het wordt door de compiler vertaald in een specifiek type. Er zijn scenario's waarin het wel nodig is, maar die zijn meer geavanceerd ("anonieme types").
Het betekent niet hetzelfde als de var
van JavaScript. In JavaScript hoef je namelijk geen type vast te leggen voor variabelen en kan je dit doen:
In C# levert dit een compilatiefout. Met de eerste regel zeg je dat de compiler uit de rechterzijde mag afleiden dat var
hier vervangen kan worden door string
. Je kan geen waarde 3
in een variabele van type string
plaatsen, dus dit levert een compilatiefout.
Wanneer je de Visual Studio code snippet voor foreach gebruikt foreach [tab][tab]
dan zal deze code ook een var gebruiken voor de iteration variabele. De compiler kan aan de te gebruiken array zien wat het type van een individueel element in de array moet zijn. De foreach van zonet kan dus herschreven worden naar:
De standaard datastructuren van C# zijn reference types. Dit betekent dat iedereen die zo'n datastructuur te pakken krijgt (bijvoorbeeld omdat je hem als argument meegeeft aan een methode), de inhoud van deze datastructuur ook kan wijzigen. Dit kan met opzet of gewoonweg per vergissing gebeuren.
Hoezo, "met opzet"? Denk eraan dat je typisch niet de enige programmeur bent die met bepaalde code in contact komt.
Bijvoorbeeld:
In het algemeen geldt: als iemand bepaalde mogelijkheden niet echt nodig heeft, geef ze dan niet. Dit is opnieuw encapsulatie.
Om te verhinderen dat een datastructuur wordt aangepast, kan je er een immutable versie van maken. Dit is een versie van die datastructuur waarvan de inhoud achteraf niet gewijzigd kan worden. Er bestaan immutable versies van de standaard datastructuren en ze heten gewoonweg ImmutableList<T>
en ImmutableDictionary<K,V>
.
Om deze versies te gebruiken, moet je de System.Collections.Immutable
namespace gebruiken. Wanneer je hier een using
directief voor hebt staan, kan je methodes zoals ToImmutableList<T>
oproepen op een lijst om er een immutable versie van te produceren. Deze immutable versie kan je dan veilig delen met code waarvan je niet wenst dat ze de inhoud van je lijst aanpast.
Een tweede manier om een immutable datastructuur te maken, is met een builder. Dit is een object waar je via Add
data aan toevoegt en dat je achteraf vraagt een immutable list te produceren. Je kan er een aanmaken met de statische methode CreateBuilder
van de immutable datastructuur die je wil gebruiken. Bijvoorbeeld:
Beginnende programmeurs denken soms dat ze hetzelfde effect kunnen verkrijgen door een property voor een datastructuur "read only" te maken. Dit doen ze dan door alleen een getter te voorzien en geen setter of, als ze buiten deze cursus gaan zoeken, met het sleutelwoordje readonly
.
Dit maakt je datastructuur niet immutable! Het zorgt er wel voor dat je het object op de heap waarin je data staat niet kan vervangen. Het zorgt er niet voor dat je de inhoud van dat object niet kan vervangen.
Bijvoorbeeld, als we personen voorzien van een array met lievelingsgerechten:
Onderstaande figuur toont een vereenvoudigde weergave van wat er aan de hand is: