Luku 3.5: Limsaa, jalkapalloa ja virheitä
Esimerkki: VendingMachine
Seuraavan esimerkin teemana ovat virtuaaliset limuautomaatit. Toteutamme luokan, jollainen voisi hallinnoida yksinkertaisten kolikoilla toimivien limsanmyyntikoneiden toimintaa (leikisti; tässäkin alkeiskurssiesimerkissä on vedetty mutkat suoriksi).
Toteutamme VendingMachine
-luokan pala palalta Scaladoc-dokumenttina (luku 3.2) annetun
spesifikaation perusteella. Tässä hieman mutkikkaammassa luokassa yhdistyvät paitsi tuore
if
-asia myös moni muu aiempi teema.
Varsinaisesti uutta asiaa esimerkissä ei tule. Jos koet ymmärtäneesi tähänastiset kurssin
asiat mainiosti, etkä ole väärässä, niin voit ohittaa tämän esimerkin ja siirtyä alla
oleviin tehtäviin. (Voit myös vapaaehtoisena tehtävänä koettaa toteuttaa VendingMachine
-luokan itse ennen kuin luet ratkaisun!)
Lue NYT dokumentaatiota
Tutustu Miscellaneous-moduulin luokan o1.soda.VendingMachine
Scaladoc-dokumentaatioon.
Varmista, että ymmärrät millaisia metodeita VendingMachine
-luokassa
on ja miten niitä käytetään. Jatka vasta sitten eteenpäin tässä
luvussa.
Luokka on toteutettu valmiiksi Miscellaneous-moduuliin, joten voit myös kokeilla sen käyttöä REPLissä, jos dokumentaatiosta jää epäselväksi, mitä metodit saavat aikaan. Toteutus esitellään alla.
Limuautomaatin tila ja sen alustaminen
Katsotaan ensiksi luontiparametreja ja ilmentymämuuttujia:
class VendingMachine(var bottlePrice: Int, private var bottleCount: Int):
private var earnedCash = 0
private var insertedCash = 0
Toinen luontiparametri on pullojen määrä. Tämäkin tieto vaihtuu
pulloja lisättäessä ja ostettaessa; kokoojana toimiva
var
-muuttuja sopii tarkoitukseen. Tämä ilmentymämuuttuja on
tarkoitettu vain luokan sisäiseen käyttöön, joten se olkoon
private
(luku 3.2). Huomaa, että voit kirjoittaa näkyvyysmääreen
myös luontiparametrin yhteyteen määritellylle ilmentymämuuttujalle.
earnedCash
-muuttuja pitää kirjaa siitä, paljonko rahaa
limuautomaatissa on eli paljonko automaatti on "ansainnut"
viimeisen tyhjennyksen jälkeen. Tästä tiedosta on pidettävä
kirjaa, jotta emptyCashbox
-metodi saadaan toimimaan
spesifikaation mukaisesti. earnedCash
-muuttuja on vain
luokan sisäiseen käyttöön eli private
. Juuri luotu
automaattiolio ei ole vielä ansainnut senttiäkään.
insertedCash
-muuttuja pitää kirjaa siitä, montako senttiä
limuautomaattiin on syötetty viimeisimmän oston jälkeen.
Esimerkiksi jos on syötetty ensin 2 € ja sitten 1 €, eikä ole
vielä ostettu pulloa, niin muuttujan arvon tulee olla 300.
Tätä muuttujaa tarvitaan, jotta sellBottle
-metodi saadaan
toimimaan spesifikaation mukaisesti. Sekin on yksityinen ja
alkuarvoltaan nolla.
Helppoja limumetoditoteutuksia
Viisi metodeista ovat toteutukseltaan varsin suoraviivaisia aiempien lukujen perusteella.
def addBottles(newBottles: Int) =
this.bottleCount = this.bottleCount + newBottles
def insertMoney(amount: Int) =
this.insertedCash = this.insertedCash + amount
def isSoldOut = this.bottleCount == 0
def enoughMoneyInserted = this.insertedCash >= this.bottlePrice
def emptyCashbox() =
val got = this.earnedCash
this.earnedCash = 0
got
addBottles
- ja insertMoney
-metodit yksinkertaisesti nostavat
kokoojamuuttujien arvoja saamiensa parametriarvojen verran.
isSoldOut
- ja enoughMoneyInserted
-metodit kumpikin selvittävät
tietyn seikan olion tiedoista vertailuoperaatiolla ja palauttavat
näin saadun Boolean
-tyyppisen arvon.
emptyCashbox
-metodi nollaa automaatin ansiot ja palauttaa
nollausta edeltäneen kokonaisansiomäärän. Tässä käytetään apuna
paikallista muuttujaa tilioliosta (luku 2.2) tuttuun tapaan.
Ehkä on hyvä katsoa samaa koodia uudestaan palauttaen mieleen Scalan tyylikäytäntöjä:
def addBottles(newBottles: Int) =
this.bottleCount = this.bottleCount + newBottles
def insertMoney(amount: Int) =
this.insertedCash = this.insertedCash + amount
def isSoldOut = this.bottleCount == 0
def enoughMoneyInserted = this.insertedCash >= this.bottlePrice
def emptyCashbox() =
val got = this.earnedCash
this.earnedCash = 0
got
Kaksi ensimmäistä metodia vaikuttavat olion tilaan. Rivitämme niiden määrittelyt, vaikka ne ovatkin yksirivisiä ja lyhyitä.
Kaksi seuraavaa metodia palauttavat arvon eivätkä vaikuta olioon mitenkään. Kun ne ovat lisäksi noin lyhyitäkin, voimme jättää ne rivittämättä niin halutessamme. Nämä kaksi metodia ovat parametrittomia, emmekä kirjaa niille parametriluetteloa lainkaan.
Viides metodi on myös parametriton mutta vaikuttaa olion tilaan. Sille kirjaamme myös tyhjät kaarisulkeet parametriluettelon paikalle, vaikka metodi ei yhtään parametria otakaan.
Mainittuja käytäntöjä voit kerrata esim. kurssin tyylioppaasta.
toString
-metodin toteutus
toString
-metodissa pääsemme hyödyntämään if
–else
-lauseketta. Tässä yksi
toteutustapa:
override def toString =
"earned " + this.earnedCash / 100.0 + " euros, " +
"inserted " + this.insertedCash + " cents, " +
(if this.isSoldOut then "SOLD OUT" else s"$bottleCount bottles left")
Tämän metodin runko on vain yksi pitkä merkkijonoja yhteen liittävä lauseke. Sen voi jakaa tähän tapaan usealle riville, kunhan katkaisee rivit plussien jälkeen eikä esimerkiksi kesken merkkijonon.
Viimeinen pala palautettavasta merkkijonosta muodostetaan sen
perusteella, onko limut loppuunmyyty vai ei. Tämä if
-lauseke
toimii suuremman lausekkeen osana.
Sulkeet if
-lausekkeen ympärillä ovat tarpeen, jotta tulee
oikein määritellyksi, missä else
-haara päättyy. (Vrt.
pikkutehtävät luvun 3.4 alussa.)
Toinen toteutustapa
Alla on vertailun vuoksi toinen tapa toteuttaa tuo toString
. Tässä
käytetään paikallisia apumuuttujia ja upotetaan ne osaksi kuvausta.
override def toString =
val earnings = this.earnedCash / 100.0
val bottleStatus = if this.isSoldOut then "SOLD OUT" else s"$bottleCount bottles left"
s"earned $earnings euros, inserted $insertedCash cents, $bottleStatus"
sellBottle
-metodin toteutus
Tässä eräs toimiva toteutustapa sellBottle
-metodille, jonka tehtävänä on "myydä pullo"
eli muuttaa automaatin raha- ja pullotietoja sekä palauttaa joko vaihtorahan määrä tai
negatiivinen luku myynnin epäonnistumisen merkiksi.
def sellBottle() =
if this.isSoldOut then
-1
else if !this.enoughMoneyInserted then
-1
else
this.earnedCash = this.earnedCash + this.bottlePrice
this.bottleCount = this.bottleCount - 1
val changeGiven = this.insertedCash - this.bottlePrice
this.insertedCash = 0
changeGiven
Ensin tarkistetaan pullojen määrä isSoldOut
-metodilla.
Loppuunmyynnin tapauksessa päädytään palauttamaan -1.
Pullomäärän ollessa kunnossa jatketaan tarkastamalla, onko
automaatissa riittävästi rahaa. Huomaa else if
-ketjutus.
Muista: huutomerkki on not-operaattori, joka kääntää totuusarvon. Tässä katsotaan, onko niin, että rahat eivät riitä. Jos on, palautetaan -1.
Olion tilaa muuttavat rivit suoritetaan vain, jos molemmat tarkistukset "menivät läpi". Metodi lisää automaatin ansioita, vähentää pullon, laskee vaihtorahat, nollaa syötetyt rahat ja lopuksi palauttaa vaihtorahasumman.
Huomaa vielä: metodin paluuarvon määrää viimeiseksi suoritettava käsky. Kun koodissa on "haaroja", se ei välttämättä ole ohjelmatekstin viimeinen koodirivi. Tässä esimerkissä on kolme eri riviä, joista mikä tahansa saatetaan tilanteesta riippuen suorittaa viimeisenä.
Limuautomaattitoteutuksen laadusta
Tuo sellBottle
-toteutus kyllä toimii spesifikaation mukaisesti. Mutta se saattaa
sinustakin näyttää peräkkäisine if
-käskyineen aavistuksen verran ikävältä. Koodi ei
myöskään ole aivan "DRY" eli toisteeton (luku 1.4), koska siinä toistuu sama kohta —
-1:n palauttaminen — turhaan kahdesti.
Lisäksi voimme kyseenalaistaa, onko hyvä ajatus palauttaa negatiivinen luku merkkinä siitä, ettei pulloa saatu myytyä — vaikka negatiivisen luvun käyttö epäonnistumisen merkkinä perinteinen ohjelmointikikka onkin. Tämä nimittäin avaa mahdollisuuden huolimattomuusvirheelle, jossa metodin kutsuja ei muista tarkistaa arvon etumerkkiä ja käsittelee palautettua negatiivista lukua kuin se olisi saadun vaihtorahan määrä ja osto olisi onnistunut.
Tulet oppimaan kurssilla tekniikoita, joilla sellBottle
-metodia voi parantaa. Voit
pitää tämän metodin ja nämä laatukysymykset mielessä lukiessasi myöhempiä lukuja.
Dokumentit spesifikaatioina tällä kurssilla
Scaladoc-dokumenteilla on kurssillamme keskeinen rooli, sillä niitä käytetään monien harjoitustehtävien tehtävänannoissa kahdella tavalla:
Monissa harjoituksissa sinun on tarpeen käyttää yhtä tai useampaa luokkaa tai yksittäisoliota, jotka on laadittu kurssia varten ja joiden kuvaukset on annettu Scaladoc-muodossa. Saat siis sekä toimivan ohjelmakoodin että sen rajapinnan kuvauksen eli "käyttöohjeen" scaladocina.
Sinulle annetaan monissa harjoituksissa yhden tai useamman luokan tai yksittäisolion rajapinnan kuvaus scaladoceina, mutta ei vastaavaa ohjelmakoodia (ainakaan kokonaan). Tehtäväksesi jää kirjoittaa sellainen Scala-koodi, joka vastaa annettua dokumentaatiota. Scaladoc-dokumentit siis toimivat spesifikaationa laadittavalle ohjelmalle; ajatus on sama kuin
VendingMachine
-esimerkissä äsken.
Tehtävissä käytetyt Scaladoc-dokumentit on lähes poikkeuksetta kirjoitettu englanniksi.
Muuttujat Scaladoc-pohjaisissa tehtävissä
Kuten luku 3.2 kertoi, yksityiset muuttujat eivät kuulu luokan julkiseen rajapintaan eivätkä siksi tule mukaan Scaladoc-dokumentteihin, joiden tehtävä on kuvata luokan käyttötapaa luokan ulkopuolelta katsottuna.
Niissä kurssin tehtävissä, joissa annetaan Scaladoc-muotoinen spesifikaatio, tämä
tarkoittaa käytännössä sitä, että dokumenteissa näkyvät ilmentymämuuttujat sinunkin tulee
määritellä julkisiksi laatimaasi ohjelmakoodiin (kuten bottlePrice
äsken). Toisaalta
muita julkisia ilmentymämuuttujia kuin dokumentaation määräämät ei luokalla tulisi olla.
Ei ole epätavallista, että tarvitset luokan sisäiseen tilakirjanpitoon muita muuttujia
kuin scaladocin kuvaamat julkiset muuttujat, mutta tee noista lisämuuttujista yksityisiä
(kuten esim. bottleCount
- ja earnedCash
-muuttujista äsken).
Huomaa lisäksi, että julkisen ilmentymämuuttujan vaihtaminen val
ista var
iksi
tarkoittaa, että tuohon muuttujaan voi sijoittaa arvon luokan ulkopuolelta. Sekin siis
muuttaa julkista rajapintaa. Jos dokumentaatio pyytää val
in, tee val
. Yksityisiin
muuttujiin voit valita val´in tai `var
in, mutta val
on yleensä parempi vaihtoehto
(luku 1.4).
Pikkutehtävä
Pääseekö private
-tietoihin siis jotenkin käsiksi?
(Seuraavaa ei tarvitse osata kurssilla. Tai usein muutenkaan.)
private
-määre estää muuttujan (tai metodin) tavallisen käytön luokan
ulkopuolelta, ja sillä merkitään, ettei muuttujaa pitäisi käyttää
ulkopuolelta missään normaaliolosuhteissa. Silti kuten äskeinen
tehtäväkin antoi ymmärtää, ei private
ole aivan ehdoton este.
Tarkastellaan vaikkapa VendingMachine
-luokkaa. Yksi sen yksityisistä
muuttujista oli earnedCash
, jonka arvoa säätelivät julkiset metodit.
Tämä ei onnistu, kuten ei pidäkään:
val machine = VendingMachine(250, 10)machine: o1.soda.VendingMachine = earned 0.0 euros, inserted 0 cents, 10 bottles left machine.earnedCash = 123456-- Error: |machine.earnedCash = 123456 |^^^^^^^^^^^^^^^^^^ |variable earnedCash cannot be accessed
Mutta seuraava onnistuu.
val accessToCurrentValue = machine.getClass.getDeclaredField("earnedCash")accessToCurrentValue: java.lang.reflect.Field = private int o1.soda.VendingMachine.earnedCash accessToCurrentValue.setAccessible(true)accessToCurrentValue.set(machine, 123456)machine.emptyCashbox()res0: Int = 123456
Tässä siis vaihdoimme earnedCash
-muuttujalle mielivaltaisen arvon
ohittamalla rajapinnan pienellä kikkailulla. Tätä ei kuitenkaan tule
tehtyä vahingossa.
Tehtävä: tunnista vääriä
Nouda IntelliJ’hin moduuli Football1, joka kuvaa jalkapallo-ottelujen tuloskirjanpitoa.
Tämä moduuli sisältää kaksi luokkaa — Match
ja Club
— sekä käynnistysolion
MatchTest
.
Luokka Match
käyttää luokkaa Club
: kuhunkin otteluun liittyy kaksi ottelevaa seuraa.
MatchTest
sisältää testikoodia, jolla voi koekäyttää luokkia.
Tehtävänanto
Tutustu moduulin Scaladoc-dokumentaatioon sekä ohjelmakoodiin. (Kertaus: löydät
dokumentaation IntelliJ’stä moduulin doc
-kansiosta sekä selaimella luvun alun
linkistä. Tarkemmat ohjeet luvussa 3.2.)
Koodia tutkiessa osoittautuu, että:
Siinä on syntaksivirheitä ("kielioppivirheitä"; ks. luku 1.8), jotka estävät luokkien käytön tyystin.
Osa dokumentaation kuvaamista metodeista puuttuu. Eikä siinäkään vielä kaikki:
Koodissa on toiminnallisia bugeja. Osa
Match
-luokan metodeista on kieliopillisesti oikein muttei toimi niin kuin dokumentaatio kuvaa.
Tehtäväsi on:
korjata virheet ja täydentää puuttuvat metodit luokista
Match
jaClub
niin, että luokat vastaavat annettua spesifikaatiotakorjata
MatchTest
-testiohjelma toimivaksi eli sellaiseksi, että se käyttääMatch
-luokkaa ohjelmassa olevien kommenttien mukaisestitäydentää tätä testiohjelmaa niin, että se testaa luokkia kattavammin (parhaaksi katsomallasi tavalla)
varmistaa testiohjelman avulla, että kaikki pelaa.
Ohjeita ja vinkkejä
Virheiksi on valittu sellaisia, joita ohjelmoinnin opiskelijat joskus tekevät. Käy ne läpi ajatuksella, jotta osaat välttää tai ainakin korjata vastaavat tulevissa tehtävissä, joissa kirjoitat enemmän omaa koodia.
Alla on vaiheittainen opastus tehtävään.
Luithan varmasti Scaladoc-dokumentaation ensin? Silmäilitkö myös annettua koodia?
Vaihe 1/17: Club
-luokan ensimmäinen virhe
Paina Football1-moduulin tai jonkin sen tiedostoista ollessa valittuna F10 (tai
valikosta Build → Build Module 'Football1'). IntelliJ’n Build-välilehti
ponnahtaa esiin. Siellä näkyy luettelot virheilmoituksista, joista osa liittyy Club
iin,
osa Match
iin ja osa MatchTest
iin.
Aloitetaan Club
-luokasta. IntelliJ’n mukaan kaksi kolmesta virheestä on class
-alkuisella rivillä ja kolmas tiedoston lopussa end
-rivillä.
class
-rivillä on tosiaan virhe, ja se toistuu kahdesti samalla rivillä. IntelliJ’n
virheilmoitus ':' expected but ',' found antaa osviittaa. Sen voi vapaasti suomentaa
"Tähän piti tulla kaksoispiste, mutta vastaan tulikin (jo) pilkku."
Voit napsauttaa ensimmäistä virheilmoitusta kahdesti, niin editoriin tulee esiin juuri se rivi, johon virheilmoitus viittaa. Kokeile!
Rivillä punainen korostus osoittaa luontiparametrien val name, val stadium
välistä
pilkkua.
Miksi tuossa pitäisi olla kaksoispiste? Mitä muuta siitä vielä puuttuu? Keksinet sen itsekin. Jos et, niin vertaa tätä määrittelyä aiempin esimerkkien luokkamäärittelyihin. Korjaa virhe nyt, niin jatketaan. Niin, ja korjaa se toinenkin vastaava virhe samalla rivillä.
Korjauksen kirjoitettuasi käännä ("rakenna"; build) koodi uudestaan: F10.
Vaihe 2/17: Club
-luokan toinen virhe
Toinen Club
-virheilmoitus osoittaa luokan loppua. Tuon viimeisen rivin virhe on
edellistäkin pienempi.
Virheilmoitus kertoo: misaligned end marker
. Loppumerkki on väärin asemoitu. Korjaa
sisennys. (Koska loppumerkki on vapaaehtoinen, sen voisi myös poistaa.)
Toistaiseksi kohtaamamme virheilmoitukset ovat olleet kohtalaisen selkeitä ja osoittaneet juuri niille riveille, joissa virheet ovat. Näin ei ole aina. Monesti virheilmoitukset ovat epäselviä, ja niitä voi joutua pohtimaan pidempään tai selvittelemään vaikkapa googlitse. Harjoitus tekee mestarin.
Vaihe 3/17: Aloitetaan Match
in korjaaminen
Virheilmoitus osoittaa private var
-alkuista riviä ja protestoi: this kind of statement is
not allowed here. Se tarkoittaa suunnilleen: ei tällaista käskyä saa laittaa tähän kohtaan.
Jaa, mihin kohtaan? Katsokaamme virheen ympärille.
Hieman ylempänä, class Match
-alkuisella rivillä, näkyy lopussa alleviivaus virheen merkiksi.
Virhe on pieni ja melko ilmeinen, ja jos viet hiiren kursorin sinne kaksoispisteen päälle,
IntelliJ vielä vahvistaa: suljehan se sieltä puuttuu.
Alkuperäinen virheilmoitus viittasi siis siihen, että private
-muuttujaa ei voi kirjoittaa
mihin tahansa. Koska class
-määrittely oli pielessä, ei tuo muuttujan määrittelykään
kelvannut. Ilmoitus osoitti oikeaan suuntaan, vaikkei ollutkaan selkein mahdollinen.
Lisää puuttuva kaarisulje. Käännä koodi uudestaan F10llä. Luettelo Match
-luokan
ongelmista päivittyy.
Vaihe 4/17: goalDifference
Match
-luokasta tulee nyt virhe missing return type, jonka tuplaklikkaaminen vie
goalDifference
-metodin kohdalle.
Virheilmoitus puuttuvasta paluuarvon tyypistä on teknisesti oikein muttei erityisen selvästi kerro, mikä varsinainen ohjelmointivirhe on tehty.
Funktion rungossa olisi tarkoitus olla (Int
-tyyppinen) lauseke, joka määrää funktion
paluuarvon. Tästä funktiomäärittelystä puuttuu välimerkki, minkä vuoksi funktion runko
ei tule määritellyksi lainkaan.
Vie hiiren kursori goalDifference
-nimen päälle, niin IntelliJ näyttää lisätietoja
metodista. Siellä näkyy muun muassa, että metodin paluuarvon tyyppi olisi Unit
.
Scala ei ole siihen Unit
ia parempaakaan tyyppiä saanut pääteltyä, koska metodin
määrittelyssä on välimerkkivirhe.
Emme halua metodin palauttavan Unit
vaan Int
. Korjaukseksi riittää yhtäsuuruusmerkin
lisäys, jolloin metodi palauttaa runkona olevan lausekkeen arvon. (Voit myös muuttaa
koko metodin yksiriviseksi, jos haluat.)
Vaihe 5/17: isHigherScoringThan
Käännä koodi painamalla F10. Ongelmaluettelo päivittyy.
Match
-luokan kohdalla näkyy nyt yksi virheilmoitus punaisella ja pari varoitusta
keltaisella. Tutkitaan ensin virheilmoitusta ja palataan varoituksiin myöhemmin.
(MatchTest
hälyttää myös, ja kovasti hälyttääkin, mutta palataan myös siihen
myöhemmin.)
IntelliJ’n mukaan koodi this.totalGoals(anotherMatch)
on viallinen, ja IntelliJ
on tässä ihan oikeassa. Huomaatko miksi? Korjaa virhe ja toinen vastaava samalta
riviltä. Virheilmoitus method totalGoals in class Match does not take parameters
on osuva.
Käännä koodi uudestaan. Nyt näyttää siltä, että Match
olisi varoitusviestejä
lukuunottamatta kunnossa. Älä kuitenkaan huokaise helpotuksesta.
Vaihe 6/17: Varoitus isGoalless
-metodissa
Varoitusilmoituksista
Monet ohjelmointityökalut, IntelliJ mukaanlukien, huomauttavat joskus epäilyttävästä koodista varoituksilla (warning). IntelliJ’n Build-välilehdellä varoitukset näyttävät pitkälti samalta kuin virheilmoituksetkin; ne erottaa keltaisesta huutomerkkikolmiosta .
Varoitus tarkoittaa, että koodissa on luultavasti (muttei 100-prosenttisen varmasti) jotain, joka on syytä korjata. Työkalu sanoo ohjelmoijalle: "Ei taida kannattaa tehdä noin?"
Kun IntelliJ varoittaa jostakin, se on usein oikeassa ainakin siinä, että jotain ohjelmassa kannattaa muuttaa. Automaattinen varoitusilmoitus ei vain välttämättä suoraan tai oikein ilmaise sitä, mitä kannattaa muuttaa.
Käytännössä hyvä nyrkkisääntö on suhtautua varoitusilmoituksiin samalla vakavuudella kuin virheilmoituksiinkin.
Tutkitaan seuraavaksi varoitusta, jonka IntelliJ antaa Match
-luokasta — tarkemmin
sanoen sen isGoalless
-metodista. Varoitus sanailee, että a pure expression does
nothing in statement position ja ehdottaa vielä päälle, että ehkäpä siitä kohdasta
puuttuisi sulkeita.
Varoitukselle on aihetta: tuossa metodissa on jotain pielessä. Ilmoitus on kuitenkin harhaanjohtava ja ehdotus sulkeiden lisäämisestä huono.
Kenties jo näetkin, mikä on vikana. Tehdään silti niin, ettei vielä korjata tuota metodia, sillä on sivistävää tutkia, millaisia oireita tuo virhe aiheuttaa muualla ohjelmassa. Palaamme siihen myöhemmin. Anna nyt noiden varoitusten olla ja jatka seuraavaan vaiheeseen.
Vaihe 7/17: Kaikki pielessä MatchTest
issä
MatchTest
-olion koodista tulee virheitä solkenaan. Suurin osa niistä on muotoa illegal
start of toplevel definition, jollainen näkyy tulevan yhdestä jos toisestakin tuon olion
rivistä, jolla kutsutaan jotakin funktiota.
Nuo ilmoitukset eivät ole selkeimmästä päästä varsinaisen virheen selventäjinä eivätkä suoraan osoita korjausta vaativaa riviäkään. Ne kuitenkin antavat vihjeen siitä, miten tietokone on koodin jäsentänyt.
Virheilmoituksen voi lukea "uloimmalla tasolla koodirivi ei voi alkaa noin", missä "uloin taso" tarkoittaa koodia, joka ei ole minkään funktion, yksittäisolion tai luokan sisällä.
Ilmoitus tuntuu erikoiselta, sillä kyllähän noiden virherivien pitäisi olla yksittäisolion
MatchTest
-sisällä. Ihan oikeinhan ne on sinne kirjoitettu object MatchTest
-rivin
perään. Miksi IntelliJ sitten väittää, että tuo koodi olisi "uloimmalla tasolla" ja siksi
epäkelpoa?
Syy on jälleen kerran välimerkissä. Katso object MatchTest
-rivin loppua. Nykyisellään
käy niin, että tuo olion koko määrittely on vain yhden rivin mittainen eikä seuraavia
koodirivejä lasketa sen osiksi lainkaan (sisennyksistä huolimatta). Lisää puuttuva merkki
ja käännä uudelleen.
Vaihe 8/17: Eikö se korjaantunutkaan?
Moni katosi, mutta osa illegal start -virheistä sinne vielä jäi. Ensimmäinen niistä
osoittaa tiettyyn println
-käskyyn.
Tarkka silmäys koodiin kertoo, että tuo rivi on sisennetty väärin, kuten on seuraavakin. Ne tosiaan ovat "uloimmalla tasolla", jonne ne eivät kuulu. Korjaa.
Paina tämänkin tehtyäsi F10 ja muista sama myös edetessäsi seuraaviin vaiheisiin.
Vaihe 9/17: Otteluiden luominen
Yksi valituksista kohdistuu käskyyn Match(club2, club1)
yksittäisoliossa MatchTest
.
Mutta sehän näyttää olevan aivan kunnossa. Se on aivan kunnossa.
Tämä on hyvä esimerkki siitä, ettei virheilmoitus aina osoita siihen paikkaan, jossa
varsinainen virhe on. Virheilmoitus osoittaa nyt MatchTest
-luokan ohjelmakoodiin ja
ilmoittaa, ettei se toimi. Ja eihän se toimikaan, mutta syy on siinä, että Match
-luokka
on määritelty virheellisesti ja MatchTest
yrittää käyttää sitä tavalla, jolla käytön
pitäisi onnistua.
Huomaa, että virheilmoitus on monirivinen ja näkyy kokonaisuudessaan Build-välilehdellä virheluettelon vieressä, kun valitset kyseisen virheen luettelosta.
Tämä on ns. type mismatch -virhe. Tällainen virhe sanoo, että tiettyyn kohtaan
koodia kaivattiin erästä tyyppiä oleva arvo, mutta löytyikin jotain muuta. Tällaisista
virheilmoituksista on melko usein helppo päätellä, missä virhe on; tässäkin tapauksessa
virheilmoitus kertoo kohtalaisen selvästi, mistä on kyse. Lue se ja vertaa keskenään
virheilmoituksen osoittamaa riviä, Match
-luokan Scaladoc-dokumentaatiota ja annettua
Match
-toteutusta.
Korjaa virhe.
Vaihe 10/17: addHomeGoal
Tarkastellaan seuraavaksi virheilmoituksia, jotka kertovat, että value addHomeGoal is
not a member of o1.football1.Match. Siis "Mitään addHomeGoal
ia ei löydy Match
-luokasta."
Häh? Onhan siellä koodissa tuo metodi. Katso vaikka itse.
Katsoitko tarkkaan? Ja huomasitko virheilmoituksen kysymyksen: did you mean AddHomeGoal?
Tämänlaisen virheilmoituksen saadessasi tarkista aina oikeinkirjoitus. Kirjoitusvirhe voi olla kutsuvassa koodissa tai kutsutussa koodissa. Huomaa myös, että virheilmoitusten did you mean -ehdotukset eivät välttämättä ole oikeassa. Toteuta ohjelma spesifikaation (eli tässä Scaladocin) mukaiseksi.
Vaihe 11/17: homeCount
ja awayCount
Sitten: variable awayCount in class Match cannot be accessed ja sama homeCount
-muuttujalle.
Olemme onnekkaita, sillä ilmoitus kertoo sekä virheen todellisen sijainnin että syyn.
Se kertoo, ettei mainittuihin Match
-olion muuttujiin pääse käsiksi MatchTest
-oliosta.
Jos et jo aiemmin huomannut, miten Match
-luokassa on muuttujat homeCount
ja awayCount
sekä metodit homeGoals
ja awayGoals
, niin kiinnitä siihen huomiota nyt. Millaisesta
virheestä on kyse? Onko virhe Match
in vai MatchTest
in koodissa?
Korjaa virhe.
Vaihe 12/17: isHomeWin
ja isAwayWin
Jaaha: value isHomeWin is not a member of o1.football1.Match ja sama isAwayWin
ille.
Mutta ihan varmastihan Match
-luokassa on nyt nuo metodit! Nimetkin on oikein
kirjoitettu.
Match
-luokan koodista nuo määrittelyt toisiaan löytyvät, mutta pienen virheen vuoksi
noita metodeita ei kuitenkaan nyt ole Match
-olioilla tarjolla. Seuraava pikkuesimerkki
johdattaa virheen äärelle.
Sanotaan, että haluamme määritellä kolmimetodisen luokan:
class Luokka:
def toiminto1(luku: Int) =
println(luku)
def toiminto2 = 1 + 1
def toiminto3 = 2 + 2
end Luokka
Ehkä kuitenkin vahingossa kirjoitamme sen näin:
class Luokka:
def toiminto1(luku: Int) =
println(luku)
def toiminto2 = 1 + 1
def toiminto3 = 2 + 2
end Luokka
Tuo jälkimmäinen luokkamäärittelymme tarkoittaa ihan samaa kuin tämä seuraava, johon on lisätty loppumerkki:
class Luokka:
def toiminto1(luku: Int) =
println(luku)
def toiminto2 = 1 + 1
def toiminto3 = 2 + 2
end toiminto1
end Luokka
Nyt siis toiminto2
ja toiminto3
ovatkin sisennysten vuoksi toiminto1
-metodin sisällä.
Niistä tulee ensimmäisen metodin paikallisia funktioita eikä luokan
metodeita. (Tämä ei ollut lainkaan tarkoituksemme, mutta paikallisilla funktioilla on kyllä
paljonkin muuta käyttöä, mistä kerrotaan kurssilla myöhemmin.)
Korjaa metodit isHomeWin
ja isAwayWin
Match
-luokasta. Tarkkana sisennyksissä!
Vaihe 13/17: totalGoals
Yksi virheistä liittyy match1.totalGoals()
-metodikutsuun. Ilmoitus on tutunlainen
(hieman ylempää): method totalGoals does not take parameters. Samoin sen syy on tuttu,
vaikka tässä ei metodille tarjota varsinaisia parametreja vaan vain tyhjä parametriluettelo.
Korjaa.
Vaihe 14/17: isGoalless
kuntoon
Virhe: Found: Unit Required: Boolean. Tämä type mismatch -virhe osoittaa
isGoalless
-kutsuihin MatchTest
issä.
Selvästikin if
-käskyyn tarvitaan Boolean
-tyyppinen lauseke. Jostain syystä
isGoalless
ei näytä sellaista tuottavan, vaan saamme Unit
in.
Pystytkö selvittämään tai arvaamaan, mikä isGoalless
-metodissa on vikana? Saatko
ongelman korjattua? Saatko toteutuksesta hyvin yksinkertaisen? Joskus korjaaminen
tarkoittaa vain koodin poistamista.
Jos ongelman ymmärtäminen ei meinaa onnistua, katso alempaa Lisää virhetilanteita:
arvojen palauttaminen ja valintakäsky. Tuo kappale käsittelee juuri tällaisia
virheitä. Tee se pikkutehtäväkin. (Ja muista vielä vinkki: isGoalless
ille voi
määritellä erittäin yksinkertaisen, toimivan toteutuksen.)
Kun korjaat tämän virheen, poistuu myös aiemmin huomaamamme varoitusviesti.
Vaihe 15/17: location
Viimeiset käännösaikaiset virheet oliosta MatchTest
johtuvat jälleen Match
-luokan
vajavaisuudesta. Ilmoitus value location is not a member of o1.football1.Match kertoo
tässä, että location
-metodi on jäänyt Match
-luokasta määrittelemättä. Ilmoitus on
samanlainen kuin joistakin muistakin metodeista yllä, mutta syy on nyt erilainen:
location
-metodi tosiaan puuttuu kokonaan.
Kirjoita toteutus location
-metodille.
Vaihe 16/17: Testaa
Ohjelman pitäisi nyt olla ajokunnossa. Aja MatchTest
. Mieti, mitä sen pitäisi tulostaa
ja mitä se todellisuudessa tulostaa.
Huomaat, että osa tulosteista on kunnossa, mutta kaikki ei ole hyvin. Match
-luokka
toimii edelleen osin dokumentaation vastaisesti, vaikka IntelliJ ei luokasta enää
virheitä löydäkään.
Alla on vinkkejä puutteiden korjaamiseksi.
Vaihe 17/17: Korjaa ja täydennä
Korjaa Match
-luokkaan jääneet puutteet: yksi virheellinen metodi ja puuttuva
toString
.
Ohjeita ja vinkkejä:
Muokkaa
MatchTest
-ohjelmaa parhaaksi katsomallasi tavalla virhettä etsiessäsi.Muistutus (viimeisen kerran tästä): Avaa Scaladoceista täydet kuvaukset metodien nimiä klikkaamalla.
Jos
toString
-metodin toteuttaminen tuottaa vaikeuksia:Varmista, että luit metodin koko Scaladocin.
Varmista, että käytit
override
-sanaa alussa.Katso
VendingMachine
-luokantoString
-metodi, josta on parikin eri toteutusta yllä.Lue jo tässä välissä loput tästä luvusta. Kyse voi olla jostakin alempana esitellystä
if
-käskyihin liittyvästä virhetilanteesta.Pyydä apua, jos nuo vinkit eivät auttaneet.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lisää virhetilanteita: muuttujat ja valintakäsky
Tutkitaan luvun lopuksi lisää koodia, joka ei toimi. Seuraavat esimerkit voivat parantaa
ymmärrystäsi if
-käskyistä ja vähän muustakin. Ne voivat myös auttaa välttämään yleisiä
virheitä.
Eräs taannoinen kurssinkävijä laati harjoituksen vuoksi tällaisen min
-funktiota
muistuttavan koodinpätkän ja ihmetteli saamaansa virheilmoitusta. Moni muukin on tehnyt
vastaavan virheen.
def palautaPienempi(eka: Int, toka: Int) =
if eka < toka then
var palautetaan = eka
else
var palautetaan = toka
palautetaan
Koodi tuottaa käännösaikaisen virheilmoituksen not found:
palautetaan. Eli muuttuja palautetaan
ei ilmeisesti
olisi määritelty tuossa viimeisellä rivillä.
Mutta johan se palautetaan
-muuttuja on sinne määritelty,
peräti kahteen kertaan?
Ongelma on tämä: palautetaan
on määritelty if
-käskyn haarojen sisällä. Kukin tällainen
määrittely kattaa vain kyseisen haaran, eikä (kumpikaan) muuttuja ole käytettävissä if
-käskyn ulkopuolella.
Jos haluat käyttää muuttujaa myös if
-käskyn jälkeen, se on määriteltävä if
-käskyn
ulkopuolella. Esimerkiksi tämä toimii:
def palautaPienempi(eka: Int, toka: Int) =
val palautetaan = if eka < toka then eka else toka
palautetaan
Tähän teemaan eli muuttujien käyttöalueisiin (scope) palataan luvussa 5.6.
Äskeisessä esimerkissähän ei tosin edes ole välttämätöntä tallentaa välitulosta muuttujaan. Tässä vielä yksinkertaisempi toteutus:
def pienempi(eka: Int, toka: Int) = if eka < toka then eka else toka
Lisää virhetilanteita: arvojen palauttaminen ja valintakäsky
Miksei tämä toimi?
Sanotaan, että haluamme laatia funktion, joka palauttaa parametriksi saamansa
kokonaisluvun neliön, jos annettu luku on positiivinen. Negatiivisen parametriarvon
tapauksessa neliön sijaan palautetaankin nolla. Funktion parametrin tyyppi olisi Int
ja paluuarvon samaten.
Seuraava toteutus on aloittelijalle varsin tyypillinen. Siinä on selkeästi ilmaistu
molemmat tapaukset erillisillä if
-käskyillä:
def kokeilu(luku: Int) =
if luku > 0 then luku * luku
if luku <= 0 then 0
Onko tässä sitten jotain vikaa? Kokeillaan REPL:issä:
def kokeilu(luku: Int) = if luku > 0 then luku * luku if luku <= 0 then 0def kokeilu(luku: Int): Unit -- Potential Issue Warning: if luku <= 0 then 0 ...
Funktion määrittely on periaatteessa laillista Scalaa. REPL kuittaa, että funktio tuli määritellyksi, mutta ...
... varoittaa, että jotain tässä taitaa olla menossa pieleen.
Määritellyksi tulleen funktion paluuarvon tyyppinä näkyy
olevan Unit
, vaikka halusimme funktion palauttavan luvun.
Tutkitaan vielä. Kun koetamme kutsua funktiota, ei käy hyvin:
kokeilu(10)7 + kokeilu(10)-- Type Error: 7 + kokeilu(10) ^^^ None of the overloaded alternatives of method + in class Int ... match arguments (Unit)
Emme saa paluuarvona lukua. Ja kun yritämme käyttää funktiota osana laskutoimitusta,
saamme virheilmoituksen, jonka voi suomentaa suunnilleen "plus-operaattoria ei ole
määritelty yhdistelmälle Int
+ Unit
". Selvästi funktiomme palauttaa vain Unit
.
Mistä on kyse?
Ongelman selitys
Katsotaan koodiamme tarkasti ja muistaen, että paluuarvo on viimeiseksi evaluoitavan lausekkeen arvo.
def kokeilu(luku: Int) =
if luku > 0 then luku * luku
if luku <= 0 then 0
Kokeilufunktiossamme on kaksi if
-käskyä, jotka ovat toisistaan
täysin erilliset. Jälkimmäinen on ensimmäisen perässä. Kumpikaan
ei ole toisen else
-haarassa tai muutenkaan yhteydessä toiseen.
Ensin funktio evaluoi lausekkeen, jonka arvoksi haluttaisiin
positiivisen luvun tapauksessa tuon luvun neliö. Tällä lausekkeella
ei ole else
-haaraa, eikä se lainkaan määrittele, mitä sen arvoksi
muussa tapauksessa tulisi!
Sen jälkeen (ja täysin riippumattomasti siitä, mitä edellinen rivi teki) evaluoidaan jälkimmäinen käsky. Tässäkin käskyssä on kerrottu, minkä arvon yksi haara tuottaa (eli nollan) mutta toinen haara on jäänyt kokonaan määrittelemättä — eikä tämä toinen rivi tiedä mitään siitä, mitä edellinen teki, eikä siis osaa sulkea pois positiivisen luvun mahdollisuutta.
Jälkimmäisen rivin tuottama arvo on koko funktion paluuarvo.
Edeltävä if
-rivi oli itse asiassa ihan merkityksetön.
Koska näin (huonosti) määritellyn funktion paluuarvo voi tilanteesta riippuen olla
joko Int
tai tarkemmin määrittelemätön, Scala-työkalusto päätyy tulokseen, että koko
lausekkeen (eli funktion toisen rivin) tyyppi on vain Unit
eli "ei oikein mitään".
Lisätieto
else
-osaton käskymme if luku <= 0 then 0
on käytännössä lyhennetty
versio tällaisesta käskystä:
if luku <= 0 then
0
()
else
()
Tyhjät sulkeet tarkoittavat Unit
-arvoa.
Toimiva versio
No, miten se sitten saadaan toimimaan?
def kokeilu(luku: Int) =
if luku > 0 then luku * luku else 0
Viimeiseksi evaluoitava lauseke on nyt tämä ainoa if
-käsky,
jonka arvo Int
-tyyppinen valittiin sitten kumpi haara vain.
Entä tämä ratkaisu?
Pari käytännön vinkkiä virheenetsintään
Jos saat hämmentäviä virheilmoituksia, katso tarkasti, mitkä ovat kunkin lausekkeen ja
paluuarvon tietotyypit. Tämä kannattaa usein muutenkin mutta erityisesti silloin, jos
ilmoituksessa näkyy sana Any tai Unit ja olet käyttänyt if
-valintakäskyä.
Lisäkikka: ohjelmointityökalut osaavat joissakin tapauksissa antaa selkeämpiä virheilmoituksia, jos kirjaat funktiollesi paluuarvojen tyypin erikseen (luvun 1.8 mukaisesti):
def kokeilu(luku: Int): Int = // jne.
Tyypin saa kirjata näin vaikka kaikille funktioille. Paluuarvojen vapaaehtoinen kirjaaminen voi muutenkin selkiyttää ohjelmia ja vähentää virheitä. Monet Scala-ohjelmoijat merkitsevät paluuarvon tyypin kaikkiin julkisiin metodeihin (mitä sinun kuitenkaan ei ole pakko O1:ssä tehdä).
Yhteenvetoa
Kurssilla Scaladoc-dokumentteja käytetään usein tehtävänannoissa.
Ohjelmointityökalujen antamat virheilmoitukset ovat usein epäselviä, mutta niitä voi tulkita ja harjoittelu auttaa.
Kun valitset eri arvojen välillä (vaikutuksettomalla)
if
-lausekkeella, muista kirjoittaa loppuunelse
-haara ilman ehtolauseketta, jotta jokin arvo tulee kaikissa tapauksissa valituksi.Lukuun liittyviä termejä sanastosivulla: dokumentaatio, Scaladoc;
if
; käännösaikainen virhe, syntaksivirhe.
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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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.
Limuautomaattioliota luotaessa annetaan parametriksi pullon hinta. Automaattiolion on pidettävä kirjaa tästä tiedosta, joten määritellään myös ilmentymämuuttuja.
bottlePrice
-muuttuja on rooliltaan tuoreimman säilyttäjä; koska hintaa on tarkoitus muuttaa sijoituskäskyillä, niin otetaan se talteenvar
-muuttujaan (kuten dokumentaatiokin sanoo).