Programavimas

Štai parametrinio polimorfizmo jėga

Tarkime, kad norite įdiegti sąrašo klasę „Java“. Pradėsite nuo abstrakčios klasės, Sąrašasir du poklasiai, Tuščia ir Minusai, atitinkamai nurodantys tuščius ir ne tuščius sąrašus. Kadangi planuojate išplėsti šių sąrašų funkcionalumą, sukursite a „ListVisitor“ sąsają ir pateikti priimti(...) kabliukai „ListVisitor“s kiekviename jūsų poklasyje. Be to, jūsų Minusai klasėje yra du laukai, Pirmas ir pailsėti, su atitinkamais prieigos metodais.

Kokie bus šių laukų tipai? Aišku, pailsėti turėtų būti tipo Sąrašas. Jei iš anksto žinote, kad jūsų sąrašuose visada bus tam tikros klasės elementai, kodavimo užduotis šiuo metu bus žymiai lengviau. Jei žinote, kad visi jūsų sąrašo elementai bus sveikasis skaičiusPavyzdžiui, galite priskirti Pirmas kad būtų tipo sveikasis skaičius.

Tačiau jei, kaip dažnai būna, jūs iš anksto nežinote šios informacijos, turite tenkintis mažiausiai paplitusia superklase, kurioje yra visi įmanomi elementai jūsų sąrašuose, o tai paprastai yra universalus nuorodos tipas Objektas. Taigi jūsų įvairių tipų elementų sąrašų kodas yra tokios formos:

abstrakti klasė Sąrašas {public abstract Object accept (ListVisitor that); } sąsaja „ListVisitor“ {public Object _case (Ištuštinti tai); public Object _case (Cons that); } klasė Tuščia pratęsia sąrašą {public Object accept (ListVisitor that) {grąžinti tą._dėklas (tai); }} klasės trūkumai pirmiausia pratęsia sąrašą {privatus objektas; privatus Sąrašo poilsis; Minusai (Object _first, List_rest) {first = _pirmas; poilsis = _pailsėjimas; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {grąžinti that__case (this); }} 

Nors „Java“ programuotojai tokiu būdu laukui dažnai naudoja rečiausiai paplitusią superklasę, požiūris turi ir trūkumų. Tarkime, kad sukūrėte a „ListVisitor“ kad pridedami visi sąrašo elementai Sveikasis skaičiuss ir grąžina rezultatą, kaip parodyta žemiau:

klasė „AddVisitor“ įgyvendina „ListVisitor“ {privatus sveikasis skaičius nulis = naujas sveikasis skaičius (0); public Object _case (Ištuštinkite tai) {return zero;} public Object _case (Cons that) {return new Integer ((((Integer) that.first ()). intValue () + ((Integer) that.rest (). (tai)). intValue ()); }} 

Atkreipkite dėmesį į aiškų perdavimą į Sveikasis skaičius antroje _case (...) metodas. Jūs pakartotinai atliekate vykdymo laiko testus, kad patikrintumėte duomenų savybes; geriausia, jei kompiliatorius turėtų atlikti šiuos bandymus jums kaip programos tipo tikrinimo dalį. Bet kadangi jums tai nėra garantuota „AddVisitor“ bus taikoma tik Sąrašass Sveikasis skaičiuss, „Java“ tipo tikrintuvas negali patvirtinti, kad iš tikrųjų pridedate du Sveikasis skaičiuss nebent dalyviai.

Galėtumėte gauti tikslesnį tipo patikrinimą, bet tik aukodami polimorfizmą ir kopijuodami kodą. Pavyzdžiui, galite sukurti specialųjį Sąrašas klasė (su atitinkama Minusai ir Tuščia poklasiai, taip pat specialus Lankytojas sąsaja) kiekvienai elementų klasei, kurią saugote a Sąrašas. Aukščiau pateiktame pavyzdyje sukursite Sveikasis sąrašas klasės, kurios elementai yra visi Sveikasis skaičiuss. Bet jei norėtumėte laikyti, tarkim, BūlioKai kur kitoje programos vietoje turėtumėte sukurti „BooleanList“ klasė.

Akivaizdu, kad naudojant šią techniką parašytos programos dydis sparčiai didėtų. Yra ir kitų stilistinių klausimų; vienas iš pagrindinių geros programinės įrangos inžinerijos principų yra turėti vieną valdymo funkcinį tašką kiekvienam funkciniam programos elementui, o kodo kopijavimas tokiu kopijavimo ir įklijavimo būdu pažeidžia šį principą. Tai darant, atsiranda didelių programinės įrangos kūrimo ir priežiūros išlaidų. Norėdami sužinoti, kodėl, apsvarstykite, kas atsitinka, kai randama klaida: programuotojas turėtų grįžti ir ištaisyti šią klaidą kiekvienoje padarytoje kopijoje. Jei programuotojas pamirš pamiršti visas pasikartojančias svetaines, bus pristatyta nauja klaida!

Tačiau, kaip parodyta aukščiau pateiktame pavyzdyje, jums bus sunku vienu metu išlaikyti vieną valdymo tašką ir naudoti statinio tipo tikrintuvus, kad garantuotumėte, jog vykdant programą niekada neatsiras tam tikrų klaidų. „Java“, kaip yra šiandien, dažnai neturite kito pasirinkimo, kaip kopijuoti kodą, jei norite tiksliai patikrinti statinį tipą. Be abejo, niekada negalėjai visiškai pašalinti šio „Java“ aspekto. Tam tikri automatų teorijos postulatai, padaryti jų loginę išvadą, reiškia, kad jokia garso tipo sistema negali tiksliai nustatyti visų programos metodų galiojančių įėjimų (ar išėjimų) rinkinio. Vadinasi, kiekvienos rūšies sistema turi išlaikyti pusiausvyrą tarp savo paprastumo ir išvestos kalbos išraiškingumo; „Java“ tipo sistema šiek tiek per daug linksta į paprastumo pusę. Pirmajame pavyzdyje šiek tiek išraiškingesnio tipo sistema būtų leidusi jums tiksliai tikrinti tipą, nereikėtų dublikuoti kodo.

Tokia išraiškingo tipo sistema pridėtų bendrieji tipai į kalbą. Bendrieji tipai yra tipo kintamieji, kuriuos galima iš karto su kiekvienos klasės egzemplioriumi tinkamai parinkti tipą. Šiame straipsnyje aš deklaruosiu tipo kintamuosius kampiniuose skliaustuose, viršijančius klasės ar sąsajos apibrėžimus. Tada tipo kintamojo taikymo sritį sudarys apibrėžties, pagal kurią jis buvo deklaruotas, turinys (neįskaitant tęsiasi sąlyga). Šioje srityje galite naudoti tipo kintamąjį bet kur, kur galite naudoti įprastą tipą.

Pavyzdžiui, naudodami bendrinius tipus, galite perrašyti savo Sąrašas klasę taip:

abstrakčios klasės sąrašas {public abstract T accept (ListVisitor that); } sąsaja „ListVisitor“ {public T _case (Ištuštinti tai); public T _case (Cons that); } klasė Tuščia pratęsia sąrašą {public T accept (ListVisitor that) {return that._case (this); }} klasės trūkumai pirmiausia pratęsia sąrašą {privatus T; privatus Sąrašo poilsis; Minusai (T _pirmas, sąrašas _pailsėjimas) {pirmas = _pirmas; poilsis = _pailsėjimas; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {grąžinkite tą._dėklas (tai); }} 

Dabar galite perrašyti „AddVisitor“ pasinaudoti bendromis rūšimis:

klasė „AddVisitor“ įgyvendina „ListVisitor“ {privatus sveikasis skaičius nulis = naujas sveikasis skaičius (0); public Integer _case (Ištuštinkite tai) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Atkreipkite dėmesį, kad aiškus metimas į Sveikasis skaičius nebereikalingi. Argumentas kad į antrą _case (...) metodas yra deklaruojamas Minusai, egzemplioriuojantis tipo kintamąjį Minusai klasė su Sveikasis skaičius. Todėl statinio tipo tikrintuvas gali tai įrodyti kad.pirmas () bus tipo Sveikasis skaičius ir tai kad.pailsėti () bus tipo Sąrašas. Panašūs pavyzdžiai būtų daromi kiekvieną kartą, kai bus sukurtas naujas Tuščia arba Minusai yra deklaruojamas.

Ankstesniame pavyzdyje tipo kintamuosius galima būtų supaprastinti bet kuriais Objektas. Taip pat galite pateikti tikslesnę viršutinę tipo kintamojo ribą. Tokiais atvejais galite nurodyti šį įrišimą tipo kintamojo deklaravimo taške naudodami šią sintaksę:

  tęsiasi 

Pavyzdžiui, jei norite savo Sąrašass turėti tik Palyginamas objektus, galite apibrėžti tris savo klases taip:

klasės sąrašas {...} klasės trūkumai {...} klasė tuščia {...} 

Nors pridėjus parametrų tipus į „Java“, gautumėte aukščiau nurodytų pranašumų, to daryti nevertėtų, jei tai reikštų proceso metu paaukoti suderinamumą su senu kodu. Laimei, tokia auka nėra būtina. Galima automatiškai išversti kodą, parašytą „Java“ plėtinyje, kuriame yra bendrieji tipai, į esamo JVM baitokodą. Keli rengėjai tai jau daro - ypač geri pavyzdžiai yra „Pizza“ ir „GJ“ sudarytojai, parašyti Martino Odersky. „Pizza“ buvo eksperimentinė kalba, kuri pridėjo keletą naujų „Java“ funkcijų, kai kurios iš jų buvo įtrauktos į „Java 1.2“; GJ yra „Pizza“ įpėdinis, kuris prideda tik bendrinius tipus. Kadangi tai yra vienintelė pridėta funkcija, GJ kompiliatorius gali sukurti baitinį kodą, kuris sklandžiai veiks su senu kodu. Jis kaupia šaltinį į baitą koduoti naudojant tipo ištrynimas, kuri pakeičia kiekvieną kiekvieno tipo kintamojo egzempliorių viršutine to kintamojo riba. Tai taip pat leidžia deklaruoti tipo kintamuosius konkretiems metodams, o ne visoms klasėms. GJ naudoja tą pačią sintaksę bendriesiems tipams, kurią naudoju šiame straipsnyje.

Darbas vyksta

Ryžių universitete programavimo kalbų technologijų grupė, kurioje dirbu, diegia aukštyn suderinamos GJ versijos kompiliatorių, vadinamą „NextGen“. „NextGen“ kalbą kartu sukūrė profesorius Robertas Cartwrightas iš Rice'o informatikos katedros ir Guy Steele'as iš „Sun Microsystems“; tai prideda galimybę atlikti GJ tipo kintamųjų vykdymą.

Kitas galimas šios problemos sprendimas, vadinamas PolyJ, buvo sukurtas MIT. Jis pratęsiamas Kornelyje. „PolyJ“ naudoja šiek tiek kitokią sintaksę nei „GJ / NextGen“. Jis taip pat šiek tiek skiriasi vartojant bendrinius tipus. Pavyzdžiui, jis nepalaiko tipų parametrų nustatymo atskiriems metodams ir šiuo metu nepalaiko vidinių klasių. Tačiau skirtingai nei „GJ“ ar „NextGen“, tai leidžia tipinius kintamuosius inicijuoti su primityviais tipais. Be to, kaip ir „NextGen“, „PolyJ“ palaiko vykdymo laiko operacijas bendruose tipuose.

„Sun“ išleido „Java Specification Request“ (JSR), kad būtų galima įtraukti bendrinius tipus į kalbą. Nenuostabu, kad vienas iš pagrindinių pateiktų tikslų yra suderinamumo su esamomis klasių bibliotekomis palaikymas. Kai prie „Java“ pridedami bendrieji tipai, tikėtina, kad vienas iš aukščiau aptartų pasiūlymų bus prototipas.

Yra keletas programuotojų, kurie nepaiso bet kokios formos bendrinių tipų pridėjimo, nepaisant jų pranašumų. Nurodysiu du tokius oponentų argumentus, kaip argumentai „šablonai yra blogi“ ir „tai nėra orientuotas į objektą“, ir nagrinėsiu kiekvieną iš jų paeiliui.

Ar šablonai yra blogi?

C ++ naudoja šablonai pateikti bendrinių tipų formą. Šablonai užsitarnavo blogą reputaciją tarp kai kurių „C ++“ kūrėjų, nes jų apibrėžimai nėra tipo patikrinami parametrų forma. Vietoj to, kodas pakartojamas kiekviename pavyzdyje, o kiekvieno replikacijos tipas tikrinamas atskirai. Šio požiūrio problema yra ta, kad pirminiame kode gali būti tipo klaidų, kurios nerodomos nė viename pradiniame pavyzdyje. Šios klaidos gali pasireikšti vėliau, jei programos pataisymai ar plėtiniai įveda naujus pavyzdžius. Įsivaizduokite kūrėjo nusivylimą, kai jie naudojasi esamomis klasėmis, kurios patikrina tipą, kai jas sudaro patys, bet ne tada, kai jis prideda naują, visiškai teisėtą poklasį! Dar blogiau, jei šablonas nebus kompiliuojamas kartu su naujomis klasėmis, tokios klaidos nebus aptiktos, o jos sugadins vykdomąją programą.

Dėl šių problemų kai kurie žmonės sunerimę grąžindami šablonus tikėjosi, kad C ++ šablonų trūkumai bus pritaikyti bendro tipo sistemai „Java“. Ši analogija yra klaidinanti, nes semantiniai „Java“ ir „C ++“ pagrindai yra radikaliai skirtingi. C ++ yra nesaugi kalba, kurioje statinio tipo tikrinimas yra euristinis procesas be jokio matematinio pagrindo. Priešingai, „Java“ yra saugi kalba, kuria statinio tipo tikrintuvas pažodžiui įrodo, kad vykdant kodą negali atsirasti tam tikrų klaidų. Todėl C ++ programos, kuriose yra šablonų, kenčia nuo begalės saugos problemų, kurių negali atsirasti „Java“.

Be to, visi žinomi bendrosios „Java“ pasiūlymai atlieka aiškų parametrinių klasių statinio tipo patikrinimą, o ne tik tai daro kiekvieną klasės pavyzdį. Jei nerimaujate, kad toks aiškus tikrinimas sulėtintų tipo tikrinimą, būkite tikri, kad iš tikrųjų yra atvirkščiai: kadangi tipo tikrintuvas atlieka tik vieną perėjimą per parametruojamą kodą, priešingai nei kiekvienam momento momentui. parametrų tipai, tipo patikrinimo procesas yra pagreitintas. Dėl šių priežasčių daugybė prieštaravimų C ++ šablonams netaikomi bendro tipo „Java“ pasiūlymams. Tiesą sakant, jei pažvelgsite ne tik į tai, kas buvo plačiai naudojama pramonėje, yra daug mažiau populiarių, bet labai gerai sukurtų kalbų, tokių kaip „Objective Caml“ ir „Eiffel“, kurios palaiko parametrizuotus tipus.

Ar bendrojo tipo sistemos yra orientuotos į objektą?

Galiausiai, kai kurie programuotojai prieštarauja bet kuriai bendro tipo sistemai, motyvuodami tuo, kad kadangi tokios sistemos iš pradžių buvo sukurtos funkcinėms kalboms, jos nėra orientuotos į objektą. Šis prieštaravimas yra netikras. Kaip rodo aukščiau pateikti pavyzdžiai ir diskusija, bendrieji tipai labai natūraliai įsilieja į objektinį planą. Tačiau įtariu, kad šis prieštaravimas yra susijęs su nepakankamu supratimu, kaip integruoti generinius tipus į „Java“ paveldėjimo polimorfizmą. Iš tikrųjų tokia integracija yra įmanoma ir yra mūsų „NextGen“ diegimo pagrindas.

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