Veel code die we hebben geschreven wordt meerdere keren, al dan niet op verschillende plaatsen, gebruikt. Dit verhoogt natuurlijk de foutgevoeligheid. Door het gebruik van methodes kunnen we de foutgevoeligheid van de code verlagen omdat de code maar op 1 plek staat én maar 1 keer dient geschreven te worden. Ook de leesbaarheid en dus onderhoudbaarheid van de code wordt verhoogd wanneer we methoden gebruiken om verschillende deeltaken van elkaar te scheiden.
Een methode (bijna hetzelfde als een "functie" in andere programmeertalen) genoemd, is in C# een stuk code ('block') bestaande uit een 0, 1 of meerdere statements. Het is eigenlijk een klein stappenplan voor een onderdeel van je totale programma.
Zoals bij elk stappenplan is er een verschil tussen het vastleggen van de stappen en het uitvoeren. Het vastleggen van de stappen noemen we definiëren van de methode. Vergelijk met het tekenen van een flowchart voor een bepaalde taak, maar dan in code: de definitie van de methode stemt overeen met de flowchart die de deeltaak beschrijft.
Het uitvoeren van de stappen, dus het volgen van de flowchart, noemen we het oproepen of uitvoeren van de methode. De code die dit proces in gang zet noemen we een oproep (in het Engels: call) van de methode.
Je hebt intussen al vaak (ingebouwde) methodes opgeroepen, waaronder:
de Main
methode van de klasse Program
(voor deze hoef je de call zelf niet te schrijven, ze start automatisch op)
de WriteLine
methode van de klasse Console
de Substring
methode van een stuk tekst
In het begin schrijven we onze methodes zo (binnenkort zien we uitbreidingen):
Dit stukje code definieert dus hoe het stappenplan er uitziet. Het zorgt er niet voor dat het ook ooit wordt uitgevoerd. Daar is een call voor nodig. public
zorgt dat deze methode opgeroepen kan worden van uit andere klassen, static
is nodig omdat we klassen alleen als organisatieprincipe voor onze code gebruiken, void
behandelen we iets verder op. Deze zaken, samen met de naam en de haakjes, noemen we ook de signatuur van de methode.
In Flowgorithm stemt de definitie van een methode overeen met een flowchart die in haar geheel getoond kan worden. Onderstaand voorbeeld bevat drie definities (flowcharts): Main
, ToonGroen
en ToonBlauw
.
Je kan elke definitie (flowchart) terugvinden via dit menu:
Om de methode te gebruiken, moeten we een statement uitvoeren die er als volgt uitziet:
Aangezien elk programma begint met een oproep van de Main
methode, zal dit programma dus wel de tekst Groen
laten zien en niet de tekst Blauw
:
In Flowgorithm herken je de oproep als volgt:
Een oproep van een methode betekent dus dat je een andere flowchart uitvoert alsof het één stap is van de flowchart waarin je bezig bent.
Net zoals eerder kan je dit ook in C# stap voor stap uitvoeren door de debugger te gebruiken. Om de werking van een methode in detail te zien, gebruik je "step into".
Een methode is een stappenplan voor een bepaalde taak. Met de syntax die we tot hiertoe gezien hebben, wordt deze taak altijd op exact dezelfde manier uitgevoerd. Dat beperkt de mogelijkheden enorm.
Een niet-digitaal voorbeeld. Ik plan een feest. Een van mijn deeltaken is om een simpele appeltaart te maken. De tweede is om een simpele perentaart te maken.
Het stappenplan voor de appeltaart is als volgt:
leg taartdeeg in een ronde bakvorm
doe er wat pudding op
snijd een appel in stukken
beleg met de stukjes fruit
zet in de oven
Het stappenplan voor de perentaart is als volgt:
leg taartdeeg in een ronde bakvorm
doe er wat pudding op
snijd een peer in stukken
beleg met de stukjes fruit
zet in de oven
Dit is hetzelfde stappenplan, op het gebruikte fruit na. Eigenlijk kan je in stap 3 allerlei soorten fruit gebruiken, afhankelijk van het resultaat dat je wenst. We kunnen het stappenplan dus algemener formuleren:
leg taartdeeg in een ronde bakvorm
doe er wat pudding op
snij het stuk fruit dat je wil gebruiken in stukken
beleg met de stukjes fruit
zet in de oven
Op het moment dat we een taart willen bakken, moeten we pas voor een bepaald stuk fruit kiezen. In de tekst hierboven is "het stuk fruit" dus een soort variabele: we schrijven een naam maar we bedoelen daarmee een waarde die in de uitvoering wordt vastgelegd.
In code zijn dit soort variabelen parameters van een methode. In de definitie van de methode werken we met hun naam (zoals "het stuk fruit"). Wanneer we de methode oproepen, voorzien we hun waarde (zoals een appel of een peer of een ananas). In de call spreken we ook over argumenten in plaats van parameters. De termen worden soms door elkaar gehaald, maar ze betekenen dus niet exact hetzelfde.
In Flowgorithm kunnen kort noteren dat we een appeltaart en een perentaart willen maken:
In de praktijk maken we natuurlijk geen taarten, maar doen we bewerkingen met gegevens. Volgende methode berekent en toont een bepaalde macht van een bepaald getal. De werkwijze is altijd dezelfde, maar de gebruiker beslist zelf welke macht hij wil berekenen en van welk getal:
Dit is een definitie van een methode, dus vergelijkbaar met een stappenplan of een flowchart. We geven aan dat er twee stukjes informatie zijn die per uitvoering kunnen verschillen: het grondtal en de macht. Omdat dit eigenlijk variabelen zijn (met een scope beperkt tot deze methode) geven we ook hun type, net zoals bij variabelen die we op de reeds gekende wijze declareren.
Als we deze methode willen gebruiken, kunnen we dit doen:
De laatste regel bevat de call van de methode. De definitie bevat een paar "gaten" en de call vult deze in met de gewenste getallen. Het is belangrijk dat de gebruikte types en de verwachte types compatibel zijn. Het is bijvoorbeeld niet mogelijk Macht("Hello World","blabla");
te schrijven als call want strings zijn geen ints.
Als je het stappenplan bovenaan deze pagina gebruikt om een appeltaart te maken, heb je achteraf geen appel meer. Hier kan het mechanisme van methodes je wat verrassen. Je moet in het achterhoofd houden dat de parameters van een methode hetzelfde werken als gewone variabelen.
Een voorbeeld:
Dit programma zal je eerst 3
tonen en dan, op de volgende regel, 4
. Dit komt omdat de =
op regel 2 niet betekent "a
is hetzelfde als b
", maar wel "kopieer het ding met naam a
en geef de kopie de naam b
". Dit is hoe toekenning werkt met alle types die we tot hiertoe gezien hebben.
Dit is niet hoe toekenning altijd werkt. Met name voor arrays is het wat anders, maar dat geval zullen we later in meer detail behandelen.
Met argumenten van een methode-oproep is het hetzelfde. Nog een voorbeeld:
Je ziet dat het getal enkel gewijzigd is binnen de methode VeranderGetal en daarna weer teruggezet lijkt op de oude waarde. Dit is niet helemaal correct. Wat eigenlijk gebeurt, is het volgende:
een waarde 4 krijgt de naam mijnGetal
de waarde met naam mijnGetal
wordt getoond
de waarde met naam mijnGetal
wordt gekopieerd en krijgt de naam getal
de naam mijnGetal
wordt toegekend aan het getal dat je verkrijgt door de kopie met één te verminderen en verwijst nu dus naar een 3
deze 3 wordt getoond
de oproep eindigt
heel de tijd is mijnGetal
blijven verwijzen naar 4 en daarom wordt er terug een 4 getoond
mijnGetal
en getal
hebben dus nooit dezelfde data bevat. Bij het begin van een oproep van VeranderGetal
zijn hun data wel kopieën van elkaar, maar na regel 2 is zelfs dat niet meer waar.
Je hebt al vaak waarden als argument meegegeven:
de tekst die je laat zien via WriteLine
de grenzen waarbinnen een willekeurig getal bepaald moet worden met Next
de tekst die je wil vervangen en de tekst die hem vervangt met Replace
Ook hier wordt altijd hetzelfde stappenplan gevolgd, maar de manier waarop dat plan doorlopen wordt, verschilt afhankelijk van de gebruikte argumenten.
Voor de eenvoud hebben we in bovenstaand voorbeeld twee namen gebruikt: mijnGetal
en getal
. Eigenlijk hoefde dat niet. We kunnen ook dit doen:
De parameter getal
van VeranderGetal
is niet dezelfde variabele als de variabele getal
van Main
. Elke functiedefinitie bakent een scope af. Vermits de definitie van VeranderGetal
niet genest is in de definitie van Main
(en het omgekeerde ook niet waar is) staan hun scopes los van elkaar. Binnenin Main
bestaat er een variabele met naam getal, binnenin VeranderGetal
bestaat er een andere variabele met naam getal.
Er staat wel een oproep van VeranderGetal
in Main
, maar dit heeft geen effect op de variabelen in scope. Enkel de definities zijn hier van belang.
Volgende sectie is grotendeels gebaseerd op het volgende artikel.
Wanneer je een methode aanroept is de volgorde van je argumenten belangrijk: deze moeten meegeven worden in de volgorde zoals de methode parameters ze verwachten.
Met behulp van named parameters kan je echter expliciet aangeven welke argument aan welke methode-parameter moet meegegeven worden.
Stel dat we een methode hebben met volgende signatuur:
Zonder named parameters zou een aanroep van deze methode als volgt kunnen zijn:
We kunnen named parameters aangeven door de naam van de parameter gevolg door een dubbel punt en de waarde. Als we dus bovenstaande methode willen aanroepen kan dat ook als volgt met named parameters:
of ook:
Je mag ook een combinatie doen van named en gewone parameters, maar dan is de volgorde belangrijk: je moet je dan houden aan de volgorde van de methode-volgorde. Je verbetert hiermee de leesbaarheid van je code dus (maar krijgt niet het voordeel van een eigen volgorde te hanteren). Enkele voorbeelden:
Enkele NIET GELDIGE voorbeelden:
Soms wil je dat een methode een standaard waarde voor een parameter gebruikt indien de programmeur in z'n aanroep geen waarde meegaf. Dat kan met behulp van optionele of default parameters.
Je geef aan dat een parameter optioneel is door deze een default waarde te geven in de methode-signatuur. Deze waarde zal dan gebruikt worden indien de parameter geen waarde van de aanroeper heeft gekregen.
Optionele parameters worden steeds achteraan de parameterlijst van de methode geplaatst .
In het volgende voorbeeld maken we een nieuwe methode aan en geven aan dat de laatste parameters optioneel zijn. discountPercentage
past een "gewone" korting toe op de aankoop van het aantal items. doubleDiscount
is een speciale extra korting die het percentage verdubbelt. We veronderstellen dat items meestal verkocht worden zonder korting:
Volgende manieren zijn nu geldige manieren om de methode aan te roepen:
Je mag enkel de optionele parameters van achter naar voor weglaten. Volgende aanroep is dus niet geldig:
Door de argumenten te benoemen, kunnen we dit indien gewenst omzeilen. Volgende aanroep is wel geldig:
Method overloading wil zeggen dat je een methode met dezelfde naam en returntype meerdere keren definieert maar met andere parameters qua type en aantal. De compiler zal dan zelf bepalen welke methode moet aangeroepen worden gebaseerd op het aantal en type parameters dat je meegeeft.
Volgende methoden zijn overloaded:
Afhankelijk van de aanroep zal dus de ene of andere uitgevoerd worden. Volgende code zal dus werken:
Method overloading is de reden waarom je in Visual Studio Code de documentatie van meerdere versies van een methode kan bekijken. Dit doe je door op de pijltjes naast de info over die methode te klikken.
Al deze oefeningen schrijf je in een klassen Methodes
.
Methodes definiëren met parameters
Oproepen van de methodes
Je stelt een aantal typische berekeningen voor met methodes.
Voor deze oefening schrijf je meerdere methodes. Je plaatst er slechts één in je keuzemenu, namelijk ReeksOperaties
. Deze voert alle andere methodes van deze oefening één voor één uit.
Enkel ReeksOperaties
mag Console.ReadLine
statements bevatten. De andere methodes tonen hun resultaat op het scherm door middel van Console.WriteLine
statements in de methode zelf die het resultaat bepaalt. (Met andere woorden, alle methodes zijn void
.)
De andere methodes zijn:
BerekenStraal
, die de straal van een cirkel kan berekenen waarvan je de diameter meegeeft (de diameter geef je mee als argument). De straal is de helft van de diameter.
Idem voor BerekenOmtrek
en BerekenOppervlakte
. De omtrek is de diameter maal het getal pi, dat je voorstelt als Math.Pi
. De oppervlakte is straal maal straal maal pi.
Methode Maximum
die het grootste van 2 getallen teruggeeft (beide getallen geef je mee als argument).
Methode IsEven
die toont of een getal even of oneven is.
Methode ToonOnEvenGetallen
die alle oneven getallen van 1 tot n toont waarbij n als argument wordt meegegeven.
Methodes definiëren met return type en parameters
Oproepen van de methodes
Schrijf een programma dat op basis van je voornaam, naam en het feit of je al dan niet een student bent een AP e-mailadres genereert. Het e-mailadres moet uit allemaal kleine letters bestaan en moet een geldig e-mailadres zijn.
Je mag geen String methodes gebruiken maar je schrijft je eigen methodes. Als aanzet is de methode StringToLower al uitgewerkt.
Uitbreiding: kijk of voor al je lectoren en medestudenten het gegenereerde e-mailadres geldig is. Pas eventueel je code aan indien niet.
Werk volgend flowchart verder af en zet om in C#.
Leerdoelen
Methodes definiëren met parameters en returnwaarde
Oproepen van de methodes
Gebruiken van returnwaarden
Je stelt een aantal typische berekeningen voor met methodes die geschikt zijn voor gebruik in andere programma's.
Voor deze oefening schrijf je meerdere methodes. Je plaatst er slechts één in je keuzemenu, namelijk ReeksOperatiesMetReturn
. Deze voert alle andere methodes van deze oefening één voor één uit.
Enkel ReeksOperaties
mag Console.ReadLine
of Console.WriteLine
statements bevatten. De andere methodes berekenen hun resultaat, zonder het te tonen op het scherm. Met andere woorden, hun return type is niet void
.
De andere methodes zijn:
BerekenStraalMetReturn
, die de straal van een cirkel kan berekenen waarvan je de diameter meegeeft (de diameter geef je mee als argument). De straal is de helft van de diameter. Voor je deze toont, rond je ze in ReeksOperatiesMetReturn
af tot één cijfer na de komma met Math.Round(reedsBerekendeStraal,1)
.
Idem voor BerekenOmtrekMetReturn
en BerekenOppervlakteMetReturn
. Ook deze waarden moet je, nadat ze uitgerekend zijn, afronden tot 1 cijfer na de komma.
Methode MaximumMetReturn
die het grootste van 2 getallen teruggeeft (beide getallen geef je mee als argument).
Methode IsEvenMetReturn
die bepaalt of een getal even of oneven is. De returnwaarde vertelt dus of iets waar of niet waar is.
Methode BepaalEvenGetallenMetReturn.
Deze geeft als antwoord een List
van alle even getallen tot n. ReeksOperatiesMetReturn
toont deze getallen dan, gescheiden door een komma.
Zoals boven, met de vermelde aanpassingen (afronding tot 1 cijfer na de komma en getallen gescheiden door ,
)
Functionele analyse
We schrijven een simpel tekenprogramma, dat driehoeken van een bepaalde afmeting, met een bepaald patroon kan tekenen.
Technische analyse
Schrijf een methode TekenDriehoek
. Deze methode verwacht twee zaken: een karakter om het patroon voor te stellen en de hoogte van de driehoek. Ze roept zelf meermaals een andere methode TekenRegel
op, die één regel van de driehoek tekent. TekenRegel toont niets op het scherm via Console.WriteLine
!
Voorbeeldinteractie
Een stappenplan, en dus een methode, voer je uit met het oog op een bepaald resultaat. Soms moet dat resultaat terugbezorgd worden aan een opdrachtgever. Soms niet. Methodes staan toe beide variaties op een stappenplan te schrijven.
We gebruiken opnieuw een proces uit het echte leven om de interactie te omschrijven voor we de sprong maken naar code.
Denk aan een ouderwetse bakker. Het stappenplan dat deze bakker volgt is er een om brood te maken. Om brood te maken heb je bloem nodig. Bloem maken is niet echt een onderdeel van het takenpakket van de bakker. Hij laat de molenaar dit voor hem doen. Hij vraagt niet gewoon aan de molenaar om bloem te maken, maar ook om deze aan hem te bezorgen zodat hij er zelf mee verder kan werken. De bakker is niet geïnteresseerd in hoe de molenaar bloem maakt, maar hij heeft het eindproduct nodig om zijn brood af te maken.
We vertalen nu deze interactie naar code en verklaren daarna de vertaling:
Dit is maar een voorbeeld om de flow te verduidelijken. Er is geen "juiste" manier om een methode te schrijven om brood te bakken (tenzij je misschien een broodmachine programmeert).
Hier vallen twee zaken op: in de signatuur (d.w.z. alles van het begin van de regel tot en met het gesloten ronde haakje) van de methode Molenaar staat niet void
, maar string
. Dit komt omdat deze methode een resultaat teruggeeft aan de opdrachtgever en dat resultaat van het type string
is. Daarnaast heb je het sleutelwoord return
, gevolgd door de tekst "bloem"
. Het woordje return
betekent: "bezorg dit aan de code die deze methode heeft opgeroepen". Anders gezegd: dit is hoe de molenaar de geproduceerde bloem overhandigt aan zijn opdrachtgever, de bakker. Waar je de call Molenaar()
schrijft komt tijdens de uitvoering het geproduceerde resultaat dankzij die return
.
return
typeJe moet dus noteren wat voor resultaat er achter return
staat: een string of eventueel iets anders. Als er een string volgt na return
, zeggen we dat string het return type is van die methode. Als een methode een geheel getal produceert, heeft ze het return type int
of een verwant getaltype.
Maar niet alle methodes bezorgen iets terug aan hun opdrachtgever. Sommige hebben gewoon een effect. Voor deze methodes schrijven we void
als return type. Als een methode dit return type heeft, kunnen we het resultaat van een call dus niet toekennen aan een variabele, want er is geen resultaat.
"Er is geen resultaat" wil niet zeggen dat een void
methode niets doet. Console.WriteLine is bijvoorbeeld een methode met return type void
. Het punt is dat de call je niets terugbezorgt waarmee je verder kan werken. Je kan bijvoorbeeld niet schrijven: string tekst = Console.WriteLine("Dit is tekst");
Er is dus een groot verschil tussen return "tekst";
en Console.WriteLine("tekst");
Bij de eerste code is er geen garantie dat de geproduceerde tekst ooit op het scherm verschijnt, maar je kan er wel mee verder werken. Bij de tweede verschijnt hij per definitie wel op het scherm, maar kan je hem niet toekennen aan een variabele om later mee verder te werken.
We kunnen dus wel doen: string ingredient3 = Molenaar();
(want het return type van Molenaar()
is string
) maar we kunnen niet schrijven: string product = Bakker();
(want Bakker()
heeft return type void
en produceert dus geen resultaat). Deze code compileert dan ook niet.
Je kent de methode Replace
van strings. Je gebruikt deze als volgt:
Veel beginners doen het volgende:
Dit is fout. Replace
berekent het resultaat van de aanpassing en geeft dat terug aan de opdrachtgever. Het past de ingevoerde tekst niet aan. In de code van Replace
staat dus ook een return
!
Als je parameters en returnwaarden combineert, worden methoden erg flexibel. Dan kan je al echte bouwsteentjes van een programma gaan schrijven.
Een voorbeeld: de stelling van Pythagoras vertelt ons hoe lang de schuine zijde van een driehoek met twee rechte hoeken is:
Als we deze berekening regelmatig uitvoeren voor verschillende waarden van a
en b
, kunnen we ze als volgt in een methode gieten:
Nu kunnen we heel snel allerlei schuine zijdes uitrekenen, bv.:
Bovendien maken we gebruik van het feit dat de methode Sqrt
(square root is vierkantswortel) ook een parameter heeft en een resultaat teruggeeft. Eigenlijk verloopt een uitvoering dus zo:
de methode Main
start op
we willen bepaalde tekst uitprinten, maar we moeten die eerst nog bepalen met de methode Pythagoras
de methode Pythagoras
start op met de argumenten 4 en 3
de methode berekent de som van de kwadraten en noemt deze som
de methode maakt zelf gebruik van nog een methode, Sqrt
, en geeft daarbij som als argument
Sqrt
rekent de vierkantswortel uit en geeft deze terug aan Pythagoras
Pythagoras
geeft deze verder door aan Main
de zin wordt correct afgeprint
Meestal is het dus een goed idee zo weinig mogelijk informatie te communiceren met WriteLine
en zo veel mogelijk met return
te werken. Meerbepaald: gebruik WriteLine wanneer je de uiteindelijke informatie in haar uiteindelijke vorm hebt en deze echt aan de gebruiker moet tonen. Gebruik return
wanneer je het resultaat van een deeltaak aan een opdrachtgever wil bezorgen.
Volgend Flowgorithm toont aan hoe je dankzij methoden een probleem kan opbreken in meerdere kleine stappen, waarbij je informatie doorgeeft via parameters en returnwaarden: