Methoden

Zie Scherp Scherper - Hoofdstuk 7

Tim Dams

Wat leer je in dit hoofdstuk?

Na dit hoofdstuk kan je:

  • uitleggen waarom methoden code herbruikbaar en leesbaar maken
  • zelf een methode schrijven met een returntype en return
  • parameters doorgeven en het verschil met by value begrijpen
  • methoden nesten en oneindige methode-lussen herkennen
  • inschatten hoe groot een methode mag zijn
  • named en optionele parameters en overloading gebruiken
  • met ///-commentaar en IntelliSense efficiënter werken

Luiheid als deugd

“I will always choose a lazy person to do a difficult job, because he will find an easy way to do it.” - Bill Gates

We jagen op zo weinig mogelijk code met zoveel mogelijk rendement. Loops waren een eerste stap, methoden de volgende.

Tip

Bestond Console.WriteLine niet, dan moest je telkens tientallen lijnen interne code overtypen. Wees blij dat methoden bestaan.

Wat is een methode?

  • Een blok code dat een specifieke taak uitvoert
  • Herkenbaar aan de ronde haakjes achteraan, al dan niet met parameters
  • Je gebruikt ze al sinds les 1: elke Console.WriteLine() is een methode-aanroep

Nieuw: we gaan nu zelf methoden definiëren, niet enkel bestaande gebruiken.

Methode-syntax

static returntype MethodeNaam(parameters)
{
    //code van de methode
}

De eerste lijn is de methode-signatuur. Die vertelt alles wat je moet weten om met de methode te werken: returntype, naam en eventuele parameters.

De onderdelen van een methode-signatuur.

Tip

Vergeet static voorlopig: beschouw het als een verplicht toverwoord. Waarom het er staat, zie je in hoofdstuk 11.

Een eenvoudige methode

static void ToonTitel()
{
    Console.WriteLine("Timsoft XP");
}

Aanroepen, eender waar:

ToonTitel();

Verandert de titel ooit? Eén plek aanpassen volstaat.

De flow van de aanroep.

Main is ook een methode

Een console-applicatie heeft een startpunt nodig, en dat is Main. Verder is het een methode zoals alle andere. Wat doet dit?

static void Main(string[] args)
{
    Console.WriteLine("Ik zit vast!");
    Main(args);
}

Opmerking

Main roept zichzelf op, en die aanroep doet exact hetzelfde. Eindeloos “Ik zit vast!” tot het programma crasht. Waarom precies zie je straks bij de oneindige methode-lussen.

Tip

Via string[] args geef je opstartparameters mee aan je programma (denk aan explorer.exe google.com). Meer daarover in hoofdstuk 8.

Returntype en return

void betekent dat een methode niets teruggeeft. Vaak wil je wel iets terug:

static string VerkrijgAuteurNaam()
{
    return "Tim Dams";
}
  • return geeft het resultaat terug en verlaat de methode meteen
  • Vang het resultaat op in een variabele van hetzelfde type:
string myName = VerkrijgAuteurNaam();
Console.WriteLine($"Auteur: {VerkrijgAuteurNaam()}");

Een uitgewerkte methode

static int FaculteitVan5()
{
    int resultaat = 1;
    for (int i = 1; i <= 5; i++)
    {
        resultaat *= i;
    }
    return resultaat;
}
Console.WriteLine($"Faculteit van 5 is {FaculteitVan5()}");  // 120

void: niets opvangen

static void ToonVersie()
{
    Console.WriteLine("Dit is versie 8.31");
}

Een void-methode geeft niets terug. Deze twee mogen dus niet:

string result = ToonVersie();      // FOUT
Console.WriteLine(ToonVersie());   // FOUT

Not all code paths return a value

return mag overal staan en sluit de methode meteen af (in een switch heb je dan geen break meer nodig):

switch (r.Next(0, 4))
{
    case 0: return "noord";
    // ...
}
return "onbekend"; // nooit bereikt, tóch vereist

Bij punten kleiner dan 10 loopt de methode ten einde zonder return.

Waarschuwing

Deze foutboodschap krijg je vaak: er is een weg door je methode waar het einde bereikt wordt zonder return. Dat mag enkel bij void.

