Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 5.6: Silmukoita, merkkijonoja ja vaalit

Tästä sivusta:

Pääkysymyksiä: Miten käytän itse silmukoita kirjoittaessani luokkaa? Saisiko lisäesimerkkejä for-silmukoista? Mitä jos laittaa silmukoita sisäkkäin? Jospa FlappyBugissa olisi useita esteitä?

Mitä käsitellään? Käytännön harjoitusta edellisen luvun aiheesta. Lisäksi: Merkkijonojen ja Range-olioiden läpikäyminen. Sisäkkäiset for-silmukat.

Mitä tehdään? Lähinnä ohjelmoidaan.

Suuntaa antava työläysarvio:? Neljä tai viisi tuntia.

Pistearvo: A220.

Oheismoduulit: ForLoops, Election (uusi), FlappyBug.

../_images/person03.png

Johdanto

Tähänastisissa esimerkeissä for-silmukoilla on käyty läpi vain vektoreita ja puskureita. Mutta kuten luvussa 5.5 mainittiin, voi for-silmukalla käydä läpi hyvinkin erilaisia alkiokokoelmia. Vaikkapa merkkijonoja:

Merkkijonon läpikäynti

Sanotaan esimerkin vuoksi, että halutaan tulostaa raportti kustakin tietyn merkkijonon sisältämästä merkistä. Merkkijonolle "kissa" tulostettaisiin näin:

Merkki: k
Merkki: i
Merkki: s
Merkki: s
Merkki: a

Tämä onnistuu seuraavalla Scala-koodilla.

for merkki <- "kissa" do
  println("Merkki: " + merkki)

Merkkijonokin on kokoelma merkkejä — siis Char-tyyppisiä arvoja (luku 5.2) — ja sitäkin voi käyttää tässä nuolen oikealla puolella.

Muuttuja merkki on siis tyyppiä Char.

Silmukan runko toistuu yhtä monta kertaa kuin merkkijonossa on merkkejä. Merkit käydään läpi järjestyksessä alusta loppuun.

Entä jos haluttaisiinkin numeroitu tuloste ja lisäksi merkkien Unicode-arvot?

Indeksillä 0 on k, Unicodessa merkki #107.
Indeksillä 1 on i, Unicodessa merkki #105.
Indeksillä 2 on s, Unicodessa merkki #115.
Indeksillä 3 on s, Unicodessa merkki #115.
Indeksillä 4 on a, Unicodessa merkki #97.

Yksi toteutustapa on tämä:

var indeksi = 0
for merkki <- "kissa" do
  println("Indeksillä " + indeksi + " on " + merkki + ", Unicodessa merkki #" + merkki.toInt + ".")
  indeksi += 1

Käytetään askeltajamuuttujaa pitämään kirjaa indeksistä, jossa ollaan menossa. Sen arvoa kasvatetaan yhdellä joka kierroksella aina tulostamisen jälkeen.

Char-luokassa on määritelty toInt-metodi, joka palauttaa kyseisen merkin Unicode-arvon kokonaislukuna.

Äskeinen ohjelma löytyy myös ForLoops-oheismoduulista pakkauksesta o1.looptest, samoin kuin usea muu tämän luvun esimerkki ja tehtävä.

Tutki debuggerilla

Debuggeri on ohjelman suoritusvaiheiden tutkimiseen sopiva aputyökalu. Esimerkiksi IntelliJ’hin sisältyy debuggeri.

Tässä kurssimateriaalissa on erillinen sivu, joka esittelee debuggerin käyttöä.

Suosittelemme, että tutustut debuggerimateriaaliin ennen kuin jatkat tässä luvussa. Tämän luvun ohjelmia voi käydä läpi debuggerilla rivi riviltä, mikä voi auttaa hahmottamaan niiden toimintaa ja toimii myös harjoituksena debuggerin käytöstä.

Debuggeri voi auttaa virheiden etsinnässä monissa tulevissa harjoitustehtävissä ja kurssin ulkopuolella ohjelmoidessasi.

Pieni ohjelmointitehtävä: vain isot kirjaimet

Moduulin ForLoops pakkauksesta o1.looptest löytyy task2.scala. Olion toivottu toiminta on kuvattu sen ohjelmakoodin kommentissa. Täydennä olio toiveiden mukaiseksi.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Pieni vaikka monisanaisempi tehtävä: DNA:n ominaisuuksia

Solujemme DNA:n keskeisiä rakennusaineita ovat neljänlaiset emäkset (base): guaniini (G), adeniini (A), sytosiini (C) ja tymiini (T). DNA-säiettä voidaan kuvata luettelemalla sen emäkset järjestyksessä, esimerkiksi GATCACAGGT ja niin edelleen. Eliön DNA on yksilöllinen, mutta saman lajin yksilöiden väliset erot ovat pieniä ja lajien välilläkin on merkittäviä yhtäläisyyksiä.

DNA:n eräitä ominaisuuksia kuvaa sen GC-pitoisuus (GC content) eli prosenttiluku, joka kertoo, kuinka suuri osuus emäksistä on guaniinia tai sytosiiniä. Esimerkiksi DNA-pätkän ATGGACAT GC-pitoisuus on 37,5 %, koska kahdeksasta emäksestä kolme on guaniini tai sytosiini. GC-pitoisuudella on merkitystä esimerkiksi geeniteknologiassa (ks. esim. Wikipedia).

Ota esiin task3.scala. Voit ajaa tämän pikkuohjelman, joka toimii tekstikonsolissa näin:

Enter a species file name (without .mtdna extension): human
The GC content is 80.13914555929992%.

Ohjelma näyttää siis laskevan käyttäjän mainitseman lajin DNA:n GC-pitoisuuden. Syötteeksi sille voi antaa jonkin merkkijonoista human, chicken, chimpanzee, mouse, opossum, pufferfish, yeast tai test; ohjelma käyttää apunaan vastaavannimisiä moduuliin liitettyjä tiedostoja, joihin on kirjattu lajien DNA-sekvenssejä (tarkemmin sanoen mitokondrio-DNA-tietoja; mtDNA).

Ohjelman raportoima tulos on kuitenkin pielessä, koska tiedostossa task3.scala määritelty funktio gcContent on virheellisesti toteutettu. Tuloksen kuuluisi syötteellä human olla reilut 44 % eikä 80 %.

Tehtäväsi on tutustua funktion toteutukseen (löytyy mainitusta tiedostosta sekä alta) ja tehdä siihen tarvittava pikkuinen muutos, joka saa sen toimimaan.

Funktion tulisi laskea parametriksi annettua merkkijonoa vastaava GC-pitoisuus, eli esimerkiksi parametriarvolla "ATGGACAT" palautettaisiin Double-arvo 37.5. Tässä leluesimerkissämme kaikki merkityksettömät merkit, kuten välilyönnit ja emästä kuvaamattomat kirjaimet, yksinkertaisesti ohitetaan, joten esimerkiksi merkkijono "ATG G ACATXXYZYZ" tuottaisi myös tuloksen 37.5.

def gcContent(dna: String) =
  var gcCount = 0
  var totalCount = 0
  for base <- dna do
    if base == 'G' || base == 'C' then
      gcCount += 1
    else if base == 'A' || base == 'T' then
      totalCount += 1
  end for
  100.0 * gcCount / totalCount

Char-olioillekin on literaalimerkintä vastaavasti kuin luvuille, totuusarvoille ja merkkijonoille. Toisin kuin merkkijonoliteraali, Char-arvo eli yksittäinen merkki kirjoitetaan yksinkertaisiin hipsukoihin.

Jos haluat kokeilla ohjelmaa itse valitsemillasi kirjainsekvensseillä, voit muokata kansiosta mkDNA_examples löytyvää tiedostoa test.mtdna ja antaa ohjelmalle syötteen test.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lukujen läpikäyminen: to, until ja indices

Luvussa 5.2 mainittiin, että Int-olioilla on metodi nimeltä to, jota on yleensä mukavinta käyttää operaattorinotaatiolla eikä pistenotaatiolla. Se palauttaa viittauksen Range-olioon:

5 to 15res0: Range.Inclusive = Range 5 to 15

Range-olio edustaa kokonaislukuluetteloa. Sekin on alkiokokoelma, jonka voi käydä läpi for-silmukalla:

val tuhanteen = 1 to 1000
for luku <- tuhanteen do
  println(luku)

Silmukkamuuttuja luku toimii askeltajana, joka saa vuorollaan kunkin positiivisen kokonaisluvun 1, 2, 3, ..., 1000. Ohjelma tulostaa nämä luvut järjestyksessä omille riveilleen.

