Ohjelmoinnin peruskurssien laaja oppimäärä Luento 11: Olioiden toteuttaminen, abstraktit luokat yms. Riku Saikkonen (merkityt ei-laajan kurssin kalvot: Otto Seppälä) 27. 1. 2011
Sisältö 1 Kertausta Scheme-tulkista (kalvot luennoilta 7 ja 8) 2 Olioiden toteuttamisesta (edelliseltä luennolta) 3 Oliot Scheme-tulkkiin 4 Abstraktit luokat, final, instanceof, tyyppipakotus
Mitä kirjan tulkki tukee? kirjan kohtien 4.14.1.6 tulkki tukee Schemestä: proseduureja, muuttujia, argumentteja (yhtä monta kuin alla oleva), rekursiota, häntärekursiota, jne. erikoismuotoja quote, set!, define, if, lambda, begin ja cond (ei condin erikoisempia syntakseja) primitiivejä car, cdr, cons, null?, +, -, *, /, =, display, newline (uusia on helppo lisätä primitive-procedures-listaan) yleisimmin käytetyistä Schemen erikoismuodoista puuttuu esim. let, and ja or (harjoitustehtävinä) myös tulkin virheidenkäsittely on melko yksinkertaista kohdassa 4.1.7 tehdään tästä tulkista hieman optimoidumpi versio, joka analysoi suoritettavan lausekkeen rakennetta etukäteen (ei käsitellä kurssilla)
Vielä abstraktimpi eval 1/3 (SICP 4.1.1) Koko eval ja siitä irrotettu apply nelilaskin9.scm (define (eval exp env) (cond ((self-evaluating? exp) exp) ((variable? exp) (lookup-variable-value exp env)) ((assignment? exp) (eval-assignment exp env)) ((definition? exp) (eval-definition exp env)) ((if? exp) (eval-if exp env)) ((lambda? exp) (make-procedure (lambda-body exp))) ((begin? exp) (eval-sequence (begin-actions exp) env)) ((application? exp) (apply (eval (operator exp) env) ; ei Schemen apply! (list-of-values (operands exp) env) env)) (else (error "Unknown expression type -- EVAL" exp)))) (define apply-in-underlying-scheme apply) ; Schemen apply talteen (define (apply procedure arguments env) ; uusi määritelmä applylle (cond ((primitive-procedure? procedure) (apply-primitive-procedure procedure arguments)) ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) env)) (else (error "Unknown procedure type -- APPLY" procedure))))
Ympäristöt kirjan tulkeissa (SICP 4.1.3) ympäristö on toteutettu listana kehyksiä sisimmästä uloimpaan; kehys on (cons lista muuttujien nimistä lista arvoista ) kirjassa määritellään proseduurit ympäristöjen käsittelyyn: lookup-variable-value, set-variable-value! ja define-variable! (extend-environment vars vals base-env ) laajentaa (ei muuta) base-env iä uudella kehyksellä ja kehysten käsittelyyn mm: (add-binding-to-frame! var val frame ) lisää kehykseen uuden muuttujan nämä vain käsittelevät ympäristö-tietorakennetta: (list (cons '(a b) '(3 4)) ; sisin kehys (cons '(x y) '(1 2)) ; toinen kehys (cons '(*...) '(<primitiivi>...))) ; globaali ympäristö joka voitaisiin tehdä tähän tapaan: (extend-environment (list 'a 'b) (list 3 4) (extend-environment (list 'x 'y) (list 1 2) the-global-environment))
Ympäristöt evaliin ja applyyn (SICP 4.1.1) Muutokset evalissa ja applyssä m-eval.scm (define (eval exp env) (cond... ((lambda? exp) (make-procedure (lambda-parameters exp) (lambda-body exp) env))... ((application? exp) (apply (eval (operator exp) env) (list-of-values (operands exp) env) ;; ei enää env-argumenttia applylle ))... )) (define apply-in-underlying-scheme apply) (define (apply procedure arguments) ; ei env-argumenttia (cond ((primitive-procedure? procedure) (apply-primitive-procedure procedure arguments)) ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) (extend-environment (procedure-parameters procedure) arguments (procedure-environment procedure)))) (else (error "Unknown procedure type -- APPLY" procedure))))
Kirjan tulkin ajoesimerkki: (fact 0) (define (fact n) (if (= n 0) 1 (* n (fact (- n 1))))) ;;; M-Eval input: (fact 0) eval (fact 0) ; evalin haara ((application? exp) (apply...)) eval fact ; siinä kutsuttu list-of-values aiheuttaa tämän eval 0 ; ja tämän apply <procedure fact> 0 ; tästä (eval-sequence (procedure-body...)...) eval (if (= n 0) 1 (* n (fact (- n 1)))) eval (= n 0) ; (if (true? (eval (if-predicate exp) env))...) eval = ; taas application?-haara, joka tulee (= n 0):sta eval n eval 0 apply <primitive => 0 0 eval 1 ;;; M-Eval value: 1 ; (eval (if-consequent exp) env) ; ja 1 on self-evaluating?
Sisältö 1 Kertausta Scheme-tulkista (kalvot luennoilta 7 ja 8) 2 Olioiden toteuttamisesta (edelliseltä luennolta) 3 Oliot Scheme-tulkkiin 4 Abstraktit luokat, final, instanceof, tyyppipakotus
Dynaaminen vs. staattinen metodikutsu kutsuttava metodi valitaan siis dynaamisesti eli ajon aikana (dynamic dispatch, dynamic method lookup) eli ajon aikana joudutaan tekemään hiukan ylimääräistä työtä proseduurikutsuun verrattuna (nykyään merkityksetöntä) pohjimmiltaan juuri tämä ominaisuus määrittelee, mikä on olio-ohjelmointikieli tarkemmin: kieli tukee olio-ohjelmointia, jos se tarjoaa mahdollisuuden tehdä dynaamista metodinvalintaa toinen vaihtoehto olisi valita metodi olion käännösaikaisen tyypin mukaan eli staattisesti monet moduulijärjestelmät toimivat periaatteessa näin (esim. ML) koska metodi on tiedossa käännösaikana, tämän voisi toteuttaa esikäsittelemällä koodia (automaattisesti tai käsin) mm. C++-kielessä on molemmat vaihtoehdot
Mitä oliosta on tallessa muistissa? ajon aikana luokat ja metodit ovat tiedossa (käännösaikana luotuja vakioita) sen sijaan kenttien arvot ovat jokaiselle oliolle yksilöllisiä ajon aikana jokaisesta oliosta on tallessa: kenttien arvot viittaus luokkaa ja metodeita kuvaaviin vakioihin mikä sitten on metodi? proseduuri, joka saa ylimääräisenä implisiittisenä argumenttina olion, jonka kautta metodia kutsuttiin (tästä tulee this) Python-kielessä tämä argumentti näkyy koodissa eksplisiittisesti metodista on siis muistissa vain koodi vrt. proseduuri Scheme-tulkissa: koodi ja määrittely-ympäristö (sisäkkäisten metodien tukemiseen tarvitsisi myös määrittely-ympäristöä)
Sisältö 1 Kertausta Scheme-tulkista (kalvot luennoilta 7 ja 8) 2 Olioiden toteuttamisesta (edelliseltä luennolta) 3 Oliot Scheme-tulkkiin 4 Abstraktit luokat, final, instanceof, tyyppipakotus
Tee se itse: oliokieli! lisätään SICP-kirjan metasirkulaariseen Scheme-tulkkiin yksinkertainen oliojärjestelmä tavoitteen saada selville: millaista uutta syntaksia olioihin tarvitaan? kuinka paljon koodia olioiden toteuttaminen vaatii? miten oliot käytännössä toimivat (esim. miten metodikutsu toteutetaan)? järjestelmä tukee: luokkia: kenttiä ja metodeita perintää ja dynaamista metodinvalintaa (ei moniperintää) oliot rakennetaan Schemen päälle eli olioita voi käyttää sekaisin muun koodin kanssa kaikkea ei tehdä niin hienosti kuin voisi (mm. ei proseduurien sisäisiä luokkia) kalvoissa vain osa toteutuksesta (mm. syntaksin ja tietorakenteiden käsittelyssä on melko paljon yksinkertaisia yksityiskohtia)
Oliojärjestelmämme syntaksi Koodiesimerkki tässä tulkattavasta koodista (define-class point object (define-field x 0) (define-field y 0) (define-method (dist point) (define (square x) (* x x)) (sqrt (+ (square (- (this get-x) (point get-x))) (square (- (this get-x) (point get-x)))))) (define-method (print) (display "Point at ") (display (this get-x)) (display ", ") (display (this get-y)) (newline))) (define-class colored-point point (define-field color 'black) (define-method (print) (display "Colored point at ") (display (this get-x)) (display ", ") (display (this get-y)) (display " colored ") (display (this get-color)) (newline))) (define p1 (new point 5 8)) (p1 set-x! 5) (p1 set-y! 8) (define p2 (new colored-point 2 3 "blue")) (p2 set-x! 2) (p2 set-y! 3) (p2 set-color! "blue") (p1 print) (p2 print) (p1 dist p2) (p2 dist p1)
Olioiden toteutus 1/5 Muutokset tulkkiin: tulkin tietorakenteita (define (make-method body parameters environment) (list 'method body parameters environment)) (define (method-body method) (cadr method)) ; jne. m-eval-object (define (make-class name parent method-alist field-names field-value-exps) (list 'class name parent method-alist field-names field-value-exps)) (define (class-name class) (cadr class)) ; jne. (define (make-object class field-values) (cons 'object (cons class field-values))) (define (object? val) (tagged-list? val 'object)) (define (object-class object) (cadr object)) (define (object-field-value-get object field-index) (list-ref (cddr object) field-index)) (define (object-field-value-set! object field-index value) (set-car! (list-tail (cddr object) field-index) value))
Olioiden toteutus 2/5 Muutokset tulkkiin: globaali luokkalista m-eval-object (define (make-initial-class-list) (list (list 'object (make-class 'object #f '() '() '())))) (define the-class-list (make-initial-class-list)) (define (find-class class-name) (let ((result (assq class-name the-class-list))) (if result (cadr result) (error "Unknown class name -- FIND-CLASS" class-name)))) (define (add-to-class-list! class-name class) (set! the-class-list (cons (list class-name class) the-class-list))) globaalin listan sijaan luokat voisi periaatteessa myös yhdistää jollain tavalla ympäristöihin
Olioiden toteutus 3/5 Muutokset tulkkiin: uusi eval m-eval-object (define (eval exp env) (cond... ((define-class? exp) (eval-class-definition exp env)) ((new? exp) (eval-new (new-class-name exp) (list-of-values (new-arguments exp) env) env)) ((application? exp) (let ((evaled-op (eval (car exp) env))) (if (object? evaled-op) ; onko tämä metodikutsu? (apply-method evaled-op (cadr exp) ; metodin nimi (list-of-values (cddr exp) env)) (apply evaled-op (list-of-values (cdr exp) env)))))... ))
Olioiden toteutus 4/5 Muutokset tulkkiin: uuden olion tekeminen m-eval-object (define (eval-new class-name constructor-arguments env) (let* ((class (find-class class-name)) (field-values (list-of-values (class-field-value-exps class) env)) (obj (make-object class field-values))) ;; käyttäjän määrittelemää konstruktoria pitäisi kutsua tässä ;; (ei vielä toteutettu) obj))
Olioiden toteutus 5/5 Muutokset tulkkiin: metodikutsu (define (apply-method object method-name arguments) (let ((method (find-method object method-name))) (eval-sequence (method-body method) (extend-environment (cons 'this (method-parameters method)) (cons object arguments) (method-environment method))))) m-eval-object (define (find-method object method-name) (define (find-in-class class) (assq method-name (class-method-alist class))) (define (find-in-parents class) (and class (or (find-in-class class) (find-in-parents (class-parent class))))) (let ((result (find-in-parents (object-class object)))) (if result (cadr result) (error "Unknown method name -- FIND-METHOD" method-name))))
Mihin pääsimme? yksinkertaisten olioiden toteuttaminen oli melko helppoa enimmäkseen uuden syntaksin ja luokkatietorakenteen käsittelyä tosin koodia tuli yhteensä aika paljon järjestelmämme muistuttaa Javan ja Pythonin (dynaamisesti tyypitetty) oliojärjestelmiä yksinkertaistettuna jotta se järjestelmä olisi kokonainen, siihen pitäisi lisätä ainakin omat konstruktorit ja super (melko yksinkertaisia toteuttaa) muuta mahdollista lisättävää: moniperintä (voisi kokeilla eri toteutusvaihtoehtoja) abstraktit luokat instanceof, final yms. yksittäisiä ominaisuuksia näkyvyydet (vähän työläämpää)
Sisältö 1 Kertausta Scheme-tulkista (kalvot luennoilta 7 ja 8) 2 Olioiden toteuttamisesta (edelliseltä luennolta) 3 Oliot Scheme-tulkkiin 4 Abstraktit luokat, final, instanceof, tyyppipakotus
Abstrakti luokka Abstrakti luokka on osittainen luokan toteutus Edellisen esimerkin LibraryItem on malliesimerkki luokasta jonka olioita ei koskaan ohjelmassa ole tarkoitus syntyä. Aliluokan olioita sensijaan syntyy paljonkin. Tällainen luokka määritellään yleensä abstraktina luokkana Luokan määrittely abstraktiksi estää olioiden luomisen Luokassa voidaan toteuttaa aliluokille yhteisiä osia Luokassa voidaan vaatia aliluokkia toteuttamaan osia 02:10 (ei-laajan kurssin kalvo)
Abstrakti luokka Abstrakti luokka on luokkamäärittely jossa osan metodeista toteutus on siirretty aliluokkien tehtäväksi Toteutuksettomat metodit ovat ns. Abstrakteja metodeja public abstract void destroyproperly(); Tälläisen metodin toteutus voi olla erilainen kaikissa aliluokissa, mutta se on välttämätön olla olemassa Jos metodia ei toteuta aliluokassa, täytyy aliluokankin olla abstrakti Abstraktilla metodilla ei ole abstraktissa luokassa toteutusta, joten metodin puumerkin jälkeen tulee vain puolipiste. 02:10 (ei-laajan kurssin kalvo)
Abstrakti luokka Abstrakti Metodi Abstraktin metodin tehtävä on kuvata jokin toiminto, joka aliluokilla väistämättä on, mutta jolla ei itse abstraktin luokan tasolla ole järkevää toteutusta Esim abstraktissa luokassa Kuvio voi olla abstrakti metodi piirrä() Kuitenkin vasta aliluokissa Soikio ja Suorakulmio metodille on olemassa mahdolliset toteutukset Koska metodi on luokassa Kuvio, voidaan kuvioille kuitenkin kutsua metodia piirrä. Lopullinen toiminta riippuu aliluokasta. 02:10 (ei-laajan kurssin kalvo)
Abstrakti luokka Instantiointi Abstraktista luokasta ei voi muodostaa olioita Jos voisi, ja tällaisen vajaan olion abstraktia metodia kutsuttaisiin ei tiedettäisi mitä tehdä koska toteutukset ovat vasta aliluokissa Konstruktorit Abstraktilla luokalla voi olla (ja on) kuitenkin konstruktoreita. Näiden tehtävä on alustaa abstraktin luokan kuvaama osa olion tilasta. Esim kirjastoesimerkin teoksen ISBN, joka oli kaikille luokille yhteinen. Abstrakti luokka aliluokkana Abstrakti luokka voi hyvin olla ei-abstraktin luokan aliluokka. Kaikki abstraktit luokat ovat mm. ei-abstraktin luokan Object aliluokkia. 02:10 (ei-laajan kurssin kalvo)
Abstrakti luokka (esimerkki) LibraryItem abstraktina luokkana Luokka AudioItem ei toteuta LibaryItemin metodia destroyproperly, joten tämänkin luokan täytyy olla abstrakti. Luokka Magazine toteuttaa abstraktit metodit, joten sen ei tarvitse olla abstrakti public abstract class LibraryItem { public String getisbn(){... } public String[] getauthors(){... } public abstract void destroyproperly(); } public abstract class AudioItem extends LibraryItem { public int getplaytime(){... } } public class Magazine extends LibraryItem { public String getissn(){... } public void destroyproperly() { // Burn the magazine } } 02:10 (ei-laajan kurssin kalvo)
final final-määreellä voidaan haluttaessa estää aliluokkien määrittäminen tai metodien korvaaminen aliluokissa. public final boolean validatepassword() Tässä final estää korvaamasta metodia validatepassword() aliluokassa public final class Esimerkki { Tässä final estää kirjoittamasta luokalle Esimerkki aliluokkia. Tällä voidaan saavuttaa etuja tehokkuudessa ja varmistaa joissain tilanteissa ettei jonkin koodin toimintaa mm. voi muuttaa luomalla uusia aliluokkia. Final-määreen käyttö voi rajoittaa jatkokehitystä, joten turhaan sitä ei kannata viljellä Muuttujien yhteydessä final ei liity perintään: public final int luku. 02:10 Tällöin final määrää että muuttujaan voi tallentaa arvon vain kerran. (ei-laajan kurssin kalvo)
InstanceOf instanceof-operaattori Joskus tulee tilanteita, jolloin ohjelman täytyy selvittää yliluokan muuttujaan tallennetun olion tyyppi Tämän voi tehdä käyttämällä instanceof-operattoria Syntaksiesimerkki if (olio instanceof String) {... Tämä on suhteellisen harvinainen tilanne, joten jos se tulee koodatessa eteen, pohdi ensin hoituisiko asia jotenkin polymorfisen kutsun kautta tms. 02:10 (ei-laajan kurssin kalvo)
Onko instanceof olio-ohjelmointia? joskus kuulee: instanceof on huonoa tyyliä, älä käytä sitä! mitä tässä on takana? instanceof ei ole puhdasta olio-ohjelmointia: sen sijaan että olion tyyppiä kysyy koodissa, pitäisi asia hoitaa perinnällä siis aliluokkiin metodi, joka tekee sen, mitä instanceof-tyypin perusteella yritettiin tehdä instanceofin ongelma on, että uutta aliluokkaa tehdessä instanceofia käyttävää koodia voi joutua muokkaamaan aina perinnän käyttö ei kuitenkaan ole järkevää eräs yleinen käyttö instanceofille on, että yritetään pitää tietty toiminto yhtenäisessä kohdassa koodia puhtaaseen oliotyyliin kuuluisi, että kunkin toiminnon koodi jaettaisiin osiin käsiteltävän asian (luokan) mukaan jolloin uuden käsiteltävän asian voi lisätä tekemällä aliluokan mutta uutta toimintoa lisätessä pitää muuttaa kaikkia luokkia tai tehdä metodi, jonka instanceofin avulla tekee koko toiminnon
Tyyppipakotus (Casting) Tyyppipakotus ja perintä Jos viittaus olioon on tallennettu yliluokan tyyppiseen muuttujaan ja halutaan kutsua jonkin aliluokan metodia täytyy viittaus tyyppipakottaa tuon aliluokan tyyppiin. Spesifi ilmaisu on downcast Kerrotaan kääntäjälle, että yliluokan muuttujan osoittama olio on todella aliluokan olio. Kun tämä menee pieleen saadaan poikkeus: ClassCastException 02:10 (ei-laajan kurssin kalvo)
Mitä tyyppipakotus tekee? tyyppipakotus muuttaa käännösaikaista tyyppiä olio sinänsä ei muutu (eikä se, mitä korvattuja metodeja kutsutaan) tyyppipakotusta käytetään, kun kääntäjä ei osaa päätellä olion todellista tyyppiä tyyppipakotus tuottaa ajon aikaisen tyyppitarkistuksen käytännössä tyyppipakotusta tarvitaan melko harvoin lähinnä instanceofin käytön yhteydessä ja toisia olioita sisältävissä säilytysolioissa, jotka eivät käytä genericsejä (tästä myöhemmin)