Ohjelmointi

Katso parametrisen polymorfismin voima

Oletetaan, että haluat toteuttaa luetteloluokan Java-sovelluksessa. Aloitat abstraktilla luokalla, Listaja kaksi alaluokkaa, Tyhjä ja Haittoja, jotka edustavat tyhjiä ja tyhjiä luetteloita. Koska aiot laajentaa näiden luetteloiden toimintoja, suunnittelet a ListVisitor käyttöliittymä ja tarjota hyväksyä(...) koukut ListVisitors jokaisessa alaluokassasi. Lisäksi sinun Haittoja luokassa on kaksi kenttää, ensimmäinen ja levätä, vastaavilla lisämenetelmillä.

Mitkä ovat näiden kenttien tyypit? Selvästi, levätä pitäisi olla tyyppiä Lista. Jos tiedät etukäteen, että luettelosi sisältävät aina tietyn luokan elementtejä, koodaustehtävä on tässä vaiheessa huomattavasti helpompaa. Jos tiedät, että luetteloelementit ovat kaikki kokonaislukuVoit esimerkiksi määrittää s ensimmäinen olla tyypiltään kokonaisluku.

Jos et kuitenkaan tiedä näitä tietoja etukäteen, kuten usein tapahtuu, sinun on tyydyttävä vähiten yleiseen superluokkaan, joka sisältää kaikki mahdolliset elementit luetteloissasi, joka on tyypillisesti yleinen viitetyyppi Esine. Eri tyyppisten elementtien luetteloiden koodilla on siis seuraava muoto:

