Aliohjelman kirjoittaminen

Katso myös Aliohjelminen kutsuminen.

1. Miksi aliohjelmia

Aliohjelmia tehdään, koska

  • niiden avulla voidaan jakaa ohjelma pienempiin osiin
  • ne jäsentävät ohjelmaa
  • ne tekevät koodista uudelleenkäytettävää
  • ne helpottavat testaamista

Aliohjelman tulee olla mahdollisimman riippumaton kokonaisuus, joka selviää tavalla tai toisella kaikista mahdollisista parametrien arvoista, joilla sitä kutsutaan.

Uudelleenkäyttöä ajatellen on oleellista että aliohjelman toteutusta kirjoitettaessa ei enää ajatella sitä, mistä aliohjelmaa kutsutaan. Esimerkiksi, aliohjelman dokumentaation ei kommentoida että "pääohjelmasta tuotu parametri -- --", koska aliohjelmaa voidaan kutsua mistä tahansa muualtakin.

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. Kirjoita (aluksi manuaalisesti, myöhemmin generoi automaattisesti) aliohjelman esittelyrivi (eng. header).
  5. Tee aliohjelmasta syntaktisesti oikea tynkä joka kääntyy, esimerkiksi funktioaliohjelmassa pitää olla return-lause.
  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 kohdat 8-10 kunnes toimii)
  11. Siirry seuraavaan aliohjelmaan.

2.1 Mihin kohtaan aliohjelma kirjoitetaan

Aliohjelma kirjoitetaan luokan sisään (eli luokkaa rajaavien aaltosulkujen sisäpuolelle), muiden aliohjelmien ulkopuolelle, esimerkiksi seuraavasti.

(Dokumentaatiot puuttuvat kaikista allaolevista aliohjelmista tilan säästämiseksi.)

public class MunLuokka
{
 void Aliohjelma()
 { 
 } // Laita aliohjelman loppusulun perään 2 tyhjää riviä


 void ToinenAliohjelma()
 {
 }
}

Alla pari esimerkkejä, miten voi kirjoittaa väärin.

public class MunLuokka
{
}

void Aliohjelmani() // Aliohjelma on kirjoitettu luokan ulkopuolelle
{
}
public class MunLuokka
{
 void Aliohjelma()
 {
  void ToinenAliohjelma() // Aliohjelma toisen sisällä, ei käy!
  {
  }
 }
}

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

Ennen esittelyrivin kirjoittamista kirjoitetaan aliohjelman kutsu. Olkoon tavoitteena laskea kolmion pinta-ala. Pinta-alan laskemiseksi tarvitaan kolmion kannan pituus ja kolmion korkeus. Näillä tiedoilla aliohjelman kutsu voisi olla muotoa:

double ala = KolmionAla(3.0, 4.2);

Seuraavaksi lähdemme miettimään aliohjelman esittelyriviä.

  1. Tämä aliohjelma (joka on funktio, koska se palauttaa arvon) 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ä).

Esittelyrivi tähän mennessä näiden tietojen perusteella olisi siis:

public static double KolmionAla()
  1. Seuraavaksi tarvitaan funktion parametrit. Kutsun perusteella niitä näyttäisi olevan kaksi. Ja niiden tyyppi näyttäisi olevan double. Keksitään kaksi kuvaavaa nimeä parametreille ja annetaan niille tyypit. Muistetaan että C#:ssa, kuten Javassakin on tapana kirjoittaa parametrien (ja muiden lokaalien muuttujien) nimet pienellä alkukirjaimella.
public static double KolmionAla(double kanta, double korkeus)
  1. Täydennetään funktion toteutus sellaiseksi että se kääntyy (eli on syntaktisesti oikein, mutta loogisesti ei tee oikeata asiaa = tynkä). Tässä tapauksessa toteutus on yksinkertaisesti return 0;. Muista aaltosulut! Toistaiseksi funktio ei vielä palauta oikeaa pinta-alaa, mutta täydennämme toteutusta myöhemmin.
  2. Kirjoitetaan mahdolliset yksikkötestit. Alla vaiheet 6--7 toteutettuna.
# kolmionalatynka
  1. Kokeillaan että koko ohjelma toimii. Jos käytetään testejä, tehdään testit ja todetaan että tulee väärä tulos.
  2. Tehdään aliohjelman toteutus 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):
# kolmionalatynka2
  1. 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.

3.2.1 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ä.

3.2.2 Funktio

Mikäli aliohjelman palauttaa arvon, 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, 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;
}

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);
}

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).

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.