Programmering

Se kraften til parametrisk polymorfisme

Anta at du vil implementere en listeklasse i Java. Du starter med en abstrakt klasse, Liste, og to underklasser, Tømme og Ulemper, som representerer henholdsvis tomme og ikke-frie lister. Siden du planlegger å utvide funksjonaliteten til disse listene, designer du en ListVisitor grensesnitt, og gi aksepterer(...) kroker for ListVisitors i hver av underklassene dine. Videre din Ulemper klassen har to felt, først og hvile, med tilsvarende tilgangsmetoder.

Hva vil typene av disse feltene være? Helt klart, hvile skal være av typen Liste. Hvis du på forhånd vet at listene dine alltid vil inneholde elementer fra en gitt klasse, vil oppgaven med koding bli betydelig lettere på dette punktet. Hvis du vet at listeelementene dine alle vil være heltalls, for eksempel, kan du tilordne først å være av typen heltall.

Men hvis du ikke, som ofte er tilfelle, ikke kjenner til denne informasjonen på forhånd, må du nøye deg med den minst vanlige superklassen som har alle mulige elementer i listene dine, som vanligvis er den universelle referansetypen. Gjenstand. Derfor har koden din for lister med forskjellige elementelementer følgende skjema:

abstrakt klasseliste {offentlig abstrakt Objekt aksepter (ListVisitor det); } grensesnitt ListVisitor {public Object _case (Tøm det); public Object _case (Cons that); } klasse Empty extends List {public Object accept (ListVisitor that) {return that._case (this); }} class Cons utvider List {private Object først; privat liste resten; Ulemper (Object _first, List _rest) {first = _first; hvile = _rest; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Selv om Java-programmerere ofte bruker den minst vanlige superklassen for et felt på denne måten, har tilnærmingen sine ulemper. Anta at du oppretter en ListVisitor som legger til alle elementene i en liste over Heltalls og returnerer resultatet, som illustrert nedenfor:

klasse AddVisitor implementerer ListVisitor {private Integer zero = new Integer (0); public Object _case (Empty that) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (dette)). intValue ()); }} 

Legg merke til de eksplisitte rollebesetningene til Heltall i det andre _sak(...) metode. Du utfører gjentatte ganger kjøretidstester for å kontrollere egenskapene til dataene; ideelt sett bør kompilatoren utføre disse testene for deg som en del av programtypekontroll. Men siden du ikke er garantert det AddVisitor blir bare brukt på Listes av Heltalls, kan Java-type kontrolløren ikke bekrefte at du faktisk legger til to Heltalls med mindre rollebesetningene er til stede.

Du kan potensielt oppnå mer presis typekontroll, men bare ved å ofre polymorfisme og duplisere kode. Du kan for eksempel lage en spesiell Liste klasse (med tilsvarende Ulemper og Tømme underklasser, samt en spesiell Besøkende grensesnitt) for hver klasse element du lagrer i en Liste. I eksemplet ovenfor vil du opprette en Heltalliste klasse hvis elementer er alle Heltalls. Men hvis du ville lagre, si, Boolsks et annet sted i programmet, må du opprette en Boolsk liste klasse.

Det er klart at størrelsen på et program skrevet med denne teknikken vil øke raskt. Det er også flere stilistiske spørsmål; et av de viktigste prinsippene for god programvareteknikk er å ha ett enkelt kontrollpunkt for hvert funksjonelle element i programmet, og duplisering av kode på denne måten å kopiere og lime bryter med dette prinsippet. Dette fører ofte til høye programvareutvikling og vedlikeholdskostnader. For å se hvorfor, bør du vurdere hva som skjer når en feil blir funnet: programmereren må gå tilbake og rette den feilen separat i hver kopi. Hvis programmereren glemmer å identifisere alle dupliserte nettsteder, vil en ny feil bli introdusert!

Men som eksemplet ovenfor illustrerer, vil du finne det vanskelig å samtidig beholde et enkelt kontrollpunkt og bruke statiske type brikker for å garantere at visse feil aldri vil oppstå når programmet kjøres. I Java, slik den eksisterer i dag, har du ofte ikke annet valg enn å duplisere kode hvis du vil ha presis statisk typekontroll. For å være sikker, kan du aldri eliminere dette aspektet av Java helt. Enkelte postulater av automatteori, tatt til deres logiske konklusjon, innebærer at ingen lydtypesystemer kan bestemme nøyaktig settet med gyldige innganger (eller utganger) for alle metodene i et program. Derfor må alle typer systemer finne en balanse mellom sin egen enkelhet og uttrykksevnen til det resulterende språket; Java-typesystemet lener seg litt for mye i retning av enkelhet. I det første eksemplet ville et litt mer uttrykksfullt typesystem la deg opprettholde presis typekontroll uten å måtte duplisere koden.

Et slikt uttrykksfullt system ville tilføyet generiske typer til språket. Generiske typer er typevariabler som kan instantieres med en passende spesifikk type for hver forekomst av en klasse. I forbindelse med denne artikkelen vil jeg erklære typevariabler i vinkelparenteser over klasse- eller grensesnittdefinisjoner. Omfanget av en typevariabel vil da bestå av hoveddelen av definisjonen som den ble erklært for (ikke inkludert strekker klausul). Innenfor dette omfanget kan du bruke typevariabelen hvor som helst du kan bruke en vanlig type.

For eksempel, med generiske typer, kan du skrive om Liste klasse som følger:

abstrakt klasseliste {offentlig abstrakt T godta (ListVisitor at); } grensesnitt ListVisitor {public T _case (Tøm det); public T _case (Cons that); } klasse Empty extends List {public T accept (ListVisitor that) {return that._case (this); }} klasse Ulemper utvider Liste {privat T først; privat liste resten; Ulemper (T _forst, List _rest) {first = _first; hvile = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Nå kan du skrive om AddVisitor for å dra nytte av generiske typer:

klasse AddVisitor implementerer ListVisitor {private Integer zero = new Integer (0); public Integer _case (Empty that) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Legg merke til at de eksplisitte kastene til Heltall er ikke lenger nødvendig. Argumentet at til det andre _sak(...) metoden er erklært å være Ulemper, instantere typevariabelen for Ulemper klasse med Heltall. Derfor kan den statiske typen kontrolløren bevise det that.first () vil være av typen Heltall og det that.rest () vil være av typen Liste. Lignende instantiations ville bli gjort hver gang en ny forekomst av Tømme eller Ulemper blir erklært.

I eksemplet ovenfor kan typevariablene instantieres med hvilken som helst Gjenstand. Du kan også gi en mer spesifikk øvre grense til en typevariabel. I slike tilfeller kan du spesifisere denne avgrensningen ved typevariabelens erklæringspunkt med følgende syntaks:

  strekker 

For eksempel hvis du vil ha din Listes bare å inneholde Sammenlignelig objekter, kan du definere de tre klassene dine som følger:

klasseliste {...} klassefall {...} klasse tom {...} 

Selv om det å legge parametrerte typer til Java til å gi deg fordelene som er vist ovenfor, ville det ikke være verdt å gjøre det hvis det betydde å ofre kompatibilitet med eldre kode i prosessen. Heldigvis er ikke et slikt offer nødvendig. Det er mulig å automatisk oversette kode, skrevet i en utvidelse av Java som har generiske typer, til bytekode for eksisterende JVM. Flere kompilatorer gjør allerede dette - Pizza- og GJ-kompilatorene, skrevet av Martin Odersky, er spesielt gode eksempler. Pizza var et eksperimentelt språk som tilførte flere nye funksjoner til Java, hvorav noen ble innlemmet i Java 1.2; GJ er en etterfølger til Pizza som bare legger til generiske typer. Siden dette er den eneste funksjonen som er lagt til, kan GJ-kompilatoren produsere bytekode som fungerer jevnt med eldre kode. Den kompilerer kilde til bytekode ved hjelp av sletting, som erstatter hver forekomst av hver type variabel med den variabelens øvre grense. Det lar også typevariabler deklareres for spesifikke metoder, snarere enn for hele klasser. GJ bruker den samme syntaksen for generiske typer som jeg bruker i denne artikkelen.

Arbeid pågår

Ved Rice University implementerer programmeringsspråk teknologigruppen der jeg jobber en kompilator for en oppoverkompatibel versjon av GJ, kalt NextGen. NextGen-språket ble utviklet i fellesskap av professor Robert Cartwright fra Rices informatikkavdeling og Guy Steele fra Sun Microsystems; det legger til muligheten til å utføre kjøretidskontroller av typevariabler til GJ.

En annen potensiell løsning på dette problemet, kalt PolyJ, ble utviklet på MIT. Den utvides på Cornell. PolyJ bruker en litt annen syntaks enn GJ / NextGen. Det skiller seg også litt ut i bruken av generiske typer. For eksempel støtter den ikke typeparameterisering av individuelle metoder, og støtter foreløpig ikke indre klasser. Men i motsetning til GJ eller NextGen, tillater det typevariabler å bli instantiert med primitive typer. I likhet med NextGen støtter PolyJ kjøretidsoperasjoner på generiske typer.

Sun har gitt ut en Java Specification Request (JSR) for å legge til generiske typer til språket. Ikke overraskende er et av nøkkelmålene som er oppført for innlevering, å opprettholde kompatibilitet med eksisterende klassebiblioteker. Når generiske typer legges til Java, er det sannsynlig at et av forslagene diskutert ovenfor vil tjene som prototype.

Det er noen programmerere som er imot å legge til generiske typer i alle former, til tross for fordelene. Jeg vil referere til to vanlige argumenter for slike motstandere som "malene er onde" -argumentet og "det er ikke objektorientert" -argumentet, og ta opp hver av dem etter tur.

Er maler onde?

C ++ bruker maler å gi en form for generiske typer. Maler har oppnådd et dårlig rykte blant noen C ++ - utviklere fordi deres definisjoner ikke er typekontrollert i parameterisert form. I stedet blir koden replikert ved hver instantiering, og hver replikering er typekontrollert separat. Problemet med denne tilnærmingen er at det kan forekomme typefeil i den opprinnelige koden som ikke vises i noen av de første instantieringene. Disse feilene kan manifestere seg senere hvis programrevisjoner eller utvidelser introduserer nye instantiations. Se for deg frustrasjonen til en utvikler som bruker eksisterende klasser som skriver sjekk når de er samlet av seg selv, men ikke etter at han legger til en ny, helt legitim underklasse! Enda verre, hvis malen ikke kompileres sammen sammen med de nye klassene, vil slike feil ikke bli oppdaget, men vil i stedet ødelegge kjøringsprogrammet.

På grunn av disse problemene, rynker noen mennesker på å bringe maler tilbake, og forventer at ulempene med maler i C ++ vil gjelde for et generisk system i Java. Denne analogien er misvisende, fordi de semantiske grunnlagene til Java og C ++ er radikalt forskjellige. C ++ er et usikkert språk der statisk typekontroll er en heuristisk prosess uten matematisk grunnlag. I motsetning til dette er Java et trygt språk der den statiske typen kontrolløren bokstavelig talt viser at visse feil ikke kan oppstå når koden kjøres. Som et resultat lider C ++ - programmer som involverer maler, utallige sikkerhetsproblemer som ikke kan oppstå i Java.

Videre utfører alle de fremtredende forslagene til en generisk Java eksplisitt statisk typekontroll av de parametriserte klassene, i stedet for bare å gjøre det ved hver instantiering av klassen. Hvis du er bekymret for at en slik eksplisitt kontroll vil redusere typekontrollen, kan du være trygg på at det faktisk er motsatt: siden typekontrollen bare gir ett pass over den parametriserte koden, i motsetning til et pass for hver instantiering av parametrerte typer, blir typekontrollprosessen fremskyndet. Av disse grunner gjelder ikke de mange innvendinger mot C ++ - maler for generiske forslag til Java. Faktisk, hvis du ser utover det som er mye brukt i bransjen, er det mange mindre populære, men veldig godt utformede språk, som Objective Caml og Eiffel, som støtter parametriserte typer til stor fordel.

Er generiske typesystemer objektorientert?

Til slutt motsetter noen programmerere seg mot et hvilket som helst system av generisk type med den begrunnelsen at fordi slike systemer opprinnelig ble utviklet for funksjonelle språk, er de ikke objektorientert. Denne innsigelsen er falsk. Generiske typer passer veldig naturlig inn i et objektorientert rammeverk, som eksemplene og diskusjonen ovenfor viser. Men jeg mistenker at denne innvendingen er forankret i mangel på forståelse av hvordan man kan integrere generiske typer med Javas arvspolymorfisme. En slik integrering er faktisk mulig, og er grunnlaget for implementeringen av NextGen.

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