Äskeisen ohjelman voi kirjoittaa yksinkertaisemminkin:

for luku <- 1 to 1000 do
  println(luku)

Vastaavaa käskyä voi käyttää myös indeksejä käyttävän kokoelman — esimerkiksi merkkijonon — indeksien läpikäymiseen. Tämä ohjelma tuottaa tutun tulosteen:

val merkkijono = "kissa"
for indeksi <- 0 to merkkijono.length - 1 do
  val merkki = merkkijono(indeksi)
  println("Indeksillä " + indeksi + " on " + merkki + ", Unicodessa merkki #" + merkki.toInt + ".")

Jos käytämme to-metodia, on parametriarvoksi muistettava antaa vähennyslaskun merkkijono.length - 1 tulos, jotta nollasta alkava indeksi pysyy rajoissa.

Pieniä ohjelmointitehtäviä: to, until yms.

Moduulin ForLoops pakkauksesta o1.looptest löytyy lisää pikkuohjelmia, jotka on tarkoitus muokata: katso nyt tiedostot task4.scala, task5.scala ja task6.scala. Olioiden toivottu toiminta on myös kuvattu noissa tiedostoissa. Täydennä ne toiveiden mukaisiksi.

Nämä pienet tehtävät ovat toisistaan riippumattomia. Voit tehdä ja palauttaa ne yksitellen, järjestyksessä.

Muista testata ohjelmia ensin itse. Käytä debuggeria virheiden paikallistamisessa apuna, kun on tarvetta.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

indices-metodi

Kun halutaan käydä läpi kaikki merkkijonon tai muun kokoelman indeksit, on kokoelman indices-metodi usein kätevä vaihtoehto until- ja to-metodeille.

val merkkijono = "kissa"merkkijono: String = kissa
for indeksi <- merkkijono.indices do
  println("Indeksillä " + indeksi + " on " + merkkijono(indeksi))Indeksillä 0 on k
Indeksillä 1 on i
Indeksillä 2 on s
Indeksillä 3 on s
Indeksillä 4 on a

Tämä perustuu siihen, että indices palauttaa juuri sellaisen Range-olion, jonka olisimme voineet until-metodillakin luoda:

merkkijono.indicesres1: Range = Range 0 until 5

Election-tehtävä

Tehtävänanto

../_images/module_election.png

Election-ohjelman rakenne on varsin yksinkertainen.

Tehtäväsi on

  • noutaa Election-moduuli,

  • tutustua moduulin Scaladoc-dokumentaatioon ja valmiina annettuun Candidate-luokan ohjelmakoodiin, joka kuvaa ehdokkaita vaaleissa,

  • kirjoittaa Scala-ohjelmakoodi, joka toteuttaa puuttuvan luokan District, joka kuvaa vaalipiirejä, ja

  • (tietysti) testata, että luokka toimii ja korjata tarvittaessa.

Yleisiä ohjeita ja vinkkejä

  • District-luokka muistuttaa monella tavalla luvun 5.5 AuctionHouse-luokkaa. Kertaa tuota lukua tarvittaessa.

    • Useassa District-luokan metodissa voi käyttää for-silmukkaa toteutuksen osana.

    • Luvussa 5.5 nähty ohjelmakoodi voi toimia inspiraationa District-luokan toteutukselle.

  • Käytä annettua käynnistysfunktiota testElection, joka kutsuu District-luokan metodeita. Voit muokata sitä testataksesi erilaisia tapauksia.

    • Ellei sinulla ole jonkinlaista toteutusta kullekin District-luokan metodille, ei testElection-ohjelmaa voi annetussa muodossa ajaa.

    • Voit testata testiohjelmalla osittaistakin District-toteutusta, jos "kommentoit ulos" puuttuvien metodien kutsut testiohjelmasta tai laadit toistaiseksi toteuttamattomille District-luokan metodeille aluksi toteutuksenraakileet (vaikkapa ???-merkintää käyttäen; luku 4.1).

  • Kannattanee edetä seuraavaksi kuvatuissa vaiheissa.

Suositellut työvaiheet

  1. Tutustu ensin dokumentaatioon. Huomaa, että usea District-luokan metodi on merkitty toteutettavaksi vasta huomattavasti myöhemmässä tehtävässä (luku 10.1). Nyt tässä luvussa kuuluu toteuttaa kaikki muut puuttuvat metodit paitsi nämä erikseen merkityt.

  2. Tutustu moduulin sisältämään ohjelmakoodiin.

  3. Ryhdy toteuttamaan District-luokkaa seuraavissa vaiheissa. Käytä testElection-ohjelmaa sitä mukaa kuin luot District-luokkaan uusia metodeita.

  4. Määrittele luokkaan District konstruktoriparametrit ja ilmentymämuuttujat.

  5. Luo toString-metodi.

  6. Luo printCandidates-metodi.

    • Voit ottaa mallia vaikkapa AuctionHouse-luokan nextDay-metodista.

  7. Luo candidatesFrom-metodi.

    • Voit ottaa mallia eräästä AuctionHouse-luokan metodista, jossa on samankaltainen perusajatus.

    • Huomaa, että palautusarvon tyypiksi on määrätty Vector[Candidate].

    Lisävinkkejä

    NäytäPiilota

    AuctionHousen purchasesOf-metodissa on samankaltainen perusidea: valitaan tietyt alkiot ja palautetaan kokoelma, jossa on vain ne.

    Voit käyttää puskuria palautettavan kokoelman koostamiseen, mutta siirrä puskurin sisältö lopuksi vektoriin toVector-metodilla.

  8. Luo topCandidate-metodi.

    • Tässäkin voi ottaa mallia eräästä AuctionHousen metodista.

    • Osaatko käyttää vektorin head- ja tail-metodeita niin, ettei metodissa turhaan vertailla ensimmäistä alkiota itseensä? (Ei välttämätöntä.)

    Lisävinkki: mistä voi ottaa mallia?

    NäytäPiilota

    AuctionHousen priciest-metodi on idealtaan samankaltainen. topCandidaten voi toteuttaa samalla algoritmilla.

  9. Luo totalVotes-nimiset metodit.

    Vinkki

    NäytäPiilota

    Voit ottaa mallia AuctionHouse-luokan totalPrice- ja numberOfOpenItems-metodeista.

Vapaaehtoinen mutta hyvin suositeltava lisätehtävä

Toimivatko kaksi totalVotes-metodiasi jo? Hyvä! Mutta tuliko niistä keskenään kovin samankaltaiset: useita samoja koodirivejä molemmissa?

Toisteisuus heikentää koodin muokattavuutta, kasvattaa virheiden mahdollisuutta eikä ole kaunista.

Yksi hyvä tapa tehdä koodista vähemmän toisteista on kirjata apumetodiin se osa algoritmista, joka on noille metodeille yhteistä. Tässä yhteinen osa on äänten laskeminen ehdokkaita sisältävästä vektorista: totalVotes-metodit eroavat toisistaan vain siinä, mitkä ehdokkaat otetaan mukaan laskentaan.

Apumetodin voi määritellä vaikkapa tähän tapaan:

private def countVotes(candidates: Vector[Candidate]) =
  // Laske yhteisäänet saadusta vektorista ja palauta summa.

Apumetodista on perusteltua tehdä yksityinen, koska metodi on tarkoitettu vain District-tyypin sisäiseen käyttöön.

Osaatko toteuttaa apumetodin countVotes ja toteuttaa totalVotes-metodit niin, että ne kutsuvat countVotes-metodia antaen sille asianmukaisen parametriarvon?

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ja vielä...

AuctionHouse-luokan metodeissa ja luultavasti myös useassa District-luokkaan laatimassasi metodissa toistuu sama kaava:

  1. Määritellään tulosmuuttuja ja asetetaan sille alkuarvo.

  2. Sitten käytetään silmukkaa, joka käy läpi kokoelman alkiot ja suorittaa niille jonkin operaation: for muuttuja <- kokoelma do

  3. Lopuksi palautetaan tulosmuuttujan arvo, joka määritettiin silmukan aikana.

Toistuvan kaavan vuoksi metodien koodissa esiintyy toisiaan muistuttavia rivejä.

Eikös tämäkin ole eräänlaista toisteisuutta? Voisiko jopa tämän välttää?

Kyllä voi ja tyylikkäällä tavalla voikin. Mutta se vaatii lisää esitietoja, joten palataan aiheeseen luvuissa 6.3 ja 7.1.

Pikkutehtävä: kuvan muodostaminen silmukalla

Tässä koodia pohdittavaksi ja ehkä REPLissäkin tutkittavaksi:

val vareja = Vector(White, Blue, Green, Black, Orange)
var yhdistelma = rectangle(150, 150, Red)
for vari <- vareja do
  val raita = rectangle(50, 150, vari)
  yhdistelma.leftOf(raita)
yhdistelma.show()

Mitkä kaikki seuraavista kuvaavat tämän ohjelman toimintaa oikein?

FlappyBug-tehtävä (osa 16/17: enemmän esteitä)

Tähän asti FlappyBug-pelissämme on ollut vain yksi este. Muokkaa peliä niin, että esteitä on useita. Kutakin estettä kuvaa oma Obstacle-olionsa, jotka tallennetaan vektoriin. Esteet käyttäytyvät keskenään samalla tavalla, mutta ne ovat erikokoisia ja kullakin on oma sijaintinsa. Toteuta muutokset for-silmukoita käyttäen.

Tarvittavat muutokset ovat:

  1. Esteet osaksi peliä: korvaa luokan Game obstacle-ilmentymämuuttujan osoittama 70-pikselinen este vektorilla, jossa on kolme estettä. Ensimmäisen koko on 70, toisen 30 ja viimeisen 20. Vaihda samalla obstacle-muuttujan nimeksi obstacles.

  2. Kukin este liikkeelle: muuta Game-luokan timePasses-metodia niin, että se liikuttaa kutakin vektoriin tallennetuista esteistä.

    • Varsin yksinkertainen for-silmukka riittää.

  3. Kuolinehdon päivitys: muuta Game-luokan isLost-metodia niin, että se palauttaa true, jos ötökkä ei ole pelialueella tai jos mikä tahansa esteistä koskettaa ötökkää.

    • Tässä kohdassa on vähän enemmän tekemistä (toistaiseksi opituilla konsteilla) kuin edellisessä. Jos et keksi ratkaisutapaa, katso vinkki alta.

    Vinkki

    NäytäPiilota

    Yksi mahdollisuus on käyttää for-silmukan apuna Boolean-tyyppistä var-muuttujaa: pidä kirjaa siitä, onko esteitä läpikäyvän silmukan suorituksen aikana löytynyt sellainen este, joka koskettaa ötökkää. Määrittele muuttuja metodin paikalliseksi muuttujaksi ennen for silmukan alkua.

  4. Kukin esteistä näkyviin: muuta käyttöliittymän makePic-metodia niin, että se muodostaa kuvan kustakin pelin estevektoriin tallennetusta esteestä ja asettaa nuo kuvat taustaa vasten. Viimeisenä päälle asetetaan edelleen ötökän kuva.

    Vinkki

    NäytäPiilota

    Käytä for-silmukan tukena Pic-tyyppistä kokoojamuuttujaa: samalla kun käyt läpi vektoria, päivität kokoojaa asemoimalla uuden estekuvan kokoojan vanhan arvon päälle. (Vrt. edellinen pikkutehtävä.)

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Uusi muuttujarooli: (yksisuuntainen) lippu

Äskeisessä isLost-vinkissä oli esimerkki eräästä muuttujan roolista (luku 2.6) eli yleisestä muuttujan käyttötavasta, josta ei ole tässä materiaalissa aiemmin mainittu.

Ohjelmoijat puhuvat melko usein lipuista (flag). Lipuksi kutsutaan tallennettua tietoa, jota käytetään ilmoittamaan jostakin asiaintilasta: "lippu nostetaan salkoon".

Yllä ehdotettiin, että käytät isLost-metodissa Boolean-tyyppistä muuttujaa tietyllä tavalla. Tämä muuttuja on lippu, joka "nostetaan" (eli saa arvon true) merkkinä siitä, että ötökkää koskettava este on löydetty. Tarkemmin sanoen kyseessä on yksisuuntainen lippu (one-way flag): kun sen arvo on kerran vaihdettu, se ei enää muutu.

Lipulla on vain kaksi mahdollista arvoa, minkä vuoksi lippumuuttujat ovat usein Boolean-tyyppisiä.

Tehokkuudesta huolestuneille

Äskeisessä isLost-metodissa homma on periaatteessa selvä heti, kun yksikin ötökkää koskettava este löytyy. Muita esteitä ei silloin edes tarvitsisi käydä läpi. Ehdotetulla tavalla toteutettu metodi kuitenkin käy aina läpi koko estevektorin.

