Aliohjelmat

Katso myös Aliohjelminen kutsuminen.

1. Miksi aliohjelmia

  • jakaa ohjelman osiin
  • jäsentää ohjelmaa
  • auttaa uudelleenkäytössä
  • helpottaa testaamista

Uudelleenkäytössä on oleellista että aliohjelman toteutusta kirjoitettaessa ei enää saa ajatella sitä, mistä aliohjelmaa kutsuttiin (ei kommentoida esim että "pääohjelmasta tuotu parametri", koska kutsu voi tulla mistä tahansa muualtakin. Aliohjelman tulee olla oma, mahdollisimman riippumaton kokonaisuus joka selviää tavalla tai toisella kaikista mahdollisista kutsuparametrien arvoista.

2. Aliohjelmien kirjoittaminen

  1. Jaa ongelma osiin
  2. mieti millaisella aliohjelmakutsulla pistät tietyn osaongelman ratkaisun käyntiin
  3. kirjoita aliohjelman kutsu ja mieti sen tarvitsemat parametrit
  4. generoi (aluksi manuaalisesti, myöhemmin automaattisesti) aliohjelman esittelyrivi (eng. header)
  5. tee aliohjelmasta syntaktisesti oikea tynkä (esim. funktioaliohjelma muotoon return 0;
  6. dokumentoi aliohjelma (nyt unohda mistä sitä kutsuttiin, sitä et enää saa ajatella)
  7. kirjoita testit
  8. aja testit (pitää "feilata")
  9. muuta aliohjelma toimivaksi
  10. aja testit (toista 9 kunnes toimii)
  11. siirry seuraavaan aliohjelmaan

2.1 Mihin kohti aliohjelma kirjoitetaan

  • aliohjelma kirjoitetaan luokan aaltosulkujen sisälle, kaikkien aliohjelmien ulkopuolelle
  • aliohjelmat voidaan kirjoittaa luokkaan mihin järjestykseen tahansa. Seuraavia tapoja voidaan suosia:
    1. Kutsuttava aliohjelma on aina kutsun jäljessä
    2. Kutsuttava aliohjelma on aina ennen kutsua
    3. Aliohjelmat ovat aakkosjärjestyksessä
    4. Muodostajat ensin, sitten joku tavoista 1-3.
  • valitset minkä tahansa edellisistä, noudata samaa järjestetystä kaikissa luokissasi

3. Aliohjelman esittelyrivin (header) kirjoittaminen

3.1 Esimerkki: Yksinkertainen funktio

Olkoon ongelma vaikka laskea kolmion pinta-ala. Silloin aliohjelman kutsu voisi olla muotoa:

double ala = KolmionAla(3.0,4.2);

Tästä lähdemme miettimään aliohjelman esittelyriviä.

  1. Tämä aliohjelma (tai siis funktio) on niin yleiskäyttöinen että kuka tahansa voisi sitä käyttää, siis public.
  2. Funktio voi tehdä kaikki hommansa pelkästään parametriensa avulla, siis static.
  3. Funktion tulos sijoitetaan kutsun perusteella double tyyppiseen muuttujaan, joten paluuarvon tyyppi on double.
  4. Funktion nimi näyttäisi olevan KolmionAla. Muista että C#-tyylissä on tapana aloittaa aliohjelmien nimet isolla kirjaimella (Javassa pienellä).
  5. Esittelyrivi tähän mennessä näiden tietojen perusteella olisi siis:
    public static double KolmionAla()
    
  6. Seuraavaksi tarvitaan funktion parametrit. Kutsun perusteella niitä näyttäisi olevan kaksi. Ja niiden tyyppi näyttäisi olevan double. Eli keksitään kaksi kuvaavaa nimeä ja annetaan niille tyypit. Nyt muistetaan että C#-tyylissä (samoin kuin Javassakin) on tapana kirjoittaa parametrien (ja lokaalien muuttujien) nimet alkamaan pienellä kirjaimella:
    public static double KolmionAla(double kanta, double korkeus)
    
  7. Tehdään aliohjelmasta aluksi syntaktisesti oikea (mutta ehkä väärän tuloksen antava, muista aaltosulut):
    public static double KolmionAla(double kanta, double korkeus)
    {
       return 0;
    }
    
  8. Kokeillaan että koko ohjelma toimii. Jos käytetään testejä, tehdään testit ja todetaan että tulee väärä tulos.
  9. Korjataan aliohjelma toimivaksi. Tässä vaiheessa unohda kaikki muu maailma ja käytä vaan sitä tietoa mitä näet aliohjelman esittelyrivillä (ja mahdollisesti tekemissäsi lokaaleissa muuttujissa):
    public static double KolmionAla(double kanta, double korkeus)
    {
       return kanta*korkeus/2.0;
    }
    
  10. Ajetaan testit uudelleen ja todetaan toimivaksi.

3.2 Aliohjelman tyyppi

Aliohjelman tyypillä tarkoitetaan sitä, palauttaako aliohjelma tietoa vai onko se ns. void-aliohjelma (ei palauta tietoa). void-aliohjelmia nimitetään (tällä kurssilla) puhtaiksi aliohjelmiksi ja aliohjelmia, jotka palauttavat tietoa, funktioiksi. Nämä kaksi tyyppiä esitellään seuraavaksi tarkemmin.

Terminologiasta väitellään kirjallisuudessa runsaasti, mutta aliohjelmatyyppien tärkein ero on juuri se, palauttaako se tietoa vai ei.

Aloita aina miettimällä miten aliohjelmaa kutsuttaisiin, ja päätä tyyppi sen perusteella.

Puhdas aliohjelma

Jos aliohjelmaa kutsutaan niin, ettei sen odoteta palauttavan arvoa/oliota/tietoa, sanotaan sitä puhtaaksi aliohjelmaksi. Esimerkki:

int[] luvut = {1, 2, 3};

Tulosta(luvut);

Yllä on aliohjelman Tulosta kutsu. Annamme aliohjelmalle (jota ei vielä ole olemassa) parametrina int-taulukon luvut, jossa on muutama kokonaisluku.

Edelleen, kirjoitetaan aliohjelman esittelyrivi ja aaltosulut. Varsinaisen aliohjelman toteutuksen jätämme tässä harjoitustehtäväksi.

public static void Tulosta(int[] luvut) 
{
  ...
}  

Miten kutsusta voidaan päätellä (generoida) aliohjelman esittely public static void Tulosta(int[] luvut) ? Seuraava kuva selkiyttää asiaa.

Huomaa, että kutsun vasemmalla puolella ei ole sijoituslausetta (eli muuttujaa ja on yhtäkuin -merkkiä). Toisin sanoen, Tulosta-aliohjelma ei palauta tietoa. Jos palauttaisi, niin tyypillisesti haluamme palautetun tiedon tallentaa johonkin muuttujaan. Näin ollen, Tulosta-aliohjelma on void-tyyppinen. Kaarisulkujen sisään tulevat parametrit ja niiden tyypit selvitetään kohdassa 3.2.

Toinen esimerkki void-aliohjelmasta alla.

PiirraPallo(game, 30, 30, 20);

public static void PiirraPallo(PhysicsGame game, double x, double y, double r) 
{
  ...
}

Tässä aliohjelmassa haluamme aliohjelman vain piirtävän haluamamme kokoisen pallon haluamaamme paikkaan -- ja siinä se. Aliohjelma ei palauta mitään, emmekä me sitä odota, koska PiirraPallo-kutsun vasemmalla puolella ei ole mitään.

Huomaa, että void-sanan paikka on aliohjelman nimen ja static-sanan välissä.

Funktio

Mikäli aliohjelman palauttaa arvon, olion tai ylipäätään jotain tietoa, on kyseessä funktio. Palautettu tulos voidaan sijoittaa muuttujaan tai käyttää muuten missä tahansa yhteydessä missä voi käyttää samantyyppistä lauseketta. Funktioista lisää perustietoa monisteessa, luku 9.

Aliohjelman paluuarvon tietotyyppi voidaan päätellä sijoituksen kohteena olevan muuttujan tietotyypistä, esimerkiksi:

double[] taulukko;
double s;
...

s = Summa(taulukko);

jolloin koska s on double, niin Summa-funktion paluuarvo esitellään double-tyyppiseksi.

public static double Summa(double[] t) 
{
  ...
}

Alla oleva kuva selkiyttää asiaa.

Huomaa tässä nuolen suunta: Syy double-sanalle löytyy sijoituksen kohteesta ja sen tietotyypistä.

Alla toinen esimerkki, missä funktio LaskeKirjaimet laskee tietyn merkin esiintymien lukumäärän annetussa merkkijonossa. Muuttuja maara on int-tyyppiä, joten LaskeKirjaimet-funktion paluuarvon tyyppi on int.

int maara;
maara = LaskeKirjaimet(jono, 'a');

public static int LaskeKirjaimet(String jono, char kirjain) 
{ 
  ...
}

Tärkeää! Se, että aliohjelma muuttaa jotakin (esimerkiksi parametrina tulevaa tietoa), tulostaa jotakin (mikä ei yleisesti ole hyvä asia), ei ole sama asia kuin että aliohjelma palauttaa jotakin. Otetaan vielä yksi naiivi esimerkki. Olkoon meillä funktio LaskeYmpyranAla, joka palauttaa ympyrän pinta-alan sille parametrina annetun säteen perusteella. Funktio suorittaa laskutoimituksen ja palauttaa tuloksen (eli ympyrän pinta-alan), jonka tallennamme johonkin muuttujaan. Kirjoitetaan tämä vielä koodina.

double ala = LaskeYmpyranAla(4.2);
...

public static double LaskeYmpyranAla(double sade)
{
  return Math.PI * sade * sade; // pii*r^2
}

Sen sijaan olisi jossain määrin epäloogista, jos aliohjelma olisi void-tyyppiä ja laskutoimituksen tulos "vain" tulostettaisiin näytölle. Tällöin emme saisi tallennettua tietoa mihinkään, eikä tiedon jatkojalostus onnistuisi.

LaskeYmpyranAla(4.2); 
...


public static void LaskeYmpyranAla(double sade)
{
  Console.WriteLine(Math.PI * sade * sade); // pii*r^2
}

3.3 Parametrien määrä ja tyypit

Seuraavaksi selvitä aliohjelman parametrien määrä, esim:

  int[] luvut;
  luvut = ...; 
  Tulosta(luvut);

Kutsussa Tulosta on yksi parametri (aliohjelmaa kutsuttaessa usein näkee käytettävän myös termiä argumentti), joten aliohjelman esittelyriville esitellään myös yksi parametri. Parametri on muuttuja, ja kaikilla muuttujilla C#-kielessä on tyyppi. Parametrin tyyppi selviää kutsusta ja sitä edeltävästä koodista. Katso alla oleva kuva.

Kutsussa (!) annetun parametrin nimi on luvut. Taulukko luvut on esitelty hieman aiemmin, ja se on tyypiltään int-taulukko. Taulukon tunnistaa tyypin perässä olevista hakasuluista ([]). Tällöin aliohjelmassa esitellyn parametrin tyyppi on myös oltava int[]. Esittelyrivi on esitetty vielä kokonaisuudessaan alla.

public static void Tulosta(int[] luvut) 
{ 
 ... 
}

Otetaan hieman monimutkaisempi esimerkki:

  String[] korvattavat = {"kissa", "koira", "kana"};
  StringBuilder jono;
  int maara;
  jono = new StringBuilder("kissa istuu puussa ja koira vahtii alla");

  maara = KorvaaKaikki(jono, korvattavat, 4, "mato") ;

Funktio palauttanee tiedon siitä, montako korvausta se teki. Parametreja näyttäisi olevan neljä kappaletta: jono, korvattavat, 4 ja "mato". Paluuarvon tyyppi on maara-muuttujan perusteella int. Näin ollen tiedämme, että KorvaaKaikki-funktion esittelyrivi alkaa sanoilla public static int KorvaaKaikki, jonka jälkeen esitellään parametrit, tehdään se seuraavaksi.

  • jono on StringBuilder, joten ensimmäinen parametri esitellään StringBuilder-tyyppiseksi
  • 2. parametri korvattavat on String[] eli String-taulukko
  • 3. parametri on jotakin mille voi sijoittaa luku 4, yleensä int, mutta voisi olla myös double joissakin tapauksissa. Käyttötarkoitus pitäisi tietää. Tässä tapauksessa se voisi olla että montako korvausta korkeintaan tehdään, jolloin int-luku on luonnollisin valinta.
  • 4. parametrin on jotakin, mille voi sijoittaa "mato". Tuo on vakiomerkkijono, joten sijoituksen voi tehdä merkkijonolle, eli tyyppi on String.
  • nyt koko esittelyrivi on siis (esittelyrivi voi olla useammallakin rivillä):
public static int KorvaaKaikki(StringBuilder jono,
                               String[] korvattavat,
                               int maxMaara, 
                               String millaKorvataan)
{ 
  ...
}

Alla oleva kuva selventää kutsussa annettujen parametrien ja aliohjelmassa esiteltyjen vastinparametrien suhdetta.

3.4 Parametrien nimeäminen

Aliohjelman esittelyssä parametrien nimet saavat olla mitä tahansa

  • Ne voivat olla samoja kuin kutsussa
  • Ne voivat olla (syntaksin rajoissa) mitä tahansa muutakin
  • Jos kutsussa on vakioita (edellä 3 ja "mato"), pitää parametrille joka tapauksessa itse keksiä kuvaava nimi. Tällöin parametrien merkitys pitää itse tietää, jotta parametrille voi antaa hyvän ja kuvaavan nimen.
  • Parametrien nimeämiseen pätee samat säännöt ja suositukset kuin muihinkin muuttujien nimeämisiin, ks. moniste 7.4 Muuttujien nimeäminen

3.5 Tyypillisiä virheitä

Alla on lueteltu yleisiä virheitä, esimerkkinä KorvaaKaikki-funktio.

public static int KorvaaKaikki(String jono, ...
  • Visual Studion antamat virheet
    • The best overloaded method match for 'MyClass.KorvaaKaikki(string, string[], int, string)' has some invalid arguments
    • Argument '1': cannot convert from 'System.Text.StringBuilder' to 'string'
  • Ei voi sijoittaa String-tyypille StringBuilder-muuttujaa (tai oikeastaan viitettä)
public static int KorvaaKaikki(StringBuilder jono, String korvaus, ...
  • korvaus on esitelty merkkijonona (String), joten siihen ei voi sijoittaa merkkijonotaulukkoa (String[])
public static int KorvaaKaikki(StringBuilder jono, String[] korvattavat, int 3, ...
  • 3 ei ole muuttujan nimi, koska C#:ssa muuttuja ei voi alkaa numerolla
public static int KorvaaKaikki(StringBuilder jono, String[] korvattavat, int n, "kissa")
  • "kissa" ei ole muuttujan nimi
public static int KorvaaKaikki(jono, String[] korvattavat, int n, String milla)
  • jono-muuttujalta puuttuu tyyppi
public static int KorvaaKaikki(StringBuilder , String[] korvattavat, int n, String milla)
  • ensimmäiseltä parametrilta puuttuu nimi
public static int KorvaaKaikki(StringBuilder jono, String[] korvattavat, int n)
  • liian vähän parametreja kutsuun verrattuna
public static int KorvaaKaikki(StringBuilder a, String[] b, int c, String d)
  • syntaktisesti oikein, surkeat parametrien nimet
  • Huom! Se että aliohjelman muuttaa jotakin (esimerkissä tuota StringBuilder-parametriä) tai tulostaa jotakin (mikä ei yleisesti ole hyvä asia), ei ole sama asia kuin että aliohjelma palauttaa jotakin.
public static int KorvaaKaikki(StringBuilder jono, String[] korvattavat, int maxMaara, String millaKorvataan);
  • Aliohjelman esittelyrivin perään ei tule puolipistettä.
  • Tämä yleinen virhe johtuu tavallisesti siitä, että aliohjelman esittelyrivin kirjoittamisen jälkeen (ennen aaltosulkujen kirjoittamista) Visual Studio neuvoo virheellisesti kirjoittamaan puolipisteen. ("; expected").

4. Syntaktisesti oikean (tynkäaliohjelman) kirjoittaminen

Olkoon meillä seuraavat muuttujat, niiden alustukset, aliohjelmakutsu ja aliohjelman esittely.

String[] korvattavat = {"kissa", "koira", "kana"};
StringBuilder jono;
int maara;
jono = new StringBuilder("kissa istuu puussa ja koira vahtii alla");

maara = KorvaaKaikki(jono, korvattavat, 4, "mato") ;

public static int KorvaaKaikki(StringBuilder jono,
                               String[] korvattavat,
                               int maxMaara, 
                               String millaKorvataan)
{ 
  // Aliohjelma toteutetaan tässä
}

Kääntäminen ei kuitenkaan vielä onnistu. Visual Studio antaa seuraavan virheilmoituksen.

Tämä johtuu siitä, että olemme yllä luvanneet, että KorvaaKaikki palauttaa int-arvon, siis kokonaisluvun. Tämän pitäisi tapahtua aina. Toisin sanoen, aliohjelmasta ei saa eikä voi olla "ulos" sellaista reittiä (codepath), joka ei palauta tätä lupaamaamme int-tyyppistä lukua.

Tynkäaliohjelman kirjoittamisella tarkoitetaan pienintä mahdollista vaivannäköä, jolla aliohjelma saadaan syntaktisesti oikeaksi. Huomaa, että aliohjelman ei tarvitse tässä vaiheessa tehdä vielä mitään "kunnollista". Riittää, että se menee kääntäjästä läpi. Käytännössä voisimme palauttaa minkä tahansa int-luvun saadaksemme aliohjelman syntaksin oikeaksi -- olkoon se tässä nolla.

public static int KorvaaKaikki(StringBuilder jono,
                               String[] korvattavat,
                               int maxMaara, 
                               String millaKorvataan)
{ 
  return 0;
}

Tynkäaliohjelman tekemisellä varmistumme siitä, että aliohjelmakutsu ja aliohjelman esittely ovat onnistuneet -- ainakin syntaktisesti. Jos kirjoittamamme tynkä menee kääntäjästä läpi, toimii se tavallaan "checkpointtina" ohjelmassamme. Ohjelmia kirjoitettaessa pitäisi aina pyrkiä tekemään pieniä muutoksia kerrallaan, ja aina muutosten tekemisen jälkeen todeta ohjelman toimivuus. Tässä osaltaan auttaa myös automaattinen testaus (ks. ComTest).

5. Aliohjelman dokumentointi

Attachments