Parameters doorgeven

Veralgemeen FaculteitVan5 met een formele parameter:

static int BerekenFaculteit(int grens)
{
    int resultaat = 1;
    for (int i = 1; i <= grens; i++)
        resultaat *= i;
    return resultaat;
}
int resultaat = BerekenFaculteit(5);  // 120

Tip

De parameternaam (grens) hoeft niet gelijk te zijn aan de meegegeven variabele. Enkel de waarde reist mee.

Wat zit er in grens?

for (int i = 1; i < 11; i++)
{
    Console.WriteLine($"Faculteit van {i} is {BerekenFaculteit(i)}");
}

Vul zelf in:

aanroep grens resultaat
BerekenFaculteit(4) ? ?
BerekenFaculteit(getal), met getal gelijk aan 3 ? ?
BerekenFaculteit(i) in de lus hierboven, als i 5 is ? ?

Opmerking

4 en 24, 3 en 6, 5 en 120. De naam links speelt geen rol.

Volgorde van parameters

De eerste waarde gaat naar de eerste parameter, de tweede naar de tweede.

static void ToonDeling(double teller, double noemer)
{
    Console.WriteLine(teller / noemer);
}
ToonDeling(3.5, 2.1);   // 1,6666666666666665
ToonDeling(2.1, 3.5);   // 0,6

Bij verschillende types helpt VS je wel: ToonInfo(37, "Tim") compileert gewoon niet.

Draai je de aanroep om, dan draait de deling mee om.

Opmerking

Zie je bij Console.WriteLine de versie met params object[] arg staan? params betekent: geef zoveel parameters mee als je wil. Zelf zoiets schrijven kan pas met arrays, in hoofdstuk 8.

By value

Parameters gaan standaard by value: de methode krijgt een kopie.

static void JaartjeOuder(int leeftijd)
{
    leeftijd++;
    Console.WriteLine($"Je bent {leeftijd} jaar geworden.");
}
Je bent 40 jaar.
Je bent 41 jaar geworden.
Je bent 40 jaar.   <- origineel ongewijzigd

De methode werkt op een kopie, dus mijnLeeftijd blijft 40.

Opmerking

By reference geeft het adres van de originele variabele mee, en komt aan bod vanaf het hoofdstuk over arrays. Je zag het al bij int.TryParse(Console.ReadLine(), out int leeftijd): die methode zet zelf een waarde in leeftijd.

Stagiair Steven

Steven laat de A.I. een methode schrijven die een getal verdubbelt:

static int Verdubbel(int getal)
{
    return getal * 2;
}

int punten = 5;
Verdubbel(punten);
Console.WriteLine($"Punten: {punten}");

“Ik roep Verdubbel op, dus punten is nu 10”, zegt hij zelfzeker. Wat verschijnt er op het scherm?

Opmerking

Er verschijnt Punten: 5. De methode werkt op een kopie, maar de echte fout is dat Steven het resultaat nergens opvangt. Verdubbel(punten); berekent wel 10, maar dat getal gaat verloren. Het moest punten = Verdubbel(punten); zijn. De code compileert zonder klacht, dus kreeg hij geen enkele waarschuwing.

Methoden nesten

Een methode mag andere methoden aanroepen:

static void SchrijfNaam()
{
    SchrijfT();
    SchrijfI();
    SchrijfM();
}

Resultaat: “Tim”.

Aanroepen van methoden uit methoden.

Tip

De volgorde waarin je je methoden in je bestand schrijft, maakt niets uit. Bij variabelen is dat wél anders: die declareer je voor je ze gebruikt.

Hoe groot mag een methode zijn?

Er bestaat geen regel over het aantal lijnen, wel een goede vuistregel: een methode doet één ding.

  • Kan je in één zin uitleggen wat ze doet? Goed bezig.
  • Krijg je de naam niet bedacht zonder er “En” in te stoppen (LeesInEnBerekenEnToon), dan zitten er eigenlijk drie methoden in.

Tip

Lukt die ene zin niet, dan splits je best op. In de kennisclip “Goede methoden schrijven” ga ik hier dieper op in.

Oneindige methode-lussen

