Object Oriented Programming of korter OOP is een techniek afkomstig van higher level programmeertalen zoals Java, C#, VB.NET, ... en vindt zijn oorsprong bij de programmeertaal Smalltalk, die het eerst de term Object Oriented Programming introduceerde.
In recentere jaren heeft deze techniek echter ook zijn weg gevonden naar scripting talen zoals Python, Ruby, Perl en zelfs PHP.
OOP streeft ernaar om een project zo structureel mogelijk op te bouwen in objecten. Dit heeft voor de programmeur het grote voordeel dat code vanaf nu in logische componenten wordt opgedeeld en veel makkelijker te hergebruiken is.
Om het concept van objecten te illustreren wordt vaak een voorwerp uit het dagelijks leven als voorbeeld gebruikt. Neem bijvoorbeeld een auto. De auto is het object en dit object heeft zowel geassocieerde data als functionaliteit. Deze data stellen eigenschappen of onderdelen van het object voor. Een eigenschap van de auto kan de kleur, de lengte, het aantal kilometers op de teller, of zelfs zijn huidige locatie of snelheid zijn. Deze zaken worden geïmplementeerd met ofwel instantievariabelen, ofwel properties. De functionaliteit is iets dat het object kan doen. Deze wordt geïmplementeerd met instantiemethodes. Als je een auto als object ziet, kunnen deze bijvoorbeeld starten, versnellen of remmen zijn.
Auto's zijn een veelgebruikt voorbeeld, maar eigenlijk schrijf je er zelden objecten voor tenzij je een soort simulator wil maken. Objecten hoeven niet altijd tastbare zaken te zijn. Je kan bijvoorbeeld ook een lening bij de bank voorstellen als een object. Dit object heeft dan onder andere volgende eigenschappen:
een begunstigde, d.w.z. de persoon die de lening heeft aangegaan
een restsaldo, d.w.z. het bedrag dat je moet betalen
een looptijd, d.w.z. de periode waarbinnen de lening afbetaald moet zijn
een intrestvoet, die uiteindelijk bepaalt hoe veel je bovenop het oorspronkelijke bedrag betaalt
Maar met een lening kunnen we ook bepaalde zaken doen die we wel in een computersysteem zouden implementeren. Deze zaken vormen de functionaliteit, dus de verzameling instantiemethodes, van het lening-object:
een afbetaling verwerken
de uiteindelijk af te betalen som berekenen
het afbetaald kapitaal en de afbetaalde interest na een bepaald aantal maanden berekenen
Dus voor zaken die (minstens) zo uiteenlopend zijn als een auto en een lening hebben we een combinatie van data en functionaliteit. Zo zit een object in een programmeertaal er ook uit.
Een belangrijk concept bij OOP is het black-box principe waarbij we de afzonderlijke objecten en hun werking als "zwarte dozen" gaan beschouwen. Dat wil zeggen dat we (meestal) niet gaan kijken hoe ze in elkaar zitten. Neem het voorbeeld van de auto: deze is in de echte wereld ontwikkeld volgens het blackbox-principe. De werking van de auto kennen tot in het kleinste detail is niet nodig om met een auto te kunnen rijden. De auto biedt een aantal zaken aan de buitenwereld aan (het stuur, pedalen, het dashboard), wat we de publieke interface of gewoonweg interface noemen. Deze zaken kan je gebruiken om de interne staat van de auto uit te lezen of te manipuleren. Stel je voor dat je moest weten hoe een auto volledig werkte voor je ermee op de baan kon.
Binnen OOP wordt dit blackbox-concept encapsulatie genoemd. Het doel van OOP is andere programmeurs (en jezelf) zoveel mogelijk af te schermen van de interne werking van je code. Net als bij een auto, moet je alleen weten wat elk stukje van de interface klaarspeelt, maar hoef je niet te weten hoe dit allemaal is geprogrammeerd.
Een elementair aspect binnen OOP is het verschil beheersen tussen een klasse en een object.
Wanneer we meerdere objecten gebruiken van dezelfde soort dan kunnen we zeggen dat deze objecten allemaal deel uitmaken van een zelfde klasse. Zo hebben we bijvoorbeeld de klasse van de leningen. Er zijn leningen van €50.000, er zijn leningen van €10.000.000. Er zijn er met een intrestvoet van 0.2% en er zijn er met een intrestvoet van 2%. Er zijn er met een looptijd van 5 jaar en er zijn er met een looptijd van 20 jaar. Het zijn allemaal leningen, maar ze hebben allemaal andere eigenschappen.
Objecten van dezelfde soort volgen wel dezelfde regels en hebben in dat opzicht dezelfde functionaliteit. Maar de eigenschappen van object bepalen ook het resultaat van de instantiemethodes. Eerder zagen we dat de uiteindelijk af te betalen som berekend kan worden door een lening. Deze som zal per definitie hoger zijn van een lening met een hoge rentevoet dan voor een lening met een lage rentevoet (als de andere eigenschappen gelijk zijn).
Samengevat:
Een klasse is een beschrijving en verzameling van dingen (objecten) met soortgelijke eigenschappen en gedrag.
Een individueel object is een instantie (exemplaar, voorbeeld, verschijningsvorm) van een klasse
In C# kunnen we geen objecten aanmaken voor we een klasse hebben gedefinieerd die de algemene eigenschappen (properties) en werking (methoden) beschrijft.
Een heel elementaire klasse heeft de volgende vorm:
class ClassName
{
// hier komen de data en functionaliteit
}
We maken hier normaal gebruik van Pascal case. Je kan ook nog enkele toevoegingen doen die niet zuiver data en functionaliteit zijn, maar die houden we voor verder in de cursus.
Volgende code beschrijft de klasse Auto
in C#
class Auto
{
// data kan bv. nummerplaat, bouwjaar of kilometerstand zijn
// gedrag kan bv. bepalen van de verkoopwaarde zijn
}
Binnen het codeblock dat bij deze klasse hoort zullen we verderop dan de werking beschrijven.
Je kan "eender waar" een klasse aanmaken, maar het is een goede gewoonte om per klasse een apart bestand te gebruiken. Dit is ook de afspraak die wij zullen volgen.
In de solution explorer, rechterklik op je project
Kies Add
Kies Class...
Geef een goede naam voor je klasse
De naam van je klasse moet voldoen aan de identifier regels die ook gelden voor het aanmaken van variabelen!
Je kan nu objecten aanmaken van de klasse die je hebt gedefinieerd. De klasse is een type en je kan dus ook variabelen aanmaken die bedoeld zijn om objecten van deze klasse in bij te houden. Je doet dit door eerst een variabele te definiëren met als type de naam van de klasse en vervolgens een object te instantiëren met behulp van het new
keyword:
Auto mijnEerste = new Auto();
Auto mijnAndereAuto = new Auto();
We hebben nu twee objecten aangemaakt van het type Auto.
Let goed op dat je dus op de juiste plekken dit alles doet (bekijk de onderstaande screenshot):
Klassen maak je aan als aparte files in je project
Objecten creëer je in je code op de plekken dat je deze nodig hebt, bijvoorbeeld in je Main
methode bij een Console-applicatie
Het .NET gegevenstype DateTime
is de ideale manier om te leren werken met objecten. Het is een nuttig en toegankelijk gegevenstype. Je kan er je iets bij voorstellen, maar het is ook een beetje abstract.
Waarom spreken we hier over "gegevenstype" en niet over "klasse"? Omdat klassen in .NET reference types zijn. DateTime
is echter een value type, dus technisch gezien is het een "struct" en geen "klasse". Wij zullen zelf geen structs schrijven, maar het verschil met klassen is uiterst klein in C#. Instanties van zowel klassen als structs zijn objecten.
Zegt het verschil tussen value types en reference types je niets meer? Kijk dan terug naar deze pagina.
Er zijn 2 manieren om DateTime
objecten aan te maken:
Door aan de klasse de huidige datum en tijd te vragen via DateTime.Now
Door manueel de datum en tijd in te stellen via de constructor
Volgend voorbeeld toont hoe we een object kunnen maken dat de huidige datum tijd van het systeem bevat. Vervolgens printen we dit op het scherm:
DateTime currentTime = DateTime.Now;
Console.WriteLine(currentTime);
DateTime
op het begin van regel 1 is het type van de variabele. Dit type drukt uit met wat voor data we te maken hebben. Dit is net dezelfde redenering als bij het gebruik van string
in string mijnTekst = "hello world";
Via de constructor kunnen we beginwaarden meegeven bij het maken van een nieuw object. Er zijn 11 manieren waarop dit kan zoals je hier kan zien.
Enkele voorbeelden:
DateTime birthday = new DateTime(1982, 3, 18); //year, month, day
DateTime someMomentInTime = new DateTime(2017, 1, 18, 10, 16,34 ); //year, month, day, hour, min, sec
Ieder DateTime
object dat je aanmaakt heeft een hoop nuttige methoden.
Deze methoden kan je gebruiken om een bepaalde aantal dagen, uren, minuten en zo voort aan je huidige object toe te voegen. Al deze methoden geven steeds een nieuw DateTime object terug dat je moet bewaren wil je er iets mee doen:
AddDays
AddHours
AddMilliseconds
AddMinutes
AddMonths
AddSeconds
AddTicks
AddYears
Een voorbeeld:
DateTime timeNow= DateTime.Now;
DateTime nextWeek= timeNow.AddDays(7);
(voorgaande kan ook in 1 lijn: DateTime nextWeek= DateTime.Now.AddDays(7)
)
Uiteraard mag je ook een bestaand object overschrijven met het resultaat van deze methoden:
DateTime someTime= new DateTime(2019, 4, 1);
// much later...
someTime = someTime.AddYears(10);
Console.WriteLine(someTime);
Properties zijn een zeer uniek aspect van C#. Ze geven toegang tot de data van een object, maar op een gecontroleerde manier. We zullen deze nog tot in den treure leren maken.
Enkele nuttige properties van DateTime
zijn:
Date
Day
DayOfWeek
DayOfYear
Hour
Millisecond
Minute
Month
Second
Ticks
TimeOfDay
Today
UtcNow
Year
Alle properties van DateTime zijn read-only. Je kan een bestaande datum dus niet aanpassen (maar je kan wel een nieuwe datum baseren op een bestaande datum).
Een voorbeeld:
DateTime moment = new DateTime(1999, 1, 13, 3, 57, 32, 11);
// Year gets 1999.
int year = moment.Year;
// Month gets 1 (January).
int month = moment.Month;
// Day gets 13.
int day = moment.Day;
// Hour gets 3.
int hour = moment.Hour;
// Minute gets 57.
int minute = moment.Minute;
// Second gets 32.
int second = moment.Second;
// Millisecond gets 11.
int millisecond = moment.Millisecond;
Uiteraard mag je ook deze properties gebruiken om direct naar het scherm te schrijven:
DateTime now = DateTime.Now;
Console.WriteLine($"The current day is {now.DayOfWeek}");
Je hebt een invloed op hoe DateTime
objecten naar string
worden opgezet. Omzetten naar een string is functionaliteit van het object. Je doet dit dus via een objectmethode. Deze gebruik je zoals de statische methoden van eerder, maar je roept ze op voor een specifiek object.
Je kan bepalen hoe de omzetting naar string
moet gebeuren door extra formatter syntax mee te geven. Dit zie je in volgende voorbeeld:
DateTime now = DateTime.Now;
WriteLine(now.ToString("d")); // short date
WriteLine(now.ToString("D")); // long date
WriteLine(now.ToString("F")); // full date and time
WriteLine(now.ToString("M")); // month and day
WriteLine(now.ToString("o")); // date en time separated by T and time zone at the end
WriteLine(now.ToString("R")); // RFC1123 date and time
WriteLine(now.ToString("t")); // short time
WriteLine(now.ToString("T")); // long time
WriteLine(now.ToString("Y")); // year and month
Wil je nog meer controle over de output dan kan je ook zelf je formaat specifieren.
De manier waarop DateTime
objecten worden getoond (via ToString
) is afhankelijk van de landinstellingen van je systeem. Soms wil je dit echter op een andere manier tonen. Je doet dit door mee te geven volgens welke culture de tijd en datum getoond moet worden.
Dit vereist dat je eerst een CultureInfo
object aanmaakt en dat je dan meegeeft:
DateTime now = DateTime.Now;
CultureInfo russianCI = new CultureInfo("ru-RU");
Console.WriteLine($"Current time in Russian style is: {now.ToString("F", russianCI)}");
CultureInfo
is een klasse waar je je misschien al iets minder bij kan voorstellen dan DateTime
. Een CultureInfo
stelt een aantal afspraken voor een bepaalde cultuur voor. De data is de combinatie van taal en regio. De functionaliteit bestaat er dan in om zaken zoals de munt, het gebruikte formaat voor datums,... in die taal en die regio te produceren. Ook hier geldt dus het black box principe: je kan code schrijven die bijvoorbeeld het juiste datumformaat voor Rusland gebruikt wanneer je zelf niet weet wat dat formaat is.
Sommige methoden zijn static
dat wil zeggen dat je ze enkel rechtstreeks op de klasse kunt aanroepen. Vaak zijn deze methoden hulpmethoden waar de individuele objecten niets aan hebben.
Parsen laat toe dat je strings omzet naar DateTime
. Dit is handig als je bijvoorbeeld de gebruiker via ReadLine
tijd en datum wilt laten invoeren:
string date_string = "8/11/2016"; //dit zou dus ook door gebruiker kunnen ingetypt zijn
DateTime dt = DateTime.Parse(date_string);
Console.WriteLine(dt);
Zoals je ziet roepen we Parse
aan op DateTime
en dus niet op een specifiek object. Dit is logisch omdat je geen bestaande datum nodig hebt om een nieuwe datum te parsen.
Deze nuttige methode geeft een bool
terug om aan te geven het meegegeven object eens schrikkeljaar is of niet:
DateTime today = DateTime.Now;
bool isLeap = DateTime.IsLeapYear(today.Year);
if(isLeap == true) {
Console.WriteLine("This year is a leap year");
}
Dit is logisch omdat je geen volledige bestaande datum nodig hebt. Je wil gewoon iets zeggen over een jaartal, terwijl een DateTime
ook een dag, maand, uur,... heeft. Dus dit heeft niet veel te maken met een specifieke DateTime
, maar heeft duidelijk wel met de klasse te maken.
Je kan DateTime objecten ook bij mekaar optellen en aftrekken. Deze bewerking geeft echter geen DateTime
object terug, maar een TimeSpan
object. Dit is een object dat dus aangeeft hoe groot het verschil is tussen de 2 DateTime
objecten:
DateTime today = DateTime.Today;
DateTime borodino_battle = new DateTime(1812, 9, 7);
TimeSpan diff = today - borodino_battle;
WriteLine("{0} days have passed since the Battle of Borodino.", diff.TotalDays);
Met klassen kan je dus complexe concepten zoals een lening modelleren in code. Soms hoeft het niet zo complex, maar is het toch nuttig om wat meer structuur te voorzien dan met heel brede datatypes zoals int en string. Met een string kan je bijvoorbeeld om het even welke tekst voorstellen, dus oneindig veel mogelijkheden. Met een byte kan je een getal tussen 0 en 255 voorstellen. Met de andere getaltypes kan je een getal in een ander bereik voorstellen. Soms matchen de waarden die je kan voorstellen niet met de mogelijkheden in het probleemdomein.
Als je de mogelijkheden in het probleemdomein op voorhand kan vastleggen, is het vaak handig gebruik te maken van een enumeratie (enum). Een enumeratie is een opsomming van alle mogelijke waarden voor een variabele van een bepaald type.
Stel dat je een programma moet schrijven dat afhankelijk van de dag van de week iets anders moet doen. Zonder enums zou je dit kunnen schrijven op 2 zeer foutgevoelige manieren:
Met een int
die een getal van 1 tot en met 7 kan bevatten
Met een string
die de naam van de dag bevat
Beide hebben tekortkomingen, omdat ze niet goed genoeg overeenstemmen met de mogelijkheden in de werkelijkheid.
De waarde van de dag staat in een variabele int dagKeuze
. We bewaren er 1 in voor Maandag, 2 voor dinsdag, enzovoort.
if(dagKeuze==1)
{
Console.WriteLine("We doen de maandag dingen");
}
else
if (dagKeuze==2)
{
Console.WritLine("We doen de dinsdag dingen");
}
else
if //enz..
Deze oplossing heeft 2 grote nadelen:
Wat als we per ongeluk dagKeuze
een niet geldige waarde geven, zoals 9, 2000, -4, etc. ? We kunnen daar wel conditionele code voor voorzien, maar de compiler zal ons niet waarschuwen als we dat vergeten!
De code is niet erg leesbaar. dagKeuze==2
? Was dat nu maandag, dinsdag of woensdag (want misschien beginnen we te tellen vanaf zondag, of misschien beginnen we te tellen vanaf 0). Je kan wel afspraken maken binnen je team, maar het is altijd mogelijk een afspraak te vergeten.
De waarde van de dag bewaren we nu in een variabele string dagKeuze
. We bewaren de dagen als "maandag"
, "dinsdag"
, etc.
if(dagKeuze=="maandag")
{
Console.WriteLine("We doen de maandag dingen");
}
else
if (dagKeuze=="dinsdag")
{
Console.WritLine("We doen de dinsdag dingen");
}
else
if //enz..
De code wordt nu wel leesbaarder dan met 1, maar toch is ook hier 1 groot nadeel:
De code is veel foutgevoeliger voor typfouten. Wanneer je "Maandag"
i.p.v. "maandag"
bewaart dan zal de if
al niet werken. Iedere schrijffout of variant zal falen.
Het keyword enum
geeft aan dat we een nieuw datatype maken. Gelijkaardig aan wat we voor een klasse doen, dus. Wanneer we dit nieuwe type hebben gedefinieerd kunnen we dan ook variabelen van dit nieuwe type aanmaken (dat is waarom we spreken van een "datatype"). Anders dan bij een klasse beperken we tot alleen de opgesomde opties.
In C# zitten al veel enum
-types ingebouwd. Denk maar aan ConsoleColor
: wanneer je de kleur van het lettertype van de console wilt veranderen gebruiken we een enum-type. Er werd reeds gedefinieerd wat de toegelaten waarden zijn, bijvoorbeeld: Console.ForegroundColor = ConsoleColor.Red;
Achter de schermen worden waarden van een enum type nog steeds voorgesteld als getallen. Maar je gebruikt deze getallen niet rechtstreeks en dat verkleint de risico's.
Zelf een enum
type gebruiken gebeurt in 2 stappen:
Het type en de mogelijke waarden definiëren
Variabele(n) van het nieuwe type aanmaken en gebruiken in je code
Stap 1: het type definiëren
We maken eerst een enum type aan. Wij zullen dit doen in een aparte file met dezelfde naam als het nieuwe datatype. Bijvoorbeeld:
namespace Programmeren {
enum Weekdagen {Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag, Zondag}
}
Vanaf nu kan je variabelen van het type Weekdagen
aanmaken. Merk op dat er geen puntkomma voorkomt. De syntax is ongeveer dezelfde als die voor de definitie van een klasse.
Stap 2: variabelen van het type aanmaken en gebruiken.
We kunnen nu variabelen van het type Weekdagen
aanmaken. Bijvoorbeeld:
Weekdagen dagKeuze;
Weekdagen andereKeuze;
En vervolgens kunnen we waarden aan deze variabelen toewijzen als volgt:
dagKeuze = Weekdagen.Donderdag;
Kortom: we hebben variabelen zoals we gewoon zijn, het enige verschil is dat we nu beperkt zijn in de waarden die we kunnen toewijzen. Deze kunnen enkel de waarden zijn die in het type gedefinieerd werden. De code is nu ook een pak leesbaarder geworden.
Eens je met enums aan het werken bent in je programma, kan je gerust zijn dat deze een geldige waarde bevatten. Er is alleen het probleem dat je soms een van de mogelijkheden moet krijgen van de gebruiker. Een waarde van een enum type rechtstreeks intypen gaat niet. Net zoals wanneer je een getal inleest, is er een omzetting nodig. Het verschil is dat we hier een dubbele omzetting doen: van tekst naar getal en dan van getal naar enum waarde.
Je kan dit als volgt doen:
Weekdagen keuze;
Console.WriteLine("Welke dag is het vandaag?");
Console.WriteLine($"{(int) Weekdagen.Maandag}. {Weekdagen.Maandag}");
Console.WriteLine($"{(int) Weekdagen.Dinsdag}. {Weekdagen.Dinsdag}");
// enzovoort
keuze = (Weekdagen) Convert.ToInt32(Console.ReadLine());
Dit werkt en je hoeft de achterliggende getallen voor de waarden niet te kennen. Er zijn meer geavanceerde mogelijkheden, maar dit volstaat op dit punt.
De data en functionaliteit van een klasse, en ook haar relatie tot andere klassen, wordt vaak voorgesteld in een UML-klassendiagram. Hoe je verschillende klassen met elkaar verbindt, houden we voor iets verderop. Ook enkele andere details volgen later. Wat we wel al kunnen vertellen:
elke klasse wordt voorgesteld als een rechthoek, met bovenaan in die rechthoek de naam van de klasse
in een tweede vakje worden dan de eigenschappen gegeven, gewoonlijk met hun datatype
in een derde vakje worden dan de methoden gegeven, met hun parameters en hun returntype
Dit vertelt ons dat een object van klasse Lening
beschikt over een saldo en een intrestvoet en het vertelt ons ook welke datatypes we gebruiken om deze voor te stellen. Bovendien kan een Lening
zelf bepalen hoe veel we in totaal zullen moeten betalen als we elke maand een vast bedrag aflossen. Dit is functionaliteit van het object, die we zullen implementeren met een instantiemethode.
Dit vertelt ons dat elke Auto
een eigen kilometerstand, bouwjaar en huidige snelheid heeft. Het is mogelijk de Auto
te laten starten en om hem te laten versnellen met een geven aantal kilometer per uur.
Een UML-klassendiagram dient voor... klassen. Als je wil kijken naar objecten, gebruik je een sequentiediagram. We gaan hier nog niet erg diep op in, maar we maken alvast duidelijk dat je op een klassendiagram geen objecten ziet.
Een voorbeeld van een sequentiediagram voor een programma dat twee leningen aanmaakt (bijvoorbeeld een programma in een bank om een simulatie te maken voor een klant):
Het figuurtje met als naam ExterneCode
stelt iets voor dat ons eigenlijk niet interesseert. Wat de pijlen en de berichten daarop technisch betekenen, maakt voorlopig ook niet zo uit. Het belangrijkste hier is dat er twee objecten van de klasse Lening
zijn. Dit wordt extra in de verf gezet omdat ze twee verschillende namen hebben (lening1
en lening2
), maar allebei hetzelfde type na de dubbele punt (Lening
). Op een klassediagram ga je nooit twee keer een box voor dezelfde klasse zien.
Kennisclip (let op: de demonstratie SchoolAdmin is verouderd en wordt nu anders aangepakt. De tekst heeft voorrang!)
Attributen, ook velden of instantievariabelen genoemd, zijn stukjes data die je bijhoudt in objecten. Ze stellen informatie voor die deel uitmaakt van een (object van een) klasse. Ze werken zoals de variabelen die je al kent, maar hun scope is een klasse of een object van een klasse, afhankelijk van de vraag of ze static
zijn of niet. Door gebruik te maken van attributen, kunnen we stukjes data die samen horen ook samen houden op het niveau van de code. Alle data die samen hoort netjes groeperen en op een gestructureerd toegankelijk maken valt onder het begrip encapsulatie dat reeds eerder aan bod kwam.
Een typisch voorbeeld van een klasse is Auto
. Er zijn verschillende stukjes data die deel kunnen uitmaken van één auto: de kilometerstand, het benzinepeil, de datum van het laatste onderhoud,...
Een reeds gekende manier om verwante informatie bij te houden is met behulp van "gesynchroniseerde" arrays, d.w.z. arrays die verwante data bijhouden op overeenkomstige posities. Onderstaand voorbeeld toont dit voor auto's:
class Program {
public static void Main() {
int aantalAutos = 3;
int[] kilometers = new int[aantalAutos];
double[] benzine = new double[aantalAutos];
DateTime[] onderhoud = new DateTime[aantalAutos];
for (int i = 0; i < aantalAutos; i++) {
Console.WriteLine($"Kilometerstand van auto {i+1}?");
kilometers[i] = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Benzinepeil van auto {i+1}?");
benzine[i] = Convert.ToDouble(Console.ReadLine());
Console.WriteLine($"Jaar recentste onderhoud auto {i+1}?");
int jaar = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Maand recentste onderhoud auto {i+1}?");
int maand = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Dag recentste onderhoud auto {i+1}?");
int dag = Convert.ToInt32(Console.ReadLine());
onderhoud[i] = new DateTime(jaar,maand,dag);
}
// later in de code
for (int i = 0; i < aantalAutos; i++) {
PrintOnderhoudsrapport(kilometers[i],benzine[i],onderhoud[i]);
}
}
}
Als we nu een methode willen uitvoeren die iets doet met alle informatie over een auto (bijvoorbeeld een onderhoudsrapport afprinten), moeten we drie waarden meegeven. Het is ordelijker deze zaken bij te houden in een object van klasse Auto
als volgt:
class Auto {
public int Kilometers;
public double Benzine;
public DateTime LaatsteOnderhoud;
}
Al deze velden zijn voorlopig public
. Dat hoeft niet absoluut, maar het vergemakkelijkt de presentatie. Verdere opties volgen snel. Het is ook een afspraak om publieke velden met een hoofdletter te schrijven. Met deze voorstelling kunnen we dit nu doen:
class Program {
public static void Main() {
int aantalAutos = 3;
Auto[] autos = new Auto[aantalAutos];
for (int i = 0; i < aantalAutos; i++) {
Auto nieuweAuto = new Auto();
autos[i] = nieuweAuto;
Console.WriteLine($"Kilometerstand van auto {i+1}?");
nieuweAuto.Kilometers = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Benzinepeil van auto {i+1}?");
nieuweAuto.Benzine = Convert.ToDouble(Console.ReadLine());
Console.WriteLine($"Jaar recentste onderhoud auto {i+1}?");
int jaar = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Maand recentste onderhoud auto {i+1}?");
int maand = Convert.ToInt32(Console.ReadLine());
Console.WriteLine($"Dag recentste onderhoud auto {i+1}?");
int dag = Convert.ToInt32(Console.ReadLine());
nieuweAuto.LaatsteOnderhoud = new DateTime(jaar,maand,dag);
}
// later in de code
for (int i = 0; i < aantalAutos; i++) {
PrintOnderhoudsrapport(autos[i]);
}
}
}
Je ziet hier al enkele voordelen van encapsulatie:
je hoeft niet bij te houden hoe de informatie verspreid is over meerdere plaatsen
als we meer informatie over auto's (bv. het oliepeil) in het onderhoudsrapport steken, hoeven we onze calls van PrintOnderhoudsrapport
niet aan te passen
een kenmerk van goede code is dat wijzigingen typisch geen grote wijzigingen vereisen
Een veld krijgt normaal de defaultwaarde voor zijn type. Defaultwaarden hebben we reeds gezien. Het is mogelijk de beginwaarde aan te passen met de syntax voor een toekenning:
class Auto {
public int Kilometers = 5; // in de fabriek vinden bv. een aantal testen plaats
public double Benzine = 10; // nieuwe auto's moeten kunnen rijden
public DateTime LaatsteOnderhoud = DateTime.Now;
}
Nu hebben nieuwe auto's standaard 5 km op de teller staan, enzovoort. Merk op: deze waarde wordt voor elk nieuw object opnieuw berekend. Als je dus twee auto's aanmaakt in je programma, zullen zij beide een verschillende datum van het laatste onderhoud bevatten.
static
attributenIets dat static
is, hoort niet bij de objecten, maar wel bij de hele klasse. Bijvoorbeeld: voor auto's worden de milieunormen na een paar jaar strenger. Er is een vaste set milieunormen en de te halen milieunorm wordt vastgelegd voor alle auto's. Daarom zouden we de milieunorm als volgt kunnen bijhouden:
enum MilieuNormen {
Euro1, Euro2, Euro3, Euro4, Euro5, Euro6
}
class Auto {
public static MilieuNormen HuidigeNorm;
// rest van de code voor Auto
}
Herhaal: static
betekent niet "onveranderlijk" of "vast". Het betekent dat iets op niveau van de klasse werkt en niet op niveau van de objecten van die klasse.
Deze opdracht maak je tijdens de les.
We willen een programma maken dat ons helpt om in een school te beheren welke studenten ingeschreven zijn, welke cijfers behaald werden, enzovoort.
We doen dit in een apart project SchoolAdmin
.
We maken een klasse student, met publieke attributen voor de naam, geboortedatum, het studentennummer en de gevolgde cursussen. We houden ook bij hoeveel studenten er zijn via een attribuut StudentCounter
dat de beginwaarde 1 krijgt.
Schrijf in de Main
de code om 2 objecten te maken:
said: Said Aziz, geboren 1 juni 2000 met studentennummer 1; Said volgt Programmeren en Databanken
mieke: Mieke Vermeulen, geboren 1 januari 1998 met studentennummer 2; Mieke volgt Communicatie
De studentennummers krijgen een waarde door een toekenning van de StudentCounter
dat die je na elke toekenning met één verhoogt.
Instantiemethoden, ook objectmethoden genoemd, weerspiegelen staan toe om functionaliteit toe te voegen aan objecten van een bepaalde klasse. Soms wordt ook gezegd dat ze "gedrag" van de objecten voorzien. Ze verschillen van statische methoden omdat ze niet alleen gebruik kunnen maken van statische onderdelen van klassen, maar ook van het object waar ze zelf bij horen.
We gaan verder met de klasse Auto
. We willen bijvoorbeeld een applicatie voor de opvolging van deelauto's (Cambio, Poppy, etc.) schrijven. Er zijn verschillende soorten functionaliteit die je kan koppelen aan één auto:
voltanken
rijden
op onderhoud gaan
verkoopsprijs bepalen
Je doet dit met objectmethoden. Deze lijken erg op static
methoden, maar ze hebben toegang tot het object waarop ze zijn toegepast.
Een simpele implementatie van dit gedrag zie je hier:
class Auto {
// objectvariabelen van eerder zijn er nog
public void Voltanken()
{
Benzine = 50.0; // we veronderstellen even dat dat het maximum is
}
public void Rijden(int aantalKilometers)
{
Kilometers += aantalKilometers;
Benzine -= 5.0 * (aantalKilometers/100.0);
}
public void Onderhouden()
{
LaatsteOnderhoud = DateTime.Now;
}
public double VerkoopsprijsBepalen()
{
return Math.Max(10000 * (1 - Kilometers / 200000.0),1000);
}
}
Bovenstaande code is kort om didactische redenen. Er wordt niet gecontroleerd dat je benzinepeil altijd minstens 0l is, er wordt verondersteld dat de capaciteit van je tank 50l is,...
Om een objectmethode te gebruiken, hebben we een object nodig. We schrijven dan de naam van het object, gevolgd door een punt en een methodeoproep.
// in Program.cs
public static void DemonstreerAttributen() {
Auto auto1 = new Auto();
Auto auto2 = new Auto();
auto1.Voltanken();
auto1.Rijden(5);
auto1.Rijden(10);
auto1.Rijden(20);
Console.WriteLine(auto1.Kilometers);
Console.WriteLine(auto2.Kilometers);
}
Het gedrag van een object kan afhangen van de waarde van de instantievariabelen. Zo zal de verkoopswaarde van auto1
iets lager liggen dan die van auto2
. Dat komt omdat this.Kilometers
deel uitmaakt van de berekening van de verkoopsprijs. Ook dit valt onder het principe van encapsulatie: er vindt een berekening plaats "onder de motorkap". We hoeven niet te weten hoe de prijs berekend wordt, elk object weet van zichzelf hoe het de prijs berekent.
static
methodesEen statische methode is een methode die wel bij de klasse hoort, maar niet te maken heeft met een specifiek object van die klasse. We gebruiken terug de euronorm als voorbeeld:
enum MilieuNormen {
Euro1, Euro2, Euro3
}
class Auto {
public static MilieuNormen HuidigeNorm;
// rest van de code voor Auto
public static void VerklaarNorm(MilieuNormen norm) {
int jaartal;
double CO;
switch (norm) {
case MilieuNormen.Euro1:
jaartal = 1992;
CO = 1.0;
break;
case MilieuNormen.Euro2:
jaartal = 1996;
CO = 1.0;
break;
case MilieuNormen.Euro3:
jaartal = 2000;
CO = 0.64;
break;
default:
jaartal = -1;
CO = -1;
break;
}
Console.WriteLine($"Geïntroduceerd in {jaartal}");
Console.WriteLine($"{CO} gram CO per km");
}
}
We breiden onze klasse uit met 2 methoden.
GenerateNameCard
toont de naam van de student gevolgd door (STUDENT).
DetermineWorkload
berekent de werkbelasting van de student aan de hand van het aantal cursussen waarvoor zij/hij is ingeschreven. Er wordt gerekend met 10u per week per cursus.
Maak in de Main
een keuzemenu zoals hieronder.
Optie 1 roept een methode DemoStudents
op. Maak deze in Program.cs. Verplaats daarvoor de reeds bestaande code in de Main naar de nieuwe methode en vul ze aan met de oproepen naar de juiste methodes om bovenstaand resultaat te krijgen.
Access modifiers bepalen welke code door welke andere code mag worden uitgevoerd of aangepast. We hebben al een aantal dingen public
gemaakt bij wijze van demonstratie. Dit is handig om voorbeeldjes te tonen, maar in code van hoge kwaliteit, denk je grondig na voor je dit doet.
De access modifier geeft aan hoe zichtbaar een bepaald deel van de klasse is voor code buiten de klasse zelf. Wanneer je niet wilt dat "van buitenuit" (bv in Main
, terwijl je een andere klasse dan Program
schrijft) een bepaalde methode kan aangeroepen worden, dan dien je deze als private
in te stellen. Wil je dit net wel dat moet je er expliciet public
voor zetten.
Test in de voorgaande klasse Auto
eens wat gebeurt wanneer je public
verwijdert voor een van de methodes. Inderdaad, je krijgt een foutmelding. Lees deze. Ze zegt dat de methode die je wil oproepen wel bestaat, maar niet gebruikt mag worden. Dat komt omdat je testcode in de klasse Program
staat en je methode deel uitmaakt van een andere klasse (meerbepaald Auto
).
Als je duidelijk wil maken dat bepaalde code niet van buitenaf aangeroepen kan worden, schrijf dan private
in plaats van public
. Er zijn nog tussenliggende niveaus waar we later op ingaan en als je geen van beide modifiers schrijft, kan het zijn dat je code op zo'n tussenliggend niveau terecht komt. Als beginnende programmeur maak je er best een gewoonte van duidelijk te kiezen voor public
of private
.
Waarom zou je bepaalde zaken private
maken? Het antwoord is opnieuw encapsulatie.
Neem als voorbeeld de kilometerstand. Het is wettelijk verboden een kilometerstand zomaar aan te passen. Hij mag alleen veranderen doordat er met een auto gereden wordt. Dan is het logisch dat dit ook alleen maar op deze manier kan voor onze auto-objecten. We kunnen dit realiseren door kilometers
van public
naar private
te wijzigen. Dan kunnen we de kilometerstand nog wijzigen, maar enkel door de (publieke) methode Rijden
te gebruiken. Als we hem dan nog willen kunnen bekijken (maar niet rechtstreeks wijzigen), kunnen we een extra (publieke!) methode ToonKilometerstand
voorzien.
We kunnen ook methoden private
maken. Dit gebeurt niet zo vaak als bij attributen, maar het kan wel. Dit doen we bijvoorbeeld als het gaat om een hulpmethode die binnen de klasse nuttig is, maar buiten de klasse fout gebruikt zou kunnen worden. Een typisch voorbeeld: een stukje code dat letterlijk herhaald wordt in twee of meer verschillende publieke methoden. In plaats van deze code te kopiëren, zonderen we ze af in een hulpmethode. Zo hoeven we eventuele bugfixes,... geen twee keer te doen.
We willen Courses
afschermen en vermijden dat er dubbele inschrijvingen zijn; namelijk dat een student meermaals voor dezelfde cursus wordt ingeschreven.
Maak Courses
private.
In C# schrijven we private members in camel case. Daarom wordt het attribuut hernoemd.
Voeg een methode RegisterForCourse
toe. Enkel als de cursus waarvoor men wil registreren zich nog niet in courses
bevindt, wordt deze toegevoegd.
Zorg ervoor dat in DemoStudents
de inschrijvingen voor een cursus gebeuren met behulp van de nieuwe methode.
Properties zijn een feature van C♯ om de leesbaarheid van code te verhogen. Ze zien er uit zoals attributen, maar werken zoals methoden.
Kennisclip voor deze inhoud. De camerabeelden zijn wat wazig, maar de schermopname is in orde.
In dit hoofdstuk bespreken we eerst waarom properties nuttig zijn. Vervolgens bespreken we de 2 soorten properties die er bestaan:
Full properties
Auto properties
Stel dat we volgende klasse hebben:
public class Auto
{
private int kilometers;
private double benzine;
}
Stel nu dat we het benzinepeil van een auto als volgt proberen aanpassen:
Auto auto = new Auto();
auto.benzine += 10; //DIT ZAL DUS NIET WERKEN, daar benzine private is.
Misschien is de eerdere methode TankVol()
te beperkt en willen we wel een willekeurige hoeveelheid benzine kunnen toevoegen of verwijderen, zo lang we niet minder dan 0l of meer dan 50l in de tank doen.
Een eerste mogelijkheid is om hier methodes voor te schrijven:
public class Auto
{
private int kilometers;
private double benzine;
public double GetBenzine() {
return this.benzine;
}
public void SetBenzine(double waarde) {
if(waarde >= 0 && waarde <= 50) {
this.benzine = waarde;
}
}
}
Dit gaat. De methodes zijn public, dus we kunnen ze overal oproepen. Bovendien verhindert de SetBenzine
-methode ongeldige waarden. Het nadeel is dat de syntax om deze methodes te gebruiken zwaar is. Met publieke attributen konden we dit doen:
Auto auto = new Auto();
auto.Benzine += 10;
Met de zogenaamde getter en setter moeten we dit doen:
Auto auto = new Auto();
auto.SetBenzine(auto.GetBenzine() + 10);
Het lijkt niet zo veel, maar code stapelt zich op doorheen de tijd. Properties lossen dit probleem op. Ze zorgen ervoor dat we kunnen "doen alsof" we publieke velden hebben, maar dezelfde hoeveelheid controle kunnen uitoefenen als met getters en setters.
Een full property ziet er als volgt uit:
class Auto
{
private int kilometers;
private double benzine;
public double Benzine
{
get
{
return benzine;
}
set
{
benzine = value;
}
}
}
Dankzij deze code kunnen we nu elders dit doen:
Auto auto = new Auto();
auto.Benzine = 20; //set
Console.WriteLine($"Het benzinepeil is {auto.Benzine}"); //get
Vergelijk dit met de vorige alinea waar we dit met Get en Set methoden moesten doen. Deze property syntax is veel eenvoudiger in het gebruik.
We zullen de property nu stuk per stuk analyseren:
public double Benzine
: Merk op dat we Benzine
met een hoofdletter schrijven. Vaak wordt gekozen voor dezelfde naam als de variabele die we afschermen (in dit geval benzine
met een kleine "b"), maar dan in Pascal case. Dat is niet strikt noodzakelijk. Je zou ook Naft
kunnen schrijven. public
en double
staan er om dezelfde reden als in de oplossing met methodes GetBenzine()
en SetBenzine()
: we willen deze property buiten de klasse kunnen gebruiken en als we hem opvragen, krijgen we een double
.
{ }: Vervolgens volgen 2 accolades waarbinnen we de werking van de property beschrijven.
get {}
: indien je wenst dat de property data naar buiten moet sturen, dan schrijven we de get
-code. Binnen de accolades van de get
schrijven we wat er naar buiten moet gestuurd worden. In dit geval return benzine
maar dit mag even goed bijvoorbeeld return 4
of een hele reeks berekeningen zijn. Eigenlijk werkt dit net als de body van een methode en kan je hierin doen wat je in een methode kan doen.
We kunnen nu van buitenaf toch de waarde van benzine
onrechtstreeks uitlezen via de property en het get
-gedeelte: Console.WriteLine(auto.Benzine);
set {}
: in het set
-gedeelte schrijven we de code die we moeten hanteren indien men van buitenuit een waarde aan de property wenst te geven om zo een instantievariabele aan te passen. De waarde die we van buitenuit krijgen (eigenlijk is dit een parameter van een methode) zal altijd in een lokale variabele value
worden bewaard. Deze zal van het type van de property zijn. In dit geval dus double
, want het type bij Benzine
is double
. Vervolgens kunnen we value
toewijzen aan de interne variabele indien gewenst: benzine=value
.
Let goed op dat je in je setter schrijft benzine = value
en niet Benzine = value
. Dat eerste past de verborgen instantievariabele aan. Dat tweede roept de setter opnieuw op. En opnieuw. En opnieuw. Probeer gerust eens een breakpoint te plaatsen voor de toekenning en dan de debugger te starten als je niet ziet waarom dit een probleem is.
De full property Benzine
heeft nog steeds het probleem dat we negatieve waarden kunnen toewijzen (via de set
) die dan vervolgens zal toegewezen worden aan benzine
.
We kunnen in de set
code extra controles inbouwen. Als volgt:
public double Benzine
{
get
{
return benzine;
}
set
{
if(value >= 0 and value <= 50) {
benzine = value;
}
}
}
Deze code zal het benzinepeil enkel aanpassen als het geldig is en anders stilletjes niets doen. Wat je vaak tegenkomt is throw new ArgumentException($"{value} is geen geldig benzinepeil")
. Dit doet je programma crashen, maar legt ook uit waarom. We kunnen de code binnen set
(en get
) zo complex maken als we zelf willen.
Je kan dus extra controles toevoegen, maar deze hebben alleen zin als je de variabele via de property aanpast. Als je in een methode van de klasse auto benzine
met kleine "b" aanpast en niet voorzichtig bent, kan je nog steeds een negatief peil instellen. Daarom wordt aangeraden ook binnen de klasse gebruik te maken van de property, dus zo veel mogelijk Benzine
in plaats van benzine
te gebruiken.
We zijn niet verplicht om zowel de get
en de set
code van een property te schrijven.
Write-only property
public double Benzine
{
set
{
if(value >= 0) {
benzine = value;
}
}
}
We kunnen dus enkel benzine
een waarde geven, maar niet van buitenuit uitlezen.
public double Benzine
{
get
{
return benzine;
}
}
Read-only property met private set
Soms gebeurt het dat we van buitenuit enkel de gebruiker de property read-only willen maken. We willen echter intern (in de klasse zelf) nog steeds controleren dat er geen illegale waarden aan private datafields worden gegeven. Op dat moment definieren we een read-only property met een private setter:
public double Benzine
{
get
{
return benzine;
}
private set
{
if(value >= 0) {
benzine = value;
}
}
}
Van buitenuit zal enkel code werken die deget
-van deze property aanroept: Console.WriteLine(auto.Benzine);
. Code die de set
van buitenuit nodig heeft zal een fout geven zoals: auto.Benzine=40
.
Read-only Get-omvormers
Veel properties bieden toegang tot een achterliggende variabele van hetzelfde type, maar dat is niet verplicht. Je kan ook iets berekenen en dat teruggeven via een getter.
Als we verder gaan met de klasse Auto
:
public class Auto
{
private int kilometers;
private double benzine;
// stelt het aantal blokjes benzine voor op je display
// bij 50l heb je 5 blokjes
// bij tussen 40 en 50l heb je 4 blokjes
// ...
// bij minder dan 10l heb je 0 blokjes
public int Blokjes {
get {
return Math.Floor(this.benzine / 10);
}
}
}
Je hebt iets gelijkaardigs gezien bij DateTime
. Daar kan je allerlei stukjes informatie uit opvragen, maar eigenlijk wordt er maar één ding bijgehouden: het aantal "ticks". Dat zijn fracties van seconden sinds een bepaalde startdatum. Als je het uur of het aantal minuten of de maand of de weekdag of nog iets anders opvraagt via een DateTime
, wordt deze ter plekke berekend uit het aantal ticks. Zo moet maar één stukje informatie worden bijgehouden, wat leidt tot minder fouten.
Automatische eigenschappen (autoproperties) in C# staan toe om eigenschappen (properties) die enkel een waarde uit een private variabele lezen en schrijven verkort voor te stellen. Ze zorgen dat je al snel properties hebt, maar staan je niet toe complexe berekeningen te doen of waarden te controleren zoals full properties dat wel doen.
Voor Auto
kan je bijvoorbeeld schrijven:
public class Auto
{
public double Benzine
{ get; set; }
}
Dit maakt achter de schermen een privé-attribuut aan zoals benzine
met kleine "b", maar met een een verborgen naam. We kunnen dus niet rechtstreeks aan dat attribuut.
Wij zullen geen gebruik maken van autoproperties, omdat het verschil met publieke attributen pas in meer geavanceerde scenario's zichtbaar wordt. We geven ze hier mee zodat je ze kan herkennen als je ze tegenkomt.
Vanaf hier veronderstellen we dat je in één groot project (OOExercises
) werkt dat één klasse Program
heeft. Deze klasse heeft een Main
methode die een keuzemenu opstart. Oefeningen rond eenzelfde topic worden (statische) methodes van één klasse met een methode ShowSubmenu
, die je een menu toont van alle oefeningen over dat topic en die je toestaat een oefening naar keuze te testen. Dit wordt uitgelegd in de eerste oefening.
een ordelijke menustructuur voor je code voorzien
We willen dat we alle oefeningen die we in dit vak maken op een ordelijke manier kunnen opstarten. We doen dit door een keuzemenu met twee niveaus te voorzien: de gebruiker kan elke reeds geschreven oefening uitvoeren door eerst het algemene onderwerp aan te geven en vervolgens de specifieke oefening.
Maak een project OOExercises
.
Laat in je Main methode een lijst van alle topics zien waarover oefeningen gemaakt zijn. In het begin is dit enkel DateTime
. De gebruiker kan een topic aanduiden door een nummer in te geven, want voor elk topic staat ook een nummer.
Gebruik een switch op de gebruikersinput om te bepalen van welk topic de ShowSubmenu
methode moet worden opgeroepen. Deze methode heeft return type void
en geen parameters.
Voorzie een eerste klasse, DateTimeExercises
, met deze methode ShowSubmenu
. Totdat je oefeningen hebt om te demonstreren, toont ShowSubmenu
gewoonweg de tekst "Er zijn nog geen oefeningen over dit topic"
.
Indien er wel oefeningen zijn (deze oefening moet je dus updaten naarmate je vordert), wordt elke reeds geprogrammeerde oefening genummerd en getoond en kan de gebruiker kiezen om deze uit te voeren.
Nadat een oefening getest is, kan je opnieuw een topic en een oefening kiezen. Het programma eindigt nooit.
Dit is maar een voorbeeld! De getoonde topics en oefeningen gaan afhangen van wat je al gedaan hebt.
Welkom bij de oefeningen van Objectgeoriënteerd Programmeren!
Topic van de uit te voeren oefening?
1. DateTime
2. Properties en access modifiers
> 1
Uit te voeren oefening?
1. H10-Clock
2. H10-Birthday
> 2
(...)
Maak een applicatie die bestaat uit een oneindige loop. De loop zal iedere seconde pauzeren: System.Threading.Thread.Sleep(1000);
Vervolgens wordt het scherm leeg gemaakt en wordt de huidige tijd getoond. Merk op dat ENKEL de tijd wordt getoond, niet de datum.
Maak een applicatie die aan de gebruiker vraagt op welke dag hij/zij jarig is. Toon vervolgens over hoeveel dagen de verjaardag van de gebruiker zal zijn.
aanmaken van DateTime
objecten
formatteren van DateTime
objecten
We willen voor een willekeurige datum kunnen bepalen welke dag van de week het is.
je moet eerst de dag, maand en jaar opvragen en een DateTime
aanmaken
daarna moet je laten zien over welke dag van de week (in het Nederlands) het gaat
gebruik hiervoor formattering van een DateTime
laat ook de datum zelf zien in een formaat dat leesbaar is voor de gebruiker
noem de methode waarin je dit schrijft DayOfTheWeek .
Welke dag?
> 14
Welke maand?
> 2
Welk jaar?
> 2020
14 februari 2020 is een vrijdag.
aanmaken van DateTime
objecten
We willen weten hoe veel fracties van een seconde al verlopen zijn sinds het begin van de jaren 2000.
.NET stelt deze fracties (1 / 10000 milliseconden) voor als "ticks"
We willen weten hoe veel ticks er voorbijgegaan zijn sinds het absolute begin van het jaar 2000
Noem de methode waarin je dit schrijft TicksSince2000
Sinds 1 januari 2000 zijn er (hier wordt het aantal getoond) ticks voorbijgegaan.
gebruik van een statische methode
We willen bepalen hoe veel schrikkeljaren er zijn tussen 1799 en 2021.
implementeer zelf geen logica voor schrikkeljaren, maar laat dit over aan de klassen DateTime
maak gebruik van een statische methode van deze klasse
noem je methode LeapYearCount
Er zijn (hier wordt het aantal getoond) schrikkeljaren tussen 1799 en 2021.
eenvoudig code leren timen
gebruiken van DateTime
herhaling arrays
We zijn benieuwd hoe lang het duurt een array van 1 miljoen int
s te maken en op te vullen met de waarden 1,2,...
Bepaal het tijdstip voor en na aanmaken van de array.
Vul de array in met een for
-lus.
Noem de methode waarin je dit schrijft CodeTiming
Het duurt (hier wordt het aantal getoond) milliseconden om een array van een miljoen elementen aan te maken en op te vullen met opeenvolgende waarden.
werken met klassen en objecten
instantieattributen
instantiemethoden
Dit programma geeft op basis van de input van twee getallen de som van beide getallen, het verschil, het product en de deling. In het laatste geval en indien er een deling door nul zou worden uitgevoerd, wordt er "Fout!" weergegeven.
Voorzie voor volgende oefening eerst een nieuwe submenuklasse met als naam ClassesAndObjects
.
Maak ook een eigen klasse CombinationOf2Numbers
in een eigen file, CombinationOf2Numbers.cs
. Deze klasse bevat 2 getallen (type int
). Er zijn 4 methoden, die allemaal een double
teruggeven:
Sum
: geeft som van beide getallen weer
Difference
: geeft verschil van beide getallen weer
Product
: geeft product van beide getallen weer
Quotient
: geeft deling van beide getallen weer. Print "Fout"
naar de console indien je zou moeten delen door 0 en voer dan de deling uit. Wat er dan gebeurt, is niet belangrijk.
Gebruik public
attributen Number1
en Number2
. Plaats onderstaande code in een publieke statische methode van ClassesAndObjects
met naam DemoCombinationOf2Numbers
met return type void
:
CombinationOf2Numbers pair = new CombinationOf2Numbers();
pair.Number1= 12;
pair.Number2= 34;
Console.WriteLine("Paar:" + pair.Number1+ ", " + pair.Number2);
Console.WriteLine("Som = " + pair.Sum());
Console.WriteLine("Verschil = " + pair.Difference());
Console.WriteLine("Product = " + pair.Product());
Console.WriteLine("Quotient = " + pair.Quotient());
Zorg dat je DemoCombinationOf2Numbers
kan oproepen via het submenu van ClassesAndObjects.
Paar: 12, 34
Som = 46
Verschil = -22
Product = 408
Quotient = 0,352941176470588
We willen per cursus bijhouden welke studenten ingeschreven zijn en maken daarvoor een klasse Course
.
Deze klasse heeft twee attributen: Students
en Title
. Students
is een List van Student
-objecten. Title
is gewoonweg een string
. Course
heeft ook een methode ShowOverview
die de titel van de cursus toont, gevolgd door de namen van alle studenten die de cursus volgen.
Test je programma door een statische methode te maken in Program.cs DemoCourses.
Deze methode maakt eerst twee studenten aan (dezelfde als in DemoStudents
; je kan de code kopiëren). Maak 4 cursussen aan ("Communicatie", "Databanken", "Programmeren" en "Webtechnologie") via variabelen communicatie
, programmeren
, webtechnologie
en databanken
en geef ze de juiste titel. Maak de 2 studenten lid van de cursussen zoals hieronder getoond wordt. Toon tenslotte voor elke cursus het overzicht via ShowOverview
.
De methode DemoCourses
wordt ook opgeroepen via het keuzemenu in Main
. Plaats deze code in een oneindige lus.
We willen voor de student goed kunnen bijhouden voor welke cursus er welk resultaat behaald werd. Daarvoor maken we een nieuwe klasse CourseResult
. Daarna passen we ook de klasse Student
aan.
Deze klasse bevat twee attributen: Name
van het type string en Result
van het type byte.
Er zijn een aantal wijzigingen in de klasse Student.
Om deze klasse te gebruiken in klasse Student
, vervangen we de list courses
door een list courseResults
van CourseResult
-objecten en vervangen we de methode RegisterForCourse
door RegisterCourseResult
.
RegisterCourseResult
controleert eerst het resultaat. Indien dit groter is dan 20, geef dan de melding "Ongeldig cijfer!". Indien het een geldig resultaat is:
maak je een nieuw object van de klasse CourseResult
;
geef je het attribuut Name van
dit object de waarde van de parameter course;
geef je het attribuut Result
van dit object de waarde van de parameter result;
voeg je het nieuwe object toe aan de lijst courseResults
.
Doe de nodige aanpassing aan de methode DetermineWorkload
.
DemoStudents
en DemoCourses
moeten ook aangepast worden:
Verwijder de lijnen met gebruik van RegisterForCourse
.
We schrijven elke student in voor enkele cursussen met behulp van RegisterCourseResult
en we kennen ook resultaten toe:
Said: Communicatie: 12; Programmeren: 15; Webtechnologie: 13
Mieke: Communicatie: 13; Programmeren: 16; Databanken:14
De uitvoer van deze 2 methodes is niet gewijzigd.
Voor een student gaan we een gemiddelde berekenen van de behaalde resultaten en we tonen dit in een overzicht.
Maak de methode Average
. Deze overloopt de list courseResults,
maakt het totaal van alle resultaten en deelt dit door het aantal resultaten.
De methode ShowOverview
toont eerst de naam van de student met daaronder de werkbelasting en vervolgens een cijferrapport met de gevolgde cursussen en de behaalde resultaten. Onderaan staat het gemiddelde.
Pas de methode DemoStudents
aan (in Program.cs). Schrap hier de methoden GenerateNameCard
en DetermineWorkload
en voer voor elke student de methode ShowOverview
uit.
Resultaat van DemoStudents
:
werken met klassen en objecten
gebruik maken van properties om geldige waarden af te dwingen
Dit programma maakt enkele rechthoeken en driehoeken met gegeven afmetingen (in meter) aan, berekent hun oppervlakte en toont deze info aan de gebruiker. De rechthoeken en driehoeken die worden aangemaakt, zijn al gecodeerd in het programma. De gebruiker hoeft dus niets anders te doen dan het programma te starten.
Er is een klasse Rectangle
met full properties Width
en Height
en een klasse Triangle
met Base
en Height
. Je programma maakt de figuren die hierboven beschreven worden aan met beginwaarde 1.0
voor elke afmeting en stelt daarna hun afmetingen in via de setters voor deze properties. De oppervlakte wordt bepaald in een read-only property (dus met alleen een getter en geen setter). Deze heet Area
en is van het type double
.
Indien om het even welk van deze properties wordt ingesteld op 0
of minder, signaleer je dit via de code Console.WriteLine($"Het is verboden een (afmeting) van (waarde) in te stellen!")
(zie voorbeeldcode).
Schrijf de voorbeelden uit in een static
methode DemoFigures
van de klasse ClassesAndObjects
.
(Er worden twee rechthoeken en twee driehoeken aangemaakt. De afmetingen van de eerste rechthoek worden eerst op -1
en 0
ingesteld. Daarna krijgen ze de waarden die je ziet in het bericht hieronder. Formatteer ook met 1 cijfer na de komma.)
Het is verboden een breedte van -1 in te stellen!
Het is verboden een breedte van 0 in te stellen!
Een rechthoek met een breedte van 2,2m en een hoogte van 1,5m heeft een oppervlakte van 3,3m².
Een rechthoek met een breedte van 3m en een hoogte van 1m heeft een oppervlakte van 3m².
Een driehoek met een basis van 3m en een hoogte van 1m heeft een oppervlakte van 1,5m².
Een driehoek met een basis van 2m en een hoogte van 2m heeft een oppervlakte van 2m².
We willen elke cursus automatisch een id toewijzen en ook willen we studiepunten per cursus bijhouden. Daarnaast willen we een lijst bijhouden met alle objecten van de klasse Cursus.
Maak de property CreditPoints
met een private setter.
Voeg de read-only property Id
toe. Om telkens een volgend nummer toe te wijzen aan dit id, maak je het static attribuut maxId
dat je de beginwaarde 1 geeft.
Maak het attribuut AllCourses
(static); dit is een list van Course-objecten.
We herschrijven de bestaande attributen naar properties
Van Name maak je een property Name
.
Van Result maak je een property Result
. Zorg ervoor dat er enkel een resultaat kan ingesteld worden dat niet meer is dan 20.
Op basis van de geboortedatum berekenen we de exacte leeftijd van de student en nemen we die mee op in het overzicht.
Maak de read-only property Age
zonder achterliggend veld ('computed property'). Bereken hier eerst het verschil in jaren tussen het huidige jaar en het geboortejaar. Pas daarna dit aantal jaren eventueel aan (-1) als de student nog niet verjaard is (de maand van de verjaardag is na de huidige maand; of we zijn de maand van de verjaardag maar de verjaardag is na de huidige dag).
Voeg de leeftijd toe aan het overzicht in ShowOverview
.
Voorbeeld: