Programiranje

Glejte moč parametričnega polimorfizma

Recimo, da želite v Javi implementirati razredni seznam. Začnete z abstraktnim predavanjem, Seznamin dva podrazreda, Prazno in Slabosti, ki predstavljajo prazne in neprazne sezname. Ker nameravate razširiti funkcionalnost teh seznamov, oblikujete a ListVisitor vmesnik in zagotoviti sprejme (...) kljuke za ListVisitors v vsakem od vaših podrazredov. Poleg tega vaš Slabosti razred ima dve polji, najprej in počitek, z ustreznimi metodami za dostop.

Kakšne bodo vrste teh polj? Jasno je, počitek mora biti tipa Seznam. Če vnaprej veste, da bodo vaši seznami vedno vsebovali elemente določenega razreda, bo naloga kodiranja v tem trenutku precej lažja. Če veste, da bodo vsi elementi seznama celo številos, na primer, lahko dodelite najprej biti tipa celo število.

Če pa, kot se pogosto dogaja, teh informacij ne poznate vnaprej, se morate strinjati z najmanj pogostim nadrazredom, ki vsebuje vse možne elemente na vaših seznamih, ki je običajno univerzalni referenčni tip Predmet. Zato ima vaša koda za sezname različnih tipov elementov naslednjo obliko:

abstraktni razred List {public abstract Object accept (ListVisitor that); } vmesnik ListVisitor {public Object _case (izpraznite); public Object _case (proti temu); } class Empty razširja List {public Object accept (ListVisitor that) {return that._case (this); }} razred Cons razširja seznam {najprej zasebni objekt; zasebni seznam počitek; Proti (Object _first, Seznam _rest) {first = _first; počitek = _počitek; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Čeprav programerji Java na ta način pogosto uporabljajo najmanj pogost superrazred za polje, ima pristop svoje slabosti. Recimo, da ustvarite ListVisitor ki doda vse elemente seznama Celo številos in vrne rezultat, kot je prikazano spodaj:

razred AddVisitor izvaja ListVisitor {zasebno celo število nič = novo celo število (0); public Object _case (Empty that) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (to)). intValue ()); }} 

Upoštevajte izrecne odlitke v Celo število v drugem _Ovitek(...) metoda. Večkrat izvajate preizkuse izvajanja, da preverite lastnosti podatkov; v idealnem primeru bi prevajalnik te preizkuse izvedel kot del preverjanja vrste programa. Ker pa vam tega ni zagotovljeno AddVisitor velja samo za Seznams od Celo številos, preveritelj vrste Java ne more potrditi, da dodajate dva Celo številos, razen če so zasedbe prisotne.

Potencialno lahko dobite natančnejše preverjanje tipa, vendar le z žrtvovanjem polimorfizma in podvajanjem kode. Lahko bi na primer ustvarili posebno Seznam razred (z ustreznim Slabosti in Prazno podrazredi, pa tudi posebni Obiskovalec vmesnik) za vsak razred elementov, ki jih shranite v Seznam. V zgornjem primeru bi ustvarili datoteko IntegerList razred, katerega elementi so vsi Celo številos. Če pa bi radi shranili, recimo, Logičnona drugem mestu v programu bi morali ustvariti datoteko BooleanList razred.

Jasno je, da bi se velikost programa, napisanega s to tehniko, hitro povečala. Obstajajo tudi dodatna stilska vprašanja; eno bistvenih načel dobrega programskega inženirstva je imeti enotno kontrolno točko za vsak funkcionalni element programa in podvajanje kode na ta način kopiraj in prilepi krši to načelo. To pogosto vodi do visokih stroškov razvoja in vzdrževanja programske opreme. Če želite ugotoviti, zakaj, razmislite, kaj se zgodi, ko se najde napaka: programer bi se moral vrniti nazaj in to napako popraviti ločeno v vsaki narejeni kopiji. Če programer pozabi identificirati vsa podvojena spletna mesta, bo uvedena nova napaka!

Kot je prikazano v zgornjem primeru, boste težko hkrati obdržali eno samo kontrolno točko in uporabili statične preverjalnike tipa, da zagotovite, da določene napake ne bodo nikoli prišle med izvajanjem programa. V Javi, kakršna obstaja danes, vam pogosto ne preostane drugega, kot da podvojite kodo, če želite natančno preverjanje statičnega tipa. Seveda tega vidika Jave nikoli ne bi mogli popolnoma odpraviti. Nekateri postulati teorije avtomatov, dovedeni do njihovega logičnega zaključka, pomenijo, da noben sistem zvočnega tipa ne more natančno določiti nabora veljavnih vhodov (ali izhodov) za vse metode v programu. Posledično mora vsak sistem tipa najti ravnovesje med svojo preprostostjo in ekspresivnostjo nastalega jezika; sistem tipa Java se nekoliko preveč nagiba v smeri preprostosti. V prvem primeru bi vam nekoliko bolj izrazit sistem tipov omogočil natančno preverjanje tipa, ne da bi morali podvajati kodo.

Tak izrazni sistem bi dodal generične vrste jeziku. Splošni tipi so spremenljivke tipov, ki jih je mogoče ustvariti z ustreznim tipom za vsak primerek razreda. Za namene tega članka bom v kotnih oklepajih nad definicijami razreda ali vmesnika navedel spremenljivke tipa. Obseg spremenljivke tipa bo nato sestavljen iz telesa definicije, v kateri je bila deklarirana (brez podaljša klavzula). V tem obsegu lahko spremenljivko tipa uporabite kjer koli, kjer lahko uporabite običajni tip.

Na primer, pri generičnih vrstah lahko prepišete svoj Seznam razred, kot sledi:

abstraktni razred List {public abstract T accept (ListVisitor that); } vmesnik ListVisitor {javni T _case (izpraznite); javni T _case (v prid); } class Empty razširja List {public T accept (ListVisitor that) {return that._case (this); }} class Cons razširja seznam {first T first; zasebni seznam počitek; Proti (T _prvi, seznam _rest) {first = _first; počitek = _počitek; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Zdaj lahko prepišete AddVisitor izkoristiti generične vrste:

razred AddVisitor izvaja ListVisitor {zasebno celo število nič = novo celo število (0); public Integer _case (Empty that) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Upoštevajte, da izrecna predvaja Celo število niso več potrebni. Argument to do drugega _Ovitek(...) metoda razglašena za Slabosti, instanciranje spremenljivke tipa za Slabosti razred s Celo število. Zato lahko statični tip za preverjanje tipa to dokaže da.prvi () bo tipa Celo število in to that.rest () bo tipa Seznam. Podobne primerke bi naredili vsakič, ko bi nov primerek Prazno ali Slabosti je razglašena.

V zgornjem primeru bi lahko spremenljivke tipa ustvarili s katero koli Predmet. Lahko navedete tudi natančnejšo zgornjo mejo spremenljivke tipa. V takih primerih lahko to vez določite na točki deklaracije spremenljivke tipa z naslednjo sintakso:

  podaljša 

Na primer, če ste želeli svoj Seznams, da vsebuje samo Primerljivo predmete, lahko svoje tri razrede definirate na naslednji način:

class class {...} class Cons {...} class Empty {...} 

Čeprav bi dodajanje parametriziranih tipov v Javo prineslo zgoraj prikazane prednosti, se to ne bi splačalo, če bi v tem procesu žrtvovali združljivost s staro kodo. Na srečo takšna žrtev ni potrebna. Kodo, napisano v razširitvi Java, ki ima generične tipe, je mogoče samodejno prevesti v bajtno kodo za obstoječi JVM. To že počne več prevajalcev - še posebej dobri primeri so prevajalci Pizza in GJ, ki jih je napisal Martin Odersky. Pizza je bil eksperimentalni jezik, ki je Javi dodal več novih funkcij, nekatere pa so bile vključene v Javo 1.2; GJ je naslednik Pice, ki dodaja samo generične vrste. Ker je to edina dodana funkcija, lahko prevajalnik GJ ustvari bajtno kodo, ki nemoteno deluje s staro kodo. Izvira v bajt kodo s pomočjo brisanje tipa, ki nadomešča vsak primerek spremenljivke vsakega tipa z zgornjo mejo te spremenljivke. Omogoča tudi deklariranje spremenljivk tipa za določene metode in ne za celotne razrede. GJ uporablja isto sintakso za generične tipe, ki jo uporabljam v tem članku.

Delo v teku

Na univerzi Rice tehnološka skupina za programske jezike, v kateri delam, izvaja prevajalnik za navzgor združljivo različico GJ, imenovano NextGen. Jezik NextGen sta skupaj razvila profesor Robert Cartwright iz oddelka za računalništvo Rice in Guy Steele iz Sun Microsystems; GJ doda zmožnost izvajanja preverjanj izvajalnih spremenljivk med izvajanjem.

Na MIT so razvili še eno potencialno rešitev tega problema, imenovano PolyJ. Podaljšujejo ga na Cornellu. PolyJ uporablja nekoliko drugačno sintakso kot GJ / NextGen. Nekoliko se razlikuje tudi pri uporabi generičnih vrst. Na primer, ne podpira parametriranja tipov posameznih metod in trenutno ne podpira notranjih razredov. Toda za razliko od GJ ali NextGen omogoča, da se tipske spremenljivke ustvarijo s primitivnimi tipi. Podobno kot NextGen tudi PolyJ podpira runtime operacije na generičnih vrstah.

Sun je izdal zahtevo za specifikacijo Java (JSR) za dodajanje generičnih vrst v jezik. Ni presenetljivo, da je eden ključnih ciljev, ki je naveden za vsako oddajo, vzdrževanje združljivosti z obstoječimi knjižnicami razredov. Ko se v Javo dodajo splošni tipi, bo verjetno eden od zgoraj obravnavanih predlogov služil kot prototip.

Nekateri programerji kljub prednostim nasprotujejo dodajanju generičnih vrst v kakršni koli obliki. Skliceval se bom na dva pogosta argumenta takšnih nasprotnikov, kot sta argument "predloge so zlobne" in argument "ni objektno usmerjeno", in se lotil vsakega po vrsti.

So predloge zlobne?

C ++ uporablja predloge zagotoviti obliko generičnih vrst. Predloge so si nekateri razvijalci C ++ prislužile slab ugled, ker njihove definicije niso tipa preverjene v parametrizirani obliki. Namesto tega se koda podvoji pri vsakem primerku in vsako podvajanje se preveri ločeno. Težava tega pristopa je v tem, da lahko v prvotni kodi obstajajo napake tipa, ki se ne prikažejo v nobeni od začetnih instanc. Te napake se lahko pokažejo pozneje, če revizije ali razširitve programa uvedejo nove instancacije. Predstavljajte si razočaranje razvijalca, ki uporablja obstoječe razrede, ki preverjajo tipe, ko jih sestavijo sami, vendar ne potem, ko doda nov, popolnoma legitimen podrazred! Še huje, če predloga ne bo znova sestavljena skupaj z novimi razredi, takšne napake ne bodo zaznane, temveč bodo pokvarile izvršilni program.

Zaradi teh težav se nekateri ljudje namrstijo, ko vrnejo predloge nazaj, in pričakujejo, da se bodo pomanjkljivosti predlog v jeziku C ++ nanašale na sistem splošnega tipa v Javi. Ta analogija je zavajajoča, ker se pomenski osnovi Java in C ++ močno razlikujeta. C ++ je nevaren jezik, v katerem je preverjanje statičnega tipa hevristični postopek brez matematične osnove. V nasprotju s tem je Java varen jezik, v katerem preverjevalnik statičnega tipa dobesedno dokazuje, da se določene napake ne morejo pojaviti med izvajanjem kode. Posledično imajo programi C ++, ki vključujejo predloge, nešteto varnostnih težav, ki se v Javi ne morejo pojaviti.

Poleg tega vsi vidnejši predlogi za generično Javo izvajajo eksplicitno statično preverjanje tipa parametriziranih razredov, namesto da bi to storili ob vsakem primerku razreda. Če vas skrbi, da bi takšno izrecno preverjanje upočasnilo preverjanje tipa, bodite prepričani, da je pravzaprav ravno obratno: ker tip za preverjanje opravi samo en prehod nad parametrizirano kodo, v nasprotju s prehodom za vsak primerek parametriziranih tipov, se postopek preverjanja tipa pospeši. Iz teh razlogov številni ugovori predlogom C ++ ne veljajo za predloge generičnega tipa za Javo. Če pogledate dlje od tistega, kar se pogosto uporablja v industriji, obstaja veliko manj priljubljenih, a zelo dobro zasnovanih jezikov, kot sta Objective Caml in Eiffel, ki v veliko podporo podpirajo parametrizirane tipe.

So sistemi splošnega tipa objektno usmerjeni?

Nenazadnje nekateri programerji ugovarjajo kateremu koli generičnemu tipu z utemeljitvijo, da ker so bili takšni sistemi prvotno razviti za funkcionalne jezike, niso objektno usmerjeni. Ta ugovor je lažen. Kot kažejo zgornji primeri in razprave, se generični tipi zelo naravno prilegajo objektno usmerjenemu okviru. Sumim pa, da ta ugovor temelji na nerazumevanju, kako integrirati generične tipe z dednim polimorfizmom Java. Dejansko je takšna integracija mogoča in je osnova za našo implementacijo NextGen.

$config[zx-auto] not found$config[zx-overlay] not found