Nythän noita esteitä oli vektorissa vain kolme, joten koneelta ei mene kauan käydä vektori läpi loppuun asti. Mutta mitä jos kävisimme samalla idealla läpi jotakin valtavaa kokoelmaa selvittääksemme, löytyykö sieltä vähintään yksi tietyn ehdon toteuttava alkio? Voisi toivoa, että koneen saisi lopettamaan heti, kun tulos on selvillä.

Asia järjestyy kyllä; konstit on monet. Palaamme aiheeseen mm. luvuissa 7.1 ja 7.2 työkalupakkimme painavoituessa.

Sisäkkäiset silmukat

Silmukoita voi laittaa sisäkkäin. Tällöin sisempi silmukka suoritetaan jokaisella ulomman silmukan "kierroksella".

Sisempi kaksikierroksinen silmukka tulee siis suoritetuksi useita kertoja, ulompi kolmikierroksinen silmukka vain kerran. Sisin println-käsky suorittuu yhteensä kuusi kertaa.

Silmukanlukemistehtävä

Mieti, mitä tapahtuu, kun seuraava ohjelmakoodi (joka ei tee mitään varsinaisesti hyödyllistä) suoritetaan. Mitä arvoja kukin muuttuja saa eri vaiheissa? Mikä merkkijono lopulta muodostuu tulosmuuttujaan?

val sanoja = Vector("funktio", "metodi", "proseduuri", "aliohjelma", "rutiini")
var tulos = ""
for sana <- sanoja do
  if sana.length < 10 then
    var laskuri = 0
    for kirjain <- sana do
      if laskuri % 2 == 0 then
        tulos += kirjain
      end if
      laskuri += 1
    end for
  end if
end for
println(tulos)

Montako kertaa alustetaan laskurimuuttuja nollaksi?

Montako kertaa tarkastetaan, onko laskurimuuttujan arvo parillinen?

Mikä on suurin arvo, jonka laskurimuuttuja saavuttaa?

Minkä kirjainrimpsun viimeinen rivi tulostaa?

Pieni ohjelmointitehtävä

Tee viimeinenkin o1.looptest-pakkauksen pikkutehtävä: task7.scala.

Muista taas testata ja käyttää debuggeria tarvittaessa.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Käyttöalue eli skooppi

Tässä vaiheessa kurssia on tullut vastaan jo esimerkkiä sisäkkäisistä koodirakenteista. Tarkastellaan siis nyt, miten sisäkkäisyys vaikuttaa esimerkiksi muuttujien määrittelyihin.

Kullakin ohjelman pääosalla — muuttujalla, metodilla, luokalla, yksittäisoliolla — on käyttöalue (eli näkyvyysalue eli skooppi; scope). Käyttöalue on se osa ohjelmakoodia, josta kyseiseen osaan pääsee käsiksi. Käyttöalue riippuu yhteydestä, jossa kyseinen piirre on määritelty, minkä lisäksi sitä voi säädellä näkyvyysmääreillä kuten private.

Yritys käyttää muuttujaa tai metodia sen käyttöalueen ulkopuolelta aiheuttaa käännösaikaisen virheilmoituksen.

Tarkastellaan ensin kertauksenomaisesti luokan välittömien osien käyttöaluetta ja sitten paikallisten muuttujien käyttöaluetta.

Luokan osien käyttöalue

Sinun ei tarvitse ymmärtää, mitä seuraava luokka tekee (joskin se on opetetun perusteella ymmärrettävissä). Luokka on tässä vain esimerkkinä sisäkkäisyydestä ja käyttöalueista, ja tutkimme sitä pintapuolisesti:

class Joulukuusi(puunKorkeus: Int):
  val korkeus = puunKorkeus.min(1000)
  private val leveinKohta = this.leveys(this.korkeus - 1)

  override def toString =
    var kuva = this.rivi('*', 1)
    for riviNro <- 1 until this.korkeus do
      kuva += this.rivi('^', this.leveys(riviNro))
    kuva += this.rivi('|', (this.leveinKohta - 4).min(3).max(1))
    kuva

  private def leveys(rivi: Int) = rivi / 2 * 2 + 1

  private def rivi(merkki: Char, montako: Int) =
    val tyhjaaVasemmalle = (this.leveinKohta - montako) / 2
    val kokoRivi = " " * tyhjaaVasemmalle + merkki.toString * montako + "\n"
    kokoRivi