static void SchrijfNaam()
{
    SchrijfNaam();
    Console.WriteLine("Klaar?");
}

Je krijgt een leeg scherm (“Klaar?” wordt nooit bereikt) en daarna een crash met een StackOverflowException.

Elke aanroep legt er een frame bij op de stack, en er komt er nooit een af.

Opmerking

Iedere aanroep neemt een stukje geheugen in op de zogenaamde stack, en die raakt vol. Wat die stack precies is, lees je in hoofdstuk 10.

Recursie (kort)

Recursie = een methode die zichzelf aanroept, maar wel stopt:

static int BerekenSomRecursief(int start, int stop)
{
    int som = start;
    if (start < stop)
    {
        start++;
        return som + BerekenSomRecursief(start, stop);
    }
    return som;
}

BerekenSomRecursief(1, 3) geeft 6.

Flow van de recursie.

Een geavanceerd concept; we komen het pas in hoofdstuk 18 kort terug tegen.

Lokale functies: niet doen

Sinds C# 7 mag een methode in een andere methode staan, maar als beginner doe je dat niet:

static void Main(string[] args)
{
    TimVindtDitNietLeuk();

    static void TimVindtDitNietLeuk() // NIET DOEN
    {
        Console.WriteLine("Doe dit niet!");
    }
}

Zo’n lokale functie kan je nergens anders aanroepen. Zet je methoden gewoon naast Main.

Belangrijk

Niet enkel een kwestie van smaak: op een vaardigheidsproef kost een methode in een methode je 3 punten.

Documentatie met ///

/// <summary>
/// Berekent de macht van een getal.
/// </summary>
/// <param name="grondtal">Het getal dat je verheft</param>
/// <param name="exponent">De exponent</param>
static int Macht(int grondtal, int exponent)
{
    //...
}

Typ /// boven een methode en VS maakt het skelet. Jij vult enkel de uitleg per parameter in.

Schrijf je documentatie best in het Engels, niet zoals in dit voorbeeld dus.

Wat kan die bibliotheek eigenlijk?

Typ Console. en wacht even: IntelliSense toont alles wat die bibliotheek kan.

  • De icoontjes: kubus = methode, Engelse sleutel = eigenschap, bliksem = event
  • Lijst per ongeluk weggeklikt? Ctrl+Spatie roept ze terug
  • Verschijnt er niets, dan heb je waarschijnlijk een schrijffout gemaakt

De parameters van een methode zien

  • Stop met typen na het eerste ronde haakje: IntelliSense toont alle manieren om de methode aan te roepen (de overloads), doorbladeren doe je met de pijltjes
  • Popup weg? Ctrl+Shift+Spatie
  • Cursor op de methode en F1 opent de online documentatie

Dit soort popups bevat een schat aan informatie.

Tip

Dit werkt ook voor je eigen methoden. Schreef je er ///-commentaar boven, dan staat jouw uitleg mee in die popup.

Herbruikbare input

Komt deze constructie vaak voor?

Console.WriteLine("Geef leeftijd");
int leeftijd = int.Parse(Console.ReadLine());

Verhuis ze naar een methode:

static int VraagInt(string zin)
{
    Console.WriteLine(zin);
    return int.Parse(Console.ReadLine());
}

int leeftijd = VraagInt("Geef leeftijd");

Je Main blijft net en leesbaar.

Named parameters

Twee parameters van hetzelfde type verwisselen? Dat compileert gewoon:

static void ToonPunten(string vak, int score, string student) { }

ToonPunten("Steven", 14, "Programmeren");  // stille bug

Geef daarom expliciet aan welke waarde bij welke parameter hoort:

ToonPunten(student: "Steven", score: 14, vak: "Programmeren");

Geef je alle parameters een naam, dan maakt de volgorde niets meer uit.

Named en gewoon mengen

Mengen mag, maar dan moet elke named parameter op zijn eigen plaats blijven staan:

ToonPunten("Programmeren", 14, student: "Steven");       // ok
ToonPunten(vak: "Programmeren", 14, student: "Steven");  // ok
ToonPunten(student: "Steven", "Programmeren", 14);       // fout
ToonPunten("Programmeren", student: "Steven", 14);       // fout