abstrakti luokka List {public abstract Object accept (ListVisitor that); } käyttöliittymä ListVisitor {public Object _case (Tyhjennä se); public Object _case (Miinukset); } luokka Tyhjä laajentaa luetteloa {public Object accept (ListVisitor that) {return that._case (this); }} luokka Haitat laajentaa ensin luetteloa {yksityinen objekti; yksityinen Luettelon lepo; Miinukset (Object _first, List _rest) {first = _first; lepo = _ levätä; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Vaikka Java-ohjelmoijat käyttävät tällä alalla kentällä usein vähiten yleistä superluokkaa, lähestymistavalla on haittoja. Oletetaan, että luot a ListVisitor joka lisää kaikki luettelon elementit Kokonaislukus ja palauttaa tuloksen alla olevan kuvan mukaisesti:

luokka AddVisitor toteuttaa ListVisitor {yksityinen kokonaisluku nolla = uusi kokonaisluku (0); public Object _case (Tyhjennä se) {return zero;} public Object _case (Cons that) {return new Integer ((((Integer) that.first ()). intValue () + ((Integer) that.rest (). (tämä)). intValue ()); }} 

Huomaa nimenomaiset näyttelijät Kokonaisluku toisessa _case (...) menetelmä. Suoritat toistuvasti ajonaikaisia ​​testejä tietojen ominaisuuksien tarkistamiseksi. ihannetapauksessa kääntäjän tulisi suorittaa nämä testit puolestasi osana ohjelmatyyppitarkistusta. Mutta koska sinulle ei taata sitä AddVisitor sovelletaan vain Listas Kokonaislukus, Java-tyypin tarkistaja ei voi vahvistaa, että olet itse asiassa lisäämässä kahta Kokonaislukus ellei näyttelijöitä ole läsnä.

Voit mahdollisesti saada tarkemman tyyppitarkistuksen, mutta vain uhraamalla polymorfismi ja kopioimalla koodi. Voisit esimerkiksi luoda erityisen Lista luokka (vastaavalla Haittoja ja Tyhjä alaluokat sekä erityinen Vierailija käyttöliittymä) jokaiselle elementtiluokalle, jonka tallennat a Lista. Yllä olevassa esimerkissä luodaan Kokonaislista luokka, jonka kaikki elementit ovat Kokonaislukus. Mutta jos haluat tallentaa BoolenJos olet jossakin muussa ohjelmassa, sinun on luotava Totuuslista luokassa.

Tällä tekniikalla kirjoitetun ohjelman koko kasvaa selvästi. On myös muita tyylillisiä kysymyksiä; yksi hyvän ohjelmistotuotannon keskeisistä periaatteista on, että jokaisella ohjelman toiminnallisella osalla on yksi ohjauspiste, ja koodin kopiointi tällä kopioi ja liitä-tavalla rikkoo tätä periaatetta. Näin tekeminen johtaa korkeisiin ohjelmistokehitys- ja ylläpitokustannuksiin. Mieti, mitä tapahtuu, kun virhe löytyy: ohjelmoijan on palattava takaisin ja korjattava virhe erikseen jokaisessa tehdyssä kopiossa. Jos ohjelmoija unohtaa tunnistaa kaikki päällekkäiset sivustot, otetaan käyttöön uusi vika!

Mutta kuten yllä oleva esimerkki havainnollistaa, sinun on vaikea pitää samanaikaisesti yhtä ohjauspistettä ja käyttää staattisen tyyppisiä tarkistimia taatakseen, että tiettyjä virheitä ei koskaan tapahdu ohjelman suorituksen aikana. Javassa, kuten se on nykyään, sinulla ei usein ole muuta vaihtoehtoa kuin kopioida koodi, jos haluat tarkan staattisen tyypin tarkistuksen. Et voi koskaan poistaa tätä Java-ominaisuutta kokonaan. Tietyt automaatiteorian postulaatit loogiseen johtopäätökseensä tarkoittavat, että mikään äänityyppijärjestelmä ei pysty määrittämään tarkasti kelvollisten tulojen (tai lähtöjen) joukkoa kaikille ohjelman menetelmille. Tämän vuoksi jokaisen tyyppijärjestelmän on löydettävä tasapaino oman yksinkertaisuutensa ja tuloksena olevan kielen ilmaisevuuden välillä; Java-tyyppinen järjestelmä nojaa hieman liikaa yksinkertaisuuden suuntaan. Ensimmäisessä esimerkissä hieman ilmeisempi tyyppijärjestelmä olisi antanut sinun ylläpitää tarkkaa tyyppitarkistusta ilman, että sinun tarvitsee kopioida koodia.

Tällainen ilmeikäs tyyppinen järjestelmä lisäisi yleiset tyypit kielelle. Yleiset tyypit ovat tyyppimuuttujia, jotka voidaan instantisoida sopivalla spesifisellä tyypillä kullekin luokan esiintymälle. Tätä artikkelia varten ilmoitan tyypin muuttujat kulmasulkeissa luokan tai rajapinnan määritelmien yläpuolella. Tyyppimuuttujan laajuus koostuu sitten sen määritelmän rungosta, jolla se on ilmoitettu (ei kuitenkaan ulottuu lauseke). Tässä laajuudessa voit käyttää tyypin muuttujaa missä tahansa, missä voit käyttää tavallista tyyppiä.

Esimerkiksi yleisillä tyypeillä voit kirjoittaa uuden Lista luokka seuraavasti:

abstrakti luokka List {public abstract T accept (ListVisitor that); } käyttöliittymä ListVisitor {public T _case (Tyhjennä se); public T _case (Miinukset siitä); } luokka Tyhjä laajentaa luetteloa {public T accept (ListVisitor that) {return that._case (this); }} luokka Haitat laajentaa ensin luetteloa {yksityinen T; yksityinen Luettelon lepo; Huonot puolet (T _ensimmäinen, Listaa_lehti) {ensin = _ensimmäinen; lepo = _ levätä; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Nyt voit kirjoittaa uudelleen AddVisitor hyödyntää yleisiä tyyppejä:

luokka AddVisitor toteuttaa ListVisitor {yksityinen kokonaisluku nolla = uusi kokonaisluku (0); public Integer _case (Empty that) {return zero;} public Integer _case (Cons that that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Huomaa, että nimenomainen heittää Kokonaisluku ei enää tarvita. Argumentti että toiseen _case (...) menetelmä ilmoitetaan olevan Haittoja, instantizing tyypin muuttuja Haittoja luokan kanssa Kokonaisluku. Siksi staattisen tyypin tarkistaja voi todistaa sen se. ensimmäinen () tulee olemaan tyyppiä Kokonaisluku ja tuo että. lepo () tulee olemaan tyyppiä Lista. Samanlaiset instantiat tekisivät aina, kun uusi Tyhjä tai Haittoja ilmoitetaan.

Yllä olevassa esimerkissä tyyppimuuttujat voidaan instantisoida millä tahansa Esine. Voit myös antaa tarkemman ylärajan tyypin muuttujalle. Tällaisissa tapauksissa voit määrittää tämän sidoksen tyypin muuttujan ilmoituskohdassa seuraavalla syntaksilla:

  ulottuu 

Esimerkiksi, jos halusit sinun Listas sisältää vain Vertailukelpoinen Voit määritellä kolme luokkaa seuraavasti:

luokan luettelo {...} luokan haitat {...} luokka Tyhjä {...} 

Vaikka parametrisoitujen tyyppien lisääminen Java-järjestelmään antaisi sinulle edellä esitetyt edut, niin tekeminen ei olisi kannattavaa, jos se tarkoittaisi yhteensopivuuden uhraamista vanhojen koodien kanssa prosessissa. Onneksi tällainen uhri ei ole välttämätön. On mahdollista kääntää koodi, joka on kirjoitettu Java-laajennukseen, jolla on yleiset tyypit, nykyisen JVM: n tavukoodiksi. Useat kääntäjät tekevät jo tämän - erityisesti Martin Oderskyn kirjoittamat Pizza- ja GJ-kääntäjät ovat erityisen hyviä esimerkkejä. Pizza oli kokeellinen kieli, joka lisäsi Javaen useita uusia ominaisuuksia, joista osa sisällytettiin Java 1.2: een; GJ on Pizzan seuraaja, joka lisää vain yleisiä tyyppejä. Koska tämä on ainoa lisätty ominaisuus, GJ-kääntäjä voi tuottaa tavukoodin, joka toimii sujuvasti vanhan koodin kanssa. Se kokoaa lähteen tavukoodiksi tyypin poisto, joka korvaa jokaisen tyypin muuttujan kaikki esiintymät muuttujan ylärajalla. Se mahdollistaa myös tyyppimuuttujien ilmoittamisen tietyille menetelmille eikä kokonaisille luokille. GJ käyttää samaa syntaksia yleistyypeille, joita käytän tässä artikkelissa.

Työn alla

Rice Universityssä ohjelmointikielien teknologiaryhmä, jossa työskentelen, toteuttaa kääntäjän ylöspäin yhteensopivalle GJ-versiolle nimeltä NextGen. NextGen-kielen ovat kehittäneet professori Robert Cartwright Ricen tietojenkäsittelytieteen osastolta ja Guy Steele Sun Microsystemsistä; se lisää kyvyn suorittaa tyypin muuttujien ajonaikaisia ​​tarkistuksia GJ: hen.

Toinen mahdollinen ratkaisu tähän ongelmaan, nimeltään PolyJ, kehitettiin MIT: ssä. Sitä jatketaan Cornellissa. PolyJ käyttää hieman erilaista syntaksia kuin GJ / NextGen. Se eroaa myös hieman yleisten tyyppien käytöstä. Se ei esimerkiksi tue yksittäisten menetelmien tyypin parametrointia eikä tällä hetkellä tue sisäisiä luokkia. Mutta toisin kuin GJ tai NextGen, se sallii tyypin muuttujien ilmentämisen primitiivisillä tyypeillä. Samoin kuin NextGen, PolyJ tukee ajonaikaisia ​​operaatioita yleistyypeissä.

Sun on julkaissut Java-määrityspyynnön (JSR) yleisten tyyppien lisäämiseksi kielelle. Ei ole yllättävää, että jokaisen lähetyksen yksi tärkeimmistä tavoitteista on yhteensopivuuden ylläpitäminen olemassa olevien luokkakirjastojen kanssa. Kun Java-tyyppiin lisätään yleisiä tyyppejä, on todennäköistä, että yksi yllä käsitellyistä ehdotuksista toimii prototyyppinä.

Jotkut ohjelmoijat vastustavat yleisten tyyppien lisäämistä missä tahansa muodossa etuistaan ​​huolimatta. Viittaan kahden tällaisten vastustajien yleiseen argumenttiin, kuten "mallipohjat ovat pahoja" -argumentti ja "se ei ole olio-suuntautunut" -argumentti, ja käsittelen niitä kaikkia vuorotellen.

Ovatko mallit pahoja?

C ++ käyttää malleja tarjota yleisten tyyppien muoto. Mallit ovat ansainneet huonoa mainetta joidenkin C ++ -kehittäjien keskuudessa, koska niiden määritelmiä ei ole tyypintarkistettu parametrisoidussa muodossa. Sen sijaan koodi replikoidaan jokaisessa ilmentymässä, ja jokainen replikointi tarkistetaan erikseen. Tämän lähestymistavan ongelmana on, että alkuperäisessä koodissa saattaa olla tyyppivirheitä, jotka eivät näy missään alkuperäisessä esimerkissä. Nämä virheet voivat ilmetä myöhemmin, jos ohjelmaversiot tai laajennukset tuovat uusia ilmentymiä. Kuvittele kehittäjän turhautumista käyttämään olemassa olevia luokkia, jotka tarkastavat itse, kun he ovat itse laatineet ne, mutta ei sen jälkeen, kun hän on lisännyt uuden, täysin laillisen alaluokan! Vielä pahempaa on, että jos mallia ei käännetä uudelleen uusien luokkien kanssa, tällaisia ​​virheitä ei havaita, vaan ne vioittavat suoritettavaa ohjelmaa.

Näiden ongelmien takia jotkut ihmiset paheksuvat tuotuaan malleja takaisin odottaen, että C ++ - mallien haittapuolet soveltuvat yleiseen Java-tyyppiseen järjestelmään. Tämä analogia on harhaanjohtava, koska Java ja C ++ -semanttinen perusta ovat radikaalisti erilaiset. C ++ on vaarallinen kieli, jossa staattinen tyypin tarkistus on heuristinen prosessi ilman matemaattista perustaa. Sen sijaan Java on turvallinen kieli, jolla staattisen tyypin tarkistaja kirjaimellisesti todistaa, että tiettyjä virheitä ei voi tapahtua koodia suoritettaessa. Tämän seurauksena C ++ -ohjelmat, joihin sisältyy malleja, kärsivät lukemattomista turvallisuusongelmista, joita ei voi esiintyä Java-ohjelmassa.

Lisäksi kaikki yleistä Java-sovellusta koskevat merkittävät ehdotukset suorittavat parametrisoitujen luokkien nimenomaisen staattisen tyyppitarkastuksen sen sijaan, että tekisivät sen vain luokan jokaisessa esimerkissä. Jos olet huolissasi siitä, että tällainen nimenomainen tarkistus hidastaa tyyppitarkistusta, voit olla varma, että tosiasiassa päinvastoin on totta: koska tyyppitarkistaja suorittaa vain yhden ohituksen parametrisoidun koodin yli, toisin kuin läpäisy kullekin parametrisoidut tyypit, tyyppitarkistusprosessi nopeutuu. Näistä syistä C ++ -malleja vastaan ​​esitetyt lukuisat vastaväitteet eivät koske Java: n yleisiä ehdotuksia. Itse asiassa, jos katsot pidemmälle kuin mitä teollisuudessa on käytetty laajasti, on olemassa monia vähemmän suosittuja, mutta erittäin hyvin suunniteltuja kieliä, kuten Objective Caml ja Eiffel, jotka tukevat parametrisoituja tyyppejä suurella edulla.

Ovatko yleistyyppiset järjestelmät olio-suuntautuneita?

Lopuksi jotkut ohjelmoijat vastustavat mitä tahansa yleistä tyyppistä järjestelmää sillä perusteella, että koska tällaiset järjestelmät kehitettiin alun perin toiminnallisille kielille, ne eivät ole olio-suuntautuneita. Tämä vastalause on väärä. Geneeriset tyypit sopivat hyvin luonnollisesti olio-orientoituun kehykseen, kuten yllä olevat esimerkit ja keskustelut osoittavat. Mutta epäilen, että tämä vastalause johtuu ymmärryksen puutteesta siitä, miten geneeriset tyypit voidaan integroida Java-perintöpolymorfismiin. Itse asiassa tällainen integraatio on mahdollista, ja se on perusta NextGen-ohjelmamme toteuttamiselle.