end Joulukuusi

Julkisen ilmentymämuuttujan kuten korkeus käyttöalueeseen sisältyy koko luokka. Lisäksi siihen voi viitata luokan ulkopuoleltakin, kunhan käytössä on kyseisen luokan ilmentymä: olio.korkeus. Samoin julkista metodia kuten toString voi käyttää mistä tahansa päin luokkaa tai sen ulkopuolelta. Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritetä.

Yksityisen ilmentymämuuttujan kuten leveinKohta ja yksityisen metodin kuten leveys tai rivi käyttöalue on koko kyseinen luokka (mahdollisine kumppaniolioineen; luku 5.3).

Luokka itse on julkinen, joten sitä voi käyttää muualta ohjelmasta vapaasti. (Yksityisiäkin luokkia on mahdollista määritellä, mutta sitä emme tee tällä kurssilla.)

Paikallisten muuttujien käyttöalue

class Joulukuusi(puunKorkeus: Int):
  val korkeus = puunKorkeus.min(1000)
  private val leveinKohta = this.leveys(this.korkeus - 1)

  override def toString =
    var kuva = this.rivi('*', 1)
    for riviNro <- 1 until this.korkeus do
      kuva += this.rivi('^', this.leveys(riviNro))
    kuva += this.rivi('|', (this.leveinKohta - 4).min(3).max(1))
    kuva

  private def leveys(rivi: Int) = rivi / 2 * 2 + 1

  private def rivi(merkki: Char, montako: Int) =
    val tyhjaaVasemmalle = (this.leveinKohta - montako) / 2
    val kokoRivi = " " * tyhjaaVasemmalle + merkki.toString * montako + "\n"
    kokoRivi

end Joulukuusi

Metodien sisäisiin algoritmitoteutuksiin ei pääse käsiksi mistään kyseisen metodin ulkopuolelta.

Kun siirrät hiiren kursorin seuraavien laatikoiden päälle, korostuvat mainittujen muuttujien käyttöalueet.

Parametrimuuttuja kuten merkki tai montako on määritelty koko kyseisen metodin ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.

Metodin koodissa uloimmalla tasolla määritelty paikallinen muuttuja, kuten tyhjaaVasemmalle, on käytettävissä määrittelykohdasta alkaen metodin koodin loppuun.

Sama pätee muuttujalle kokoRivi.

Kun ulompi käsky sisältää muuttujamäärittelyn, niin määritelty muuttuja on käytettävissä vain kyseisen ulomman käskyn sisällä. Esimerkiksi tässä muuttuja riviNro on määritelty vain for-silmukan sisällä.

Toinen esimerkki

Tässä vielä yksi (sinänsä täysin hyödytön) esimerkkikoodi, joka korostaa paikallisten muuttujien käyttöalueita:

def metodi(parametri: Int) =
  println("Terve")
  var ulko = parametri * 2
  val joku = 1
  for keski <- 1 to 3 do
    println("Moi")
    val joku = 10
    ulko += keski * 2 + joku
    if keski < ulko + parametri then
       val sisa = readLine("Anna luku: ").toInt
       ulko += sisa + joku
    end if
  end for
  ulko + joku

parametrin käyttöalue on koko metodi.

ulkon käyttöalue on koko metodi sen määrittelystä alkaen.

keski toimii vain silmukan sisällä.

sisa-muuttuja on käytettävissä vain if-käskyn sisällä.

Entä joku? Niitä on kaksi, mikä on sallittua sisäkkäisissä rakenteissa:

Sisempi joku-muuttuja on käytettävissä silmukan sisällä sen määrittelykäskyn jälkeen.

Ulompi joku on periaatteessa käytettävissä koko metodissa sen määrittelystä alkaen. Mutta: sisempi joku peittää (shadows) ulomman samannimisen muuttujan ja käytännössä poistaa for-silmukan sen käyttöalueesta. Niinpä...

... osa joku-nimistä viittaa sisempään muuttujaan...

... ja yksi ulompaan.

Esimerkistä on pääteltävissä yleinen periaate: käyttöalue rajoittuu sisimpään lohkoon (block), jossa muuttuja on määritelty. Epävirallisesti voimme sanoa, että lohko tarkoittaa ohjelman osaa, jossa on sisällä käskyjä, esimerkiksi for-silmukan runkoa tai if-käskyn haaraa. Lohkot voivat olla sisäkkäisiä; sisennykset auttavat hahmottamaan lohkorakenteen.