Waarschuwing

Named argument ‘student’ is used out-of-position but is followed by an unnamed argument. In beide foute lijnen staat student niet op de derde plaats, terwijl er daarna nog naamloze parameters volgen.

Optionele parameters

Geef een default waarde mee; die parameters staan achteraan:

static void ToonFactuur(string klant, int korting = 0, int btw = 21)
ToonFactuur("Tim", 10, 6);
ToonFactuur("Tim", 10);   // btw = 21
ToonFactuur("Tim");       // korting = 0, btw = 21

Weglaten mag enkel van achter naar voor.

Zonder naam landt de 6 in korting. Met btw: 6 blijft korting op zijn default.

Waarschuwing

ToonFactuur("Tim", 6) compileert zonder klacht, maar die 6 belandt in korting. Fouten met optionele parameters zijn zelden compilerfouten: je merkt ze pas aan de output. Schrijf ToonFactuur("Tim", btw: 6).

Method overloading

Dezelfde naam, maar een andere parameterlijst. De compiler kiest de juiste:

static int Verdubbel(int getal)
{
    return getal * 2;
}
static double Verdubbel(double getal)
{
    return getal * 2;
}
static string Verdubbel(string tekst)
{
    return tekst + tekst;
}
Verdubbel(21);    // 42
Verdubbel(2.5);   // 5
Verdubbel("ha");  // haha

Belangrijk

Het returntype mág verschillen, maar telt niet mee. Twee versies die enkel in returntype verschillen geven already defines a member called ‘Verdubbel’ with the same parameter types.

Overloaden op het aantal parameters

Naast ToonTitel(string naam) zetten we een versie zonder parameters, die gewoon de standaardtitel gebruikt:

static void ToonTitel(string naam)
{
    Console.WriteLine($"*** {naam} ***");
}

static void ToonTitel()
{
    ToonTitel("Timsoft XP");
}

Precies wat er bij Console.WriteLine() gebeurt: er bestaat een versie zonder parameters die een lege lijn toont.

Tip

Overload enkel wanneer je versies hetzelfde doen met andere input. Een BerekenOpp(lengte, breedte) naast een BerekenOpp(straal) compileert netjes, maar wie je code leest moet raden welke van de twee berekeningen bedoeld wordt.

Wanneer de compiler niet kan kiezen

static void ToonPrijs(int bedrag, double korting) { }   // versie A
static void ToonPrijs(double bedrag, int korting) { }   // versie B
Aanroep Resultaat
ToonPrijs(100, 5.5) versie A wordt uitgevoerd
ToonPrijs(99.9, 5) versie B wordt uitgevoerd
ToonPrijs(100, 5) fout: the call is ambiguous
ToonPrijs(99.9, 5.5) fout: geen enkele versie past

Belangrijk

Bij ToonPrijs(100, 5) passen A en B even goed, en dan kiest de compiler niet. Jij maakt het duidelijk: ToonPrijs(100, 5.0) of ToonPrijs(100.0, 5). Hoe hij bij twijfel tussen types wél kiest (de betterness rule) is verdiepingsstof.

Methoden debuggen: step in

Herinner je de debugknoppen uit hoofdstuk 4? Eén lieten we toen liggen: step in.

  • Step over voert de methode wel uit, maar springt naar de volgende lijn van Main
  • Step in brengt je in je eigen methode, waar je lijn per lijn verder stapt

Simpel, maar oefen het toch een paar keer.

Step over versus step in.

Methoden in actie: verdeel en heers

Een groot probleem splits je op in kleine methoden, elk met een duidelijke taak: een IN (parameters), een ACTIE en een UIT (returntype).

Teken eerst een flowchart. Elk blokje wordt een methode. Daarna rijg je ze aan elkaar in een hoofdmethode.

Tip

Zo blijft je Main kort en lees je je programma als een inhoudstafel.

Showcase: Avenger Mission Planner

Nick Fury managet de Avengers. Elke fase van een missie is een methode:

Methode IN UIT
Alarm niets moeilijkheidsgraad (int, 0 = geen alarm)
MissieVoorbereiden graad naam held (string)
MissieUitvoeren graad + held missiepercentage (int)
Evaluatie percentage + held niets (void)

