Programovanie

Počujte na sile parametrického polymorfizmu

Predpokladajme, že chcete implementovať triedu zoznamu v Jave. Začínaš abstraktnou triedou, Zoznama dve podtriedy, Prázdny a Zápory, ktoré predstavujú prázdne zoznamy a zoznamy prázdnych zoznamov. Pretože plánujete rozšíriť funkčnosť týchto zoznamov, navrhujete a ListVisitor rozhranie a poskytnúť súhlasiť(...) háčiky pre ListVisitors v každej z vašich podtried. Ďalej vaše Zápory trieda má dve polia, najprv a odpočívaj, so zodpovedajúcimi metódami prístupového objektu.

Aké budú typy týchto polí? Jasne, odpočívaj by mali byť typu Zoznam. Ak vopred viete, že vaše zoznamy budú vždy obsahovať prvky danej triedy, bude úloha kódovania v tomto okamihu podstatne ľahšia. Ak viete, že všetky prvky zoznamu budú celé číslos, môžete napríklad priradiť najprv byť typu celé číslo.

Ak však, ako to často býva, tieto informácie vopred nepoznáte, musíte sa uspokojiť s najmenej bežnou nadtriedou, ktorá obsahuje všetky možné prvky vo vašich zoznamoch, čo je zvyčajne univerzálny referenčný typ. Objekt. Váš kód pre zoznamy prvkov rôzneho typu má preto nasledujúcu formu:

abstraktná trieda Zoznam {verejný abstraktný objekt akceptovať (ListVisitor that); } rozhranie ListVisitor {public Object _case (prázdne); public Object _case (Nevýhody); } trieda Empty extends List {public Object accept (ListVisitor that) {return that._case (this); }} class Cons rozširuje najskôr List {private Object; súkromný odpočinok v zozname; Nevýhody (Object _first, List _rest) {first = _first; rest = _rest; } public Object first () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Aj keď programátori Java často používajú pre pole najmenej bežnú nadtriedu, prístup má svoje nevýhody. Predpokladajme, že vytvoríte a ListVisitor ktorý pridá všetky prvky zo zoznamu Celé číslos a vráti výsledok, ako je znázornené nižšie:

trieda AddVisitor implementuje ListVisitor {súkromné ​​celé číslo nula = nové celé číslo (0); public Object _case (Prázdne, že) {return zero;} public Object _case (Cons that) {return new Integer ((((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (toto)). intValue ()); }} 

Všimnite si výslovné obsadenie Celé číslo v druhom _case (...) metóda. Opakovane vykonávate testy za behu, aby ste skontrolovali vlastnosti údajov; v ideálnom prípade by kompilátor mal tieto testy vykonať za vás ako súčasť kontroly typu programu. Ale keďže to nemáte zaručené AddVisitor sa bude vzťahovať iba na Zoznams z Celé číslos, kontrola typu Java nemôže potvrdiť, že v skutočnosti pridávate dva Celé číslos pokiaľ nie sú prítomné obsadenia.

Potenciálne by ste mohli získať presnejšiu kontrolu typu, ale iba obetovaním polymorfizmu a duplikovania kódu. Môžete napríklad vytvoriť špeciálny Zoznam trieda (s príslušnými Zápory a Prázdny podtriedy, ako aj špeciálne Návštevník rozhranie) pre každú triedu prvkov, ktoré ukladáte do a Zoznam. V príklade vyššie by ste vytvorili IntegerList trieda, ktorej prvky sú všetky Celé číslos. Ale ak ste chceli uložiť, povedzte Boolovskýs na nejakom inom mieste v programe, museli by ste vytvoriť a BooleanList trieda.

Je zrejmé, že veľkosť programu napísaného pomocou tejto techniky by sa rýchlo zväčšila. Existujú aj ďalšie štylistické problémy; jedným zo základných princípov dobrého softvérového inžinierstva je mať jeden bod kontroly pre každý funkčný prvok programu a duplikovanie kódu týmto spôsobom kopírovania a vkladania tento princíp porušuje. To bežne vedie k vysokým nákladom na vývoj a údržbu softvéru. Ak chcete zistiť prečo, zvážte, čo sa stane, keď sa vyskytne chyba: programátor by sa musel vrátiť späť a túto chybu opraviť osobitne v každej vytvorenej kópii. Ak programátor zabudne identifikovať všetky duplikované stránky, bude zavedená nová chyba!

Ako však ilustruje vyššie uvedený príklad, bude pre vás ťažké súčasne udržať jediný bod kontroly a použiť kontroly statického typu, aby ste zaručili, že pri spustení programu nedôjde k určitým chybám. V prostredí Java, aké existuje dnes, často nemáte inú možnosť, iba duplikovať kód, ak chcete vykonať presnú kontrolu statického typu. Pre istotu nikdy nemôžete úplne vylúčiť tento aspekt Javy. Niektoré logické závery teórie automatov naznačujú, že žiadny systém zvukového typu nedokáže presne určiť súbor platných vstupov (alebo výstupov) pre všetky metódy v programe. V dôsledku toho musí každý typový systém nájsť rovnováhu medzi svojou jednoduchosťou a expresívnosťou výsledného jazyka; systém typu Java sa trochu nakláňa smerom k jednoduchosti. V prvom príklade by vám systém trochu expresívnejšieho typu umožnil udržiavať presnú kontrolu typu bez toho, aby ste museli duplikovať kód.

Takýto systém expresívneho typu by dodal generické typy do jazyka. Generické typy sú typové premenné, ktoré je možné vytvoriť inštanciami s vhodne špecifickým typom pre každú inštanciu triedy. Na účely tohto článku budem deklarovať premenné typu v lomených zátvorkách nad definíciami triedy alebo rozhrania. Rozsah typovej premennej potom bude pozostávať z tela definície, pri ktorej bola deklarovaná (okrem predlžuje doložka). V tomto rozsahu môžete premennú typu použiť kdekoľvek, kde môžete použiť bežný typ.

Napríklad pri všeobecných typoch môžete prepísať svoj Zoznam triedy nasledovne:

abstraktná trieda Zoznam {verejný abstrakt T akceptovať (ListVisitor že); } rozhranie ListVisitor {public T _case (prázdne); public T _case (Nevýhody); } trieda Empty rozširuje List {public T accept (ListVisitor that) {return that._case (this); }} class Cons najskôr rozširuje Zoznam {private T; súkromný odpočinok v zozname; Nevýhody (T _prvý, Zoznam _rest) {prvý = _prvý; rest = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Teraz môžete prepísať AddVisitor využiť výhody generických typov:

trieda AddVisitor implementuje ListVisitor {private Integer zero = new Integer (0); public Integer _case (Prázdne, že) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Všimnite si, že explicitné obsadenie Celé číslo už nie sú potrebné. Tvrdenie že do druhej _case (...) metóda je deklarovaná ako Zápory, inštancovanie premennej typu pre Zápory trieda s Celé číslo. Kontrola statického typu to teda môže dokázať that.first () bude typu Celé číslo a to that.rest () bude typu Zoznam. Podobné inštancie by sa vytvorili pri každej novej inštancii Prázdny alebo Zápory je vyhlásený.

Vo vyššie uvedenom príklade je možné premenné typu vytvoriť s ľubovoľnými Objekt. Môžete tiež zadať konkrétnejšiu hornú hranicu typovej premennej. V takýchto prípadoch môžete túto väzbu určiť v deklaračnom bode typovej premennej s nasledujúcou syntaxou:

  predlžuje 

Napríklad, ak ste chceli svoj Zoznams obsahovať iba Porovnateľné objekty, môžete definovať svoje tri triedy nasledovne:

trieda Zoznam {...} trieda Nevýhody {...} trieda Prázdna {...} 

Aj keď by vám pridanie parametrizovaných typov do Javy prinieslo výhody uvedené vyššie, nebolo by to užitočné, ak by to malo znamenať obetovanie kompatibility so starým kódom v tomto procese. Našťastie takáto obeta nie je potrebná. Je možné automaticky preložiť kód napísaný v rozšírení Java, ktoré má všeobecné typy, do bytecode pre existujúci JVM. Robí to už niekoľko prekladačov - obzvlášť dobrým príkladom sú prekladače Pizza a GJ, ktorých autorom je Martin Odersky. Pizza bol experimentálny jazyk, ktorý do Javy pridal niekoľko nových funkcií, z ktorých niektoré boli začlenené do Javy 1.2; GJ je nástupcom spoločnosti Pizza, ktorá pridáva iba všeobecné typy. Pretože toto je jediná pridaná funkcia, kompilátor GJ môže vytvárať bytecode, ktorý hladko funguje so starým kódom. Kompiluje zdroj do bajtkódu pomocou vymazanie typu, ktorá nahradí každú inštanciu každej premennej typu hornou hranicou tejto premennej. Umožňuje tiež deklarovať premenné typu pre konkrétne metódy, a nie pre celé triedy. GJ používa rovnakú syntax pre všeobecné typy, ktoré používam v tomto článku.

Prebieha práca

Na Rice University technologická skupina pre programovacie jazyky, v ktorej pracujem, implementuje kompilátor pre hore kompatibilnú verziu GJ s názvom NextGen. Jazyk NextGen vyvinuli spoločne profesor Robert Cartwright z oddelenia počítačovej vedy Rice a Guy Steele zo spoločnosti Sun Microsystems; pridáva GJ schopnosť vykonávať runtime kontroly typových premenných.

Ďalšie potenciálne riešenie tohto problému s názvom PolyJ bolo vyvinuté na MIT. Predlžuje sa v Cornelle. PolyJ používa mierne odlišnú syntax ako GJ / NextGen. Mierne sa tiež líši v použití generických typov. Napríklad nepodporuje parametrizáciu typov jednotlivých metód a momentálne nepodporuje vnútorné triedy. Ale na rozdiel od GJ alebo NextGen umožňuje inštanciu typových premenných s primitívnymi typmi. Rovnako ako NextGen, PolyJ podporuje aj behové operácie na všeobecných typoch.

Spoločnosť Sun vydala požiadavku JSR (Java Specification Request) na pridávanie všeobecných typov do jazyka. Nie je prekvapením, že jedným z kľúčových cieľov uvedených pre každé zadanie je udržiavanie kompatibility s existujúcimi knižnicami tried. Keď sa do Javy pridajú všeobecné typy, je pravdepodobné, že jeden z vyššie diskutovaných návrhov bude slúžiť ako prototyp.

Existuje niekoľko programátorov, ktorí sú proti pridávaniu generických typov v akejkoľvek forme, a to aj napriek ich výhodám. Odvolám sa na dva bežné argumenty takýchto oponentov, ako je argument „šablóny sú zlé“ a argument „nie je to objektovo orientované“ a postupne sa budem venovať každému z nich.

Sú šablóny zlé?

Používa C ++ šablóny poskytnúť formu všeobecných typov. Šablóny si medzi niektorými vývojármi jazyka C ++ vyslúžili zlú povesť, pretože ich definície nie sú typovo kontrolované v parametrizovanej podobe. Namiesto toho sa kód replikuje pri každej inštancii a každá replikácia sa typovo kontroluje osobitne. Problém s týmto prístupom spočíva v tom, že v pôvodnom kóde môžu existovať chyby typu, ktoré sa nezobrazia v žiadnom z počiatočných inštancií. Tieto chyby sa môžu prejaviť neskôr, ak revízie alebo rozšírenia programu zavedú nové inštancie. Predstavte si frustráciu vývojára, ktorý používa existujúce triedy, ktoré zadávajú kontrolu, keď sú zostavené sami, ale nie potom, čo pridá novú, úplne legitímnu podtriedu! Horšie však je, že ak šablóna nebude znovu skompilovaná spolu s novými triedami, takéto chyby sa nezistia, ale skôr poškodia vykonávajúci program.

Kvôli týmto problémom sa niektorí ľudia zamračia nad prinášaním šablón späť a očakávajú, že nevýhody šablón v C ++ sa budú vzťahovať na systém generického typu v Jave. Táto analógia je zavádzajúca, pretože sémantické základy Java a C ++ sú radikálne odlišné. C ++ je nebezpečný jazyk, v ktorom je statická kontrola typu heuristickým procesom bez matematického základu. Naproti tomu Java je bezpečný jazyk, v ktorom kontrola statického typu doslovne dokazuje, že pri spustení kódu nemôžu nastať určité chyby. Výsledkom je, že programy C ++ zahŕňajúce šablóny trpia nespočetnými bezpečnostnými problémami, ktoré sa v Jave nemôžu vyskytnúť.

Navyše, všetky prominentné návrhy pre všeobecnú Javu vykonávajú explicitnú kontrolu statického typu parametrizovaných tried, a nielen to pri každej inštancii triedy. Ak sa obávate, že by takáto explicitná kontrola spomalila kontrolu typu, buďte si istí, že v skutočnosti je pravý opak: pretože kontrola typu vykoná iba jeden prechod parametrizovaným kódom, na rozdiel od priechodu pre každú inštanciu parametrizovaných typov sa urýchli proces kontroly typu. Z týchto dôvodov sa početné výhrady k šablónam C ++ nevzťahujú na návrhy generických typov pre Javu. V skutočnosti, ak sa pozriete nad rámec toho, čo sa v priemysle často používa, existuje veľa menej populárnych, ale veľmi dobre navrhnutých jazykov, ako napríklad Objective Caml a Eiffel, ktoré s veľkou výhodou podporujú parametrizované typy.

Sú systémy generického typu objektovo orientované?

Nakoniec, niektorí programátori namietajú proti každému systému generického typu z toho dôvodu, že pretože tieto systémy boli pôvodne vyvinuté pre funkčné jazyky, nie sú objektovo orientované. Táto námietka je falošná. Ako ukazujú príklady a diskusia uvedené vyššie, generické typy zapadajú veľmi prirodzene do objektovo orientovaného rámca. Mám ale podozrenie, že táto námietka má pôvod v nepochopení toho, ako integrovať generické typy s dedičným polymorfizmom Javy. V skutočnosti je takáto integrácia možná a je základom pre našu implementáciu NextGen.

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