wiki:aliohjelmienKutsuminen
Last modified 11 months ago Last modified on 2016-09-29 15:36:21

Dokumentti siirretty TIMiin


Luentomonisteen täydennykset » Aliohjelmien kutsuminen

Katso myös Aliohjelmien kirjoittaminen.

0. Aluksi

Aliohjelmakutsuihin EI tule tyyppejä (ellei samalla luoda new:llä uusia olioita, mutta siitä myöhemmin). Olisi väärin kirjoittaa seuraavasti.

y = double Math.Sin(double x); // TÄMÄ ON VÄÄRIN!!! Kutsuu ei tule tyyppejä!

Alla oleva on oikein (olettaen että x on hyvin määritelty).

y = Math.Sin(x);

Oikeastaan "vaikeinta" aliohjelmakutsuissa on päättää onko kyseessä staattisen aliohjelman kutsu (luku 1.) vai olion metodin kutsu (luku 2). Tämä päätös voidaan tehdä katsomalla aliohjelman esittelyrivistä lukeeko siinä static vai ei.

1. Miten aliohjelmia kutsutaan

1.1 Miltä aliohjelmakutsu näyttää

Jo kurssin ensimmäisistä esimerkeistä alkaen on kutsuttu aliohjelmia, funktiota, muodostajia ja metodeja:

System.Console.WriteLine("Moikka!"); // aliohjelmakutsu (static)
Camera.ZoomToLevel();                // metodikutsu
// Seuraavana muodostajan kutsu:
PhysicsObject p1 = new PhysicsObject(2 * 100.0, 2 * 100.0, Shape.Circle);
Add(p1);                             // oman metodin kutsu
PiirraLumiukko(this, 0, 100);        // oman luokan aliohjelman kutsu (static)
ala = KolmionAla(kanta, korkeus);    // oman luokan funktion kutsu (static)

Eli aliohjelmakutsuissa (tai metodi/funktio) ei ole sinällään mitään ihmeellistä. Pitää vain tietää aliohjelman nimi ja parametrilista. Jos kyseessä on funktio, tulos ehkä sijoitetaan johonkin apumuuttujaan tms.

1.2 Mistä löytää miten aliohjelmia kutsutaan

Tätä varten pitää osata lukea dokumentaatiota. Esimerkiksi: WriteLine-aliohjelman kohdalta: http://msdn.microsoft.com/en-us/library/xf2k8ftb%28v=vs.100%29.aspx löytyy mm:

Namespace:  System
Console.WriteLine Method (String)
public static void WriteLine(string value)

Tätä luetaan seuraavasti:

  1. aliohjelma on kirjoitettu System-nimiavaruuden Console-luokan staattiseksi (static) aliohjelmaksi ja se ei palauta mitään (void).
  2. aliohjelma ottaa parametrikseen yhden merkkijonon. Kutsussa pitää siis olla jokin merkkijonoarvoinen lauseke.
  3. Eli laillinen kutsu olisi esimerkiksi:
System.Console.WriteLine("Hello World!");

Toisaalta jos koodin alussa on using System; niin nimiavaruutta ei tarvitse enää erikseen sanoa (mutta saa sanoa jos haluaa):

using System;
...
   Console.WriteLine("Hello World!");

1.3 Arvon palauttava aliohjelma, eli funktio

Tutkitaan esimerkiksi Char-luokan funktiota ToLower (dokumentaatio), joka antaa kirjaimen pienenä kirjaimena:

Namespace:  System
Char.ToLower Method (Char)
public static char ToLower(char c)
  1. Jälleen funktio on System-nimiavaruudessa, joten kutsuun tulee ensin System. (huomaa piste.) tai ohjelman alussa on oltava using System;
  2. Funktio on staattinen Char-luokassa, eli kutsussa on oltava myös Char.
  3. Funktio palauttaa char-tyypisen arvon, eli funktiota käytettäessä sen tulos usein sijoitetaan char-tyyppiseen apumuuttujaan.
  4. Funktiolle täytyy viedä parametrina jokin char-tyyppinen lauseke (esim char tyyppisen muuttujan arvo). Kutsuun ei missään nimessä kirjoiteta parametrien tyyppejä.
  5. Näin ollen kutsu voisi olla muotoa:
char eka = 'A';
char pieni = System.Char.ToLower(eka);

tai

using System;
...
   char pieni;
   pieni = Char.ToLower('A');