Avenger Mission Planner: de manager

Een MissieManager rijgt de losse methoden aan elkaar:

static void MissieManager(int moeilijkheidsgraad)
{
    string held = MissieVoorbereiden(moeilijkheidsgraad);
    int percentage = MissieUitvoeren(moeilijkheidsgraad, held);
    Evaluatie(percentage, held);
}

Tip

Uitbreiding: bij een alarm waarschuw je ook DC via DCWaarschuwer(moeilijkheidsgraad). Eén extra methode-aanroep, klaar.

Showcase: verhaal-generator

Bouw van onder naar boven, elke laag hergebruikt de vorige:

  • GenereerKlinker() / GenereerMedeklinker(): losse letters
  • GenereerNaam(int lengte): een uitspreekbare naam (klinker na medeklinker)
  • GenereerKorteZin(): onderwerp + werkwoord + voorwerp
  • GenereerVerhaal(): meerdere zinnen aan elkaar

Tip

GenereerMedeklinker hergebruikt IsKlinker: blijf letters trekken tot er een geen klinker is. Methoden die methoden gebruiken.

Verhaal-generator: zinnen samenstellen

static string GenereerKorteZin()
{
    string onderwerp = GenereerNaam(6);
    string werkwoord = GenereerWerkwoord();   // "gooit", "trapt", ...
    string voorwerp  = GenereerVoorwerp();    // "de bal", "de lepel", ...
    return $"{onderwerp} {werkwoord} {voorwerp}";
}

Tip

Uitdaging: schrijf zelf GenereerVerhaal() dat meerdere zinnen met voegwoorden (” en “,”, maar “,”, dus “) aan elkaar plakt. Ben jij de volgende Shakespeare?

Showcase: Sinterklaas cadeau-generator

De Sint verdeelt het werk over drie hulp-elfen, elk een methode:

  • IsBraaf(...): braaf of stout? (bool)
  • BerekenAantalCadeaus(...): hoeveel cadeaus (int)
  • GenereerCadeau(): een willekeurig cadeau (string)

Een TestenMaar()-methode maakt met Random willekeurige kinderen en draait alles in een loop.

Tip

Zelfde idee als de Avengers: splits het probleem op, laat elke methode één ding goed doen.

Conclusie

  • Een methode is een herbruikbaar codeblok: minder fouten, betere leesbaarheid, en ze doet één ding
  • void geeft niets terug; anders gebruik je return met het juiste type, en vang je het resultaat ook op
  • Parameters gaan by value (een kopie); de volgorde telt
  • Named en optionele parameters en overloading maken je methoden flexibel
  • Met /// en IntelliSense werk je sneller en met betere documentatie

Tip

Volgende halte: arrays, om vele waarden samen in één variabele bij te houden.

Meer weten

Kennisclips

Oefeningen H7 · Quizlet flashcards

Zie verder: andere talen

Dezelfde concepten uit dit hoofdstuk, maar dan in andere talen. Handig om later code in Python of JavaScript te leren lezen.

Zie verder: methode-signatuur

Python doet dit zo:

def tel_op(a, b):
    return a + b

Geen returntype, geen types voor parameters. Dat scheelt typewerk, maar Python merkt een verkeerd type pas tijdens het uitvoeren.

Zie verder: bibliotheken importeren

In C# haal je bibliotheken binnen met using. Python doet dit met import:

import math

print(math.sqrt(16))

Hetzelfde idee als bij C#: je geeft aan welke bibliotheek je wilt en spreekt de methoden aan via de naam (math.sqrt, zoals Math.Sqrt).

Zie verder: overloading

Python kent geen overloading: een tweede functie met dezelfde naam overschrijft de eerste. Meestal is dat niet nodig, want * werkt er op getallen én op strings:

def verdubbel(waarde):
    return waarde * 2

print(verdubbel(21))    # 42
print(verdubbel("ha"))  # haha

Wil je per type toch iets anders doen, dan schrijf je die keuze zelf met een if isinstance(...).

Samenvattende poster

Een handige samenvattende poster (Opgelet: gemaakt m.b.v. ChatGPT Image Gen 2).