Käyttöalueen valitseminen muuttujalle

Nyrkkisääntö: Valitse mahdollisimman pieni toimiva käyttöalue.

Jos et keksi hyvää perustelua tehdä muuttujasta ilmentymämuuttujaa, tee siitä paikallinen muuttuja. Sijoita paikalliset muuttujat sisimpään mahdolliseen lohkoon.

Käyttöalueen rajaaminen usein parantaa ohjelman luettavuutta ja muokattavuutta. Kun käyttöalue on kapea, on selvää, missä osissa koodia muuttuja on merkityksellinen eikä synny turhia riippuvuuksia koodin eri osien välillä. Pieni käyttöalue joskus myös vähentää turhaa muistinkäyttöä ja voi kasvattaa suoritusnopeuttakin.

Liiallinen ilmentymämuuttujien käyttö on tyypillinen aloittelevan olio-ohjelmoijan virhe. Tutkitaan nyt siksi käyttöalueiden näkökulmasta vielä yhtä esimerkkiä, luvusta 3.5 tuttua VendingMachine-limuautomaattiluokkaa. Siinä valinnat ilmentymä- ja paikallisten muuttujien välillä on tehty järkevästi:

class VendingMachine(var bottlePrice: Int, private var bottleCount: Int):
  private var earnedCash = 0
  private var insertedCash = 0

  def sellBottle() =
    if this.isSoldOut || !this.enoughMoneyInserted then
      None
    else
      this.earnedCash = this.earnedCash + this.bottlePrice
      this.bottleCount = this.bottleCount - 1
      val changeGiven = this.insertedCash - this.bottlePrice
      this.insertedCash = 0
      Some(changeGiven)

  def addBottles(newBottles: Int) =
    this.bottleCount = this.bottleCount + newBottles

Limuautomaattiin päätyneen rahan määrä on automaatin tilaa kuvaava arvo, automaattiolion ominaisuus. Tästä arvosta tulee pitää kirjaa silloinkin, kun ei olla suorittamassa mitään automaattiluokan metodia. Näinpä se on syytä määritellä ilmentymämuuttujiksi. Sama pätee muihinkin tämän luokan ilmentymämuuttujiin.

Myyntimetodissa tarvitaan väliaikaisesti muuttujaa vaihtorahasumman tallentamiseen ennen sen palauttamista. Kyseessä on yksittäistä metodia suoritettaessa käytetty tilapäissäilö, välitulos, joka ei kuvaa mitään pysyvämpää limuautomaatin tilasta eikä ole muiden metodien kannalta kiinnostava. Niinpä on järkevää käyttää paikallista muuttujaa.

Metodien parametriarvot ovat usein (kuten tässäkin) kiinnostavia vain paikallisesti.

Määrittele siis ilmentymämuuttujiksi vain muuttujat, joille pätee ainakin jokin seuraavista.

  • Muuttuja selvästi kuvaa oliolle kuuluvaa tietoa (henkilön nimi, opiskelijan kurssit).

  • Muuttuja tallentaa arvon, jonka täytyy pysyä tallessa silloinkin, kun ei olla suorittamassa mitään luokassa määriteltyä metodia.

  • Muuttuja liittyy johonkin resurssioptimointiin (jollaisia emme tavoittele tällä kurssilla).

Yhteenvetoa

  • for-silmukalla voi käydä vektorien ja puskurien alkioiden lisäksi läpi myös esimerkiksi lukuja ja merkkijonojen merkkejä.

  • Silmukoita voi laittaa sisäkkäin, jolloin sisempi silmukka tulee suoritetuksi jokaisella ulomman silmukan "kierroksella".

  • Mm. muuttujilla ja metodeilla on käyttöalue eli rajattu osa ohjelmasta, josta käsin niitä voi käyttää.

    • Käyttöalue kannattaa yleensä valita mahdollisimman pieneksi.

    • Paikallisten muuttujien käyttöalueeseen vaikuttaa metodin lohkorakenne.

  • Lukuun liittyviä termejä sanastosivulla: silmukka, for-silmukka, iteraatio; kokoelma, merkkijono; käyttöalue, lohko; yksisuuntainen lippu; debuggeri.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

a drop of ink
Palautusta lähetetään...