Toki funktion palauttamaa arvoa voidaan käyttää suoraankin, esim:

using System;
...
   Console.WriteLine(Char.ToLower('M'));

Vastaavasti trigonemetrista sin-funktiota käytettäisiin dokumentaation

Namespace:  System
Math.Sin Method 
public static double Sin(double a)

mukaan seuraavasti:

  1. Funktio on System-nimiavaruudessa, joten kutsuun tulee jälleen System. tai ohjelman alussa on oltava using System;.
  2. Funktio on staattinen Math-luokassa, eli kutsussa on oltava myös Math.
  3. Funktio palauttaa double-tyypisen arvon, eli funktiota käytettäessä sen tulos (usein, ei välttämättä) sijoitetaan double-tyyppiseen apumuuttujaan.
  4. Funktiolle täytyy viedä parametrina jokin double-tyyppinen lauseke (esim double tyyppisen muuttujan arvo). Kutsuun ei missään nimessä kirjoiteta parametrien tyyppejä.
  5. Näin ollen kutsu voisi olla muotoa:
using System;
...
double kulma = Math.PI / 4; // 45 astetta
double y;
y = Math.Sin(0.5*kulma); 

1.4 Eihän PiirraLumiukko-aliohjelman kutsussa ollut mitään pistettä!

Mutta eihän esimerkiksi LumiukkkoAli.cs:ssä olleessa lumiukon piirtämisessä ollut muuta kuin:

PiirraLumiukko(this, 0, Level.Bottom + 200.0);

Miksi?

Tuossa ei enempää tarvittu, koska PiirraLumiukko oli esitelty samassa luokassa kuin mistä sitä kutsuttiin. Toki kutsu olisi voinut olla myös

LumiukkoAli.PiirraLumiukko(this, 0, Level.Bottom + 200.0);

2. Metodin kutsuminen

Edellä käsiteltiin staattisten aliohjelmien kutsumista. Staattisia aliohjelmia voidaan kutsua ilman että yhtään vastaavaa oliota on luotu. Suuri osa C#:in aliohjelmista on kuitenkin olioiden metodeja. Tällöin metodin kutsumista varten pitää ensin olla luotu tai muulla tavoin saatu (silloinkin joku on jossakin vaiheessa luonut) olio (tai oikeastaan viite olioon).

2.1 StringBuilder ja String

Oletetaan että meillä pitäisi poistaa sanasta "Krokotiili" alkuosa ja lisätä sana "talo" tuohon perään. Tämä voitaisiin tehdä pelkästään luomalla uusia olioita String-luokan avulla, mutta muutoksia voidaan tehdä merkkijonossa suoraan jos jono luodaan StringBuilder-luokasta.

Valmis "ohjelma" tulee olemaan:

using System;
using System.Text;
...
StringBuilder sb = new StringBuilder("Krokotiili");
string s = sb.ToString();  // s = "Krokotiili"
int i = s.IndexOf('t');    // i = 5
sb.Remove(0, i);           // sb => tiili
sb.Append("talo");         // sb => tiilitalo
Console.WriteLine(sb);

Aluksi meidän pitäisi luoda siis uusi olio jonka sisältönä on jono "Krokotiili". Siksi on etsittävä StringBuilder-luokasta muodostaja (dokumentaatio), eli jolla tuo voitaisiin tehdä:

Namespace: System.Text
StringBuilder.StringBuilder(String) Constructor
public StringBuilder (string value)
  1. new luo uuden olion vaatiman muistitilan ja muodostajan tehtävä on alustaa oliolle varattu tila halutulla tavalla. Muodostajan nimi on C#:issa aina sama kuin luokan nimi. Muodostajia voi olla useita (sama nimi, eri parametrilista). Muodostajan esittelystä ei suoraan näe että sitä on useimmiten (aina?) käytettävä nimenomaan new-avainsanan kanssa. Tämä on siis tiedettävä siitä, kun nähdään kyseessä olevan muodostaja
using System.Text;
...
  StringBuilder sb = new StringBuilder("Krokotiili");
  1. Näin meille syntyi uusi StringBuilder luokan edustaja, jonka sisältönä on jono "Krokotiili" ja nyt apumuuttuja sb viittaa siihen, koska new palauttaa viitteen luomaansa olioon, jonka sisällön muodostaja laittoi pyydetyksi.

Seuraavaksi syntyneestä oliosta (eli muutettavasta merkkijonosta) pitäisi löytää 1. t-kirjain, mutta StringBuilder-luokasta ei tällaista metodia (valitettavasti???) löydy. Voimme tosin tehdä jonosta uuden String-tyyppisen kopion ja kysyä tältä t-kirjaimen paikkaa (dokumentaatio):

Namespace: System.Text
StringBuilder.ToString Method ()
public override string ToString()
  1. Koska ToString on metodi (ei siis staattinen aliohjelma), PITÄÄ sen kutsumiseksi olla olemassa (tässä tapauksessa) StringBuilder-tyyppinen olio. Sellainen meillä onneksi nyt on, eli sb viittaa tuollaiseen olioon.
  2. Metodille ei tule yhtään parametria (koska olio saa itsestään kaiken tarvitsemansa informaation kopioin luomiseen).
  3. Metodi palauttaa string-tyyppisen viitteen. override tarkoittaa, että tässä luokassa tehdään uusi versio perityn luokan metodista. Kutsumisen kannalta tällä tiedolla ei meille ole (tässä vaiheessa vielä) lisäarvo. Kutsu on siis muotoa:
string s = sb.ToString();

Tästä jonosta voimme etsiä t-kirjaimen paikkaa (String.IndexOf-dokumentaatio):

Namespace:  System
String.IndexOf Method (Char)
public int IndexOf(char value)
  1. Jälleen on kyse metodista, eli kutsumiseksi pitää olla String-tyypin olio (String ja string ovat samoja jos on Using System;. Meillä nyt s on viite sellaiseen olioon.
  2. Metodi tarvitsee yhden char-tyyppisen lausekkeen parametrikseen.
  3. Metodi palauttaa kokonaisluvun. Näin ollen kutsu voisi olla esim:
int i;
i = s.IndexOf('t');

Koska sb ja s ovat toistensa kopioita (olioita, joissa on sama sisältö, mutta asuvat aivan eri puolilla muistia), on i nyt samalla myös t-kirjaimen paikka sb-jonossa.

StringBuilder-tyyppisestä jonosta merkkejä voidaan oikeasti poistaa (StringBuilder.Remove-dokumentaatio):

Namespace: System.Text
StringBuilder.Remove Method 
public StringBuilder Remove(int startIndex,int length)
  1. Jälleen kyseessä on metodi, eli sen kutsumiseksi tarvitaan nyt StringBuilder-luokan oliota (koska on StringBuilder.Remove Method). Meillä sb on viite sellaiseen olioon.
  2. Metodi tarvitsee parametrikseen poistamisen alkupaikan ja poistettavien merkkien määrän. Jos haluamme poistaa jonon alkuosan t-merkkiin saakka, niin t:n paikka on itse asiassa tällä kertaa myös sitä edeltävien merkkien lukumäärä. Ja koska alusta, niin poiston alkupaikka on tällä kertaa 0.
  3. Metodi palauttaa viitteen StringBuilder tyyppiseen olioon, joka itse asiassa on viite samaan olioon, josta olemme poistamassa. Usein tehdään tällaisia metodeja jotta ketjutetut (esimerkki myöhemmin) kutsut olisivat sujuvampia. Tällä kertaa emme tarvitse tuota viitetietoa vaan voimme unohtaa sen (koska poisto tehdään oliosta itsestään ja siihen meillä on jo viite)
sb.Remove(0, i);  // poistaa 0:sta alkaen i merkkiä, eli jonoksi jää tiili

Lopuksi haluaisimme lisätä jonon loppuun jonon "talo": http://msdn.microsoft.com/en-us/library/b4sc8ca8%28v=vs.100%29.aspx

Namespace:  System.Text
StringBuilder.Append Method (String)
public StringBuilder Append(string value)
  1. Kuten edellä kyseessä on metodi ja sb osoittaa kivasti tarvittavaan olioon.
  2. Metodi tarvitsee parametrikseen lisättävän jonon. Mehän aiomme lisätä jonon "talo".
  3. Kuten edellellä kutsu muuttuu itse jonoa, mutta palauttaa myös viitteen (samaan) muutettuun jonoon. Tälläkään kertaa emme tarvitse muutettua jonoa. Joten kutsuksi riittää
sb.Append("talo");  // tiilitalo

Nyt voisimme vaikkapa tulostaa jonon sb sisällön.

Mikäli olisimme varmoja t:n löytymisestä (emme nytkään tarkistaneet että löytyykö t-kirjainta, eli että oliko i>=0), koko ketju voitaisiin kirjoittaa paluuviitteiden ansiosta yhdeksi ainoaksi lauseeksi:

using System;
using System.Text;
...
 StringBuilder sb2 = new StringBuilder("Krokotiili");
 Console.WriteLine(sb2.Remove(0,sb2.ToString().IndexOf('t')).Append("talo"));

2.2 String

Vastaava "ohjelma" tehtynä pelkästään String-luokan avulla olisi:

string s2 = "Krokotiili";
int tp = s2.IndexOf('t');
string s3 = s2.Substring(tp);
string s4 = s3 + "talo";
System.Console.WriteLine(s4);

Edeltä muistamme että IndexOf:in käyttöön tarvitsisimme olion, joka olisi luotu. Tässähän new lausetta ei esiinny??? Mutta tosiasiassa "Krokotiili" on oikeastaan vain lyhenne new String(new char[]{'K',...'i'}) . Eli siinä mielessä tarvittava luonti on kyllä tehty.

Subtring palauttaa viitteen uuteen merkkijono-olioon (koska itse String luokan olioita ei voi muuttaa (immutable)). Samoin s3 + "talo luo uuden olion ja palauttaa siihen viiteen. Näin ollen koko "ohjelman" aikana syntyy 3 uutta oliota (tai oikeastaan 4, koska syntyy myös jono "talo"). Olioita syntyy riippumatta siihen, minkä nimiseen muuttujaan viite sijoitetaan. Ohjelma olisi täsmälleen sama vaikka se olisi:

string s2 = "Krokotiili";
int tp = s2.IndexOf('t');
s2 = s2.Substring(tp);
s2 = s2 + "talo";
System.Console.WriteLine(s2);

Ainoa ero olisi, että "Krokotiili"-merkkijonoon ei päästäisi enää käsiksi ohjelman lopussa (kun siihen ei enää kukaan viittaa).

StringBuilder-oliolla tehtynä ei syntynyt näin montaa oliota.

Tämäkin voitaisiin kirjoittaa yhtenä ketjutettuna lauseena:

string s5 = "Krokotiili";
System.Console.WriteLine(s5.Substring(s5.IndexOf('t'))+"talo");

tai jopa:

Console.WriteLine("Krokotiili".Substring("Krokotiili".IndexOf('t')) + "talo");

Apumuuttujia käyttäen kuitenkin ehkä paljastuu paremmin miten monta oliota matkalla syntyy.

2.3 Omien metodien kutsuminen

Kurssilla ei hirveästi kirjoiteta omia luokkia. Kuitenkin esimerkissä

public class Lumiukko : PhysicsGame
{
    public override void Begin()
    {
        PhysicsObject p1 = new PhysicsObject(2 * 100.0, 2 * 100.0, Shape.Circle);
        p1.Y = Level.Bottom + 200.0;;
        this.Add(p1);
...

oleva Lumiukko-luokka perii kaikki PhysicsGame-luokassa olevat ominaisuudet. Siis myös metodit. Näin ollen Lumiukko-luokassa on myös metodi Add http://kurssit.it.jyu.fi/npo/material/latest/documentation/html/class_jypeli_1_1_game.html

Game luokkareferenssi:
void Add(IGameObject o)	
Lisää olion peliin, kerrokseen 0.

joka lisää minkä tahansa IGameObject rajapinnan (interface) toteuttavan peliolion peliin.

Add-metodin kutsumiseksi siis pitäisi olla olemassa Game-luokan olio. Kun kuitenkin olemme kirjoittamassa Lumiukko-luokan Begin metodia, niin sen olion, jonka metodia tässä kirjoitamme, on joku joutunut jo luomaan että Begin-metodiin päästään. Jypelissä yleensä Main-funktiossa luodaan peliluokka. Jokaisessa metodissa on käytettävissä avainsana this, joka viittaa siihen olioon, jonka metodia on kutsuttu. Begin-metodia kutsutaan Jypelissä itse peliluokasta sitten, kun sen luomisen perusjutut ovat valmiita.

Näin ollen kutsu on muotoa:

this.Add(p1);

Mutta C#:issa (kuten Javassa ja C++:ssakin) kutsut ovat oletuksena this-olioon jos muuta ei mainita. Eli em. on täsmälleen sama kuin kutsu:

Add(p1);  // kutsuu this-olion Add-metodia.