Programmēšana

Redziet parametru polimorfisma spēku

Pieņemsim, ka vēlaties Java ieviest saraksta klasi. Jūs sākat ar abstraktu klasi, Sarakstsun divas apakšklases, Tukšs un Cons, kas attiecīgi attēlo tukšos un neizturīgos sarakstus. Tā kā jūs plānojat paplašināt šo sarakstu funkcionalitāti, jūs izstrādājat a ListVisitor saskarni un nodrošināt pieņemt (...) āķi ListVisitors katrā no jūsu apakšklasēm. Turklāt jūsu Cons klasei ir divi lauki, vispirms un atpūsties, ar atbilstošām piekļuves metodēm.

Kādi būs šo lauku veidi? Skaidrs, atpūsties jābūt veida Saraksts. Ja iepriekš zināt, ka jūsu sarakstos vienmēr būs noteiktas klases elementi, kodēšanas uzdevums šajā brīdī būs ievērojami vienkāršāks. Ja jūs zināt, ka visi jūsu saraksta elementi būs vesels skaitlisPiemēram, jūs varat piešķirt vispirms būt tipa vesels skaitlis.

Tomēr, ja, kā tas bieži notiek, jūs iepriekš nezināt šo informāciju, jums ir jāsamierinās ar vismazāk izplatīto superklasi, kurai ir visi iespējamie elementi jūsu sarakstos, kas parasti ir universālais atsauces tips Objekts. Tādējādi dažāda veida elementu sarakstu kodam ir šāda forma:

abstrakta klase Saraksts {public abstract Object accept (ListVisitor that); } interfeiss ListVisitor {public Object _case (Iztukšojiet to); public Object _case (Cons that); } klase Tukšs paplašina sarakstu {public Object accept (ListVisitor that) {return that._case (this); }} klase Cons vispirms paplašina sarakstu {private Object; privāta Sarakstu atpūta; Mīnusi (Object _first, List_rest) {first = _first; atpūta = _atpūšanās; } public Object first () {first first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Kaut arī Java programmētāji šādā veidā laukam bieži izmanto vismazāk izplatīto superklasi, pieejai ir savi trūkumi. Pieņemsim, ka jūs izveidojat a ListVisitor kas pievieno visus elementu sarakstā Vesels skaitliss un atgriež rezultātu, kā parādīts zemāk:

klase AddVisitor īsteno ListVisitor {privātais skaitlis nulle = jaunais vesels skaitlis (0); public Object _case (iztukšot to) {return zero;} public Object _case (Cons that) {return new Integer ((((Integer) that.first ()). intValue () + ((Integer) that.rest (). (šis)). intValue ()); }} 

Ievērojiet nepārprotamus pārraides Vesels skaitlis otrajā _case (...) metodi. Jūs atkārtoti veicat izpildlaika testus, lai pārbaudītu datu īpašības; ideālā gadījumā kompilatoram jāveic šie testi jums kā daļa no programmas tipa pārbaudes. Bet, tā kā jums tas nav garantēts AddVisitor tiks piemērots tikai Sarakstss no Vesels skaitliss, Java tipa pārbaudītājs nevar apstiprināt, ka jūs faktiski pievienojat divus Vesels skaitliss, ja vien cast nav klāt.

Jūs, iespējams, varētu iegūt precīzāku tipa pārbaudi, bet tikai upurējot polimorfismu un dublējot kodu. Jūs, piemēram, varētu izveidot īpašu Saraksts klase (ar atbilstošo Cons un Tukšs apakšklases, kā arī īpašs Apmeklētājs interfeiss) katrai elementu klasei, kuru saglabājat a Saraksts. Iepriekš minētajā piemērā jūs izveidosiet IntegerList klase, kuras elementi ir visi Vesels skaitliss. Bet, ja vēlaties glabāt, teiksim: Būlas kādā citā programmas vietā jums vajadzētu izveidot Būla saraksts klasē.

Skaidrs, ka programmas apjoms, kas rakstīts, izmantojot šo paņēmienu, strauji palielināsies. Ir arī citi stilistiskie jautājumi; viens no labas programmatūras inženierijas pamatprincipiem ir vienots vadības punkts katram programmas funkcionālajam elementam, un koda dublēšana šādā kopēšanas un ielīmēšanas veidā pārkāpj šo principu. Tas parasti rada augstas programmatūras izstrādes un uzturēšanas izmaksas. Lai uzzinātu, kāpēc, ņemiet vērā, kas notiek, ja tiek atrasta kļūda: programmētājam katrā atgrieztajā kopijā būtu jāatgriežas un šī kļūda jālabo atsevišķi. Ja programmētājs aizmirst identificēt visas dublētās vietnes, tiks ieviesta jauna kļūda!

Bet, kā ilustrēts iepriekš minētajā piemērā, jums būs grūti vienlaikus saglabāt vienu vadības punktu un izmantot statiskā tipa pārbaudītājus, lai garantētu, ka programmas izpildes laikā nekad nenotiks noteiktas kļūdas. Java, kā tas pastāv šodien, jums bieži nav citas izvēles kā kopēt kodu, ja vēlaties veikt precīzu statiskā tipa pārbaudi. Lai pārliecinātos, jūs nekad nevarēja pilnībā novērst šo Java aspektu. Daži automatātu teorijas postulāti, kas pieņemti to loģiskajā secinājumā, nozīmē, ka neviena skaņas tipa sistēma nevar precīzi noteikt derīgo ievades (vai izejas) kopumu visām programmas metodēm. Līdz ar to katrai tipa sistēmai ir jāpanāk līdzsvars starp savu vienkāršību un no tā izrietošās valodas izteiksmīgumu; Java tipa sistēma mazliet par daudz sliecas vienkāršības virzienā. Pirmajā piemērā nedaudz izteiksmīgāka tipa sistēma ļaus jums uzturēt precīzu tipa pārbaudi, nedublējot kodu.

Šāda izteiksmīga tipa sistēma pievienotu vispārīgi veidi uz valodu. Vispārējie tipi ir tipa mainīgie, kurus var precizēt ar attiecīgi specifisku tipu katram klases gadījumam. Šī raksta vajadzībām es deklarēšu tipa mainīgos leņķa iekavās virs klases vai saskarnes definīcijām. Tipa tipa mainīgā darbības joma tad sastāvēs no definīcijas pamatteksta, kurā tas tika deklarēts (neieskaitot pagarina klauzula). Šajā darbības jomā tipa mainīgo var izmantot jebkurā vietā, kur varat izmantot parasto veidu.

Piemēram, izmantojot vispārīgus veidus, jūs varētu pārrakstīt savu Saraksts klase šādi:

abstraktā klase Saraksts {public abstract T accept (ListVisitor that); } interfeiss ListVisitor {public T _case (Iztukšojiet to); public T _case (Cons that); } klase Tukšs paplašina sarakstu {public T accept (ListVisitor that) {return that._case (this); }} klase Cons vispirms paplašina sarakstu {private T; privāts Saraksta atpūta; Mīnusi (T _pirmais, List_rest) {pirmais = _pirmais; atpūta = _atpūšanās; } public T first () {first first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Tagad jūs varat pārrakstīt AddVisitor lai izmantotu vispārīgo veidu priekšrocības:

klase AddVisitor īsteno ListVisitor {privātais skaitlis nulle = jaunais vesels skaitlis (0); public Integer _case (Iztukšojiet to) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }} 

Ievērojiet, ka nepārprotami tiek atskaņoti Vesels skaitlis vairs nav vajadzīgi. Arguments to uz otro _case (...) metode ir deklarēta Cons, instancējot tipa mainīgo Cons klase ar Vesels skaitlis. Tāpēc statiskā tipa pārbaudītājs to var pierādīt tas. pirmais () būs veida Vesels skaitlis un tas tas. atpūta () būs veida Saraksts. Līdzīgi gadījumi tiek veikti katru reizi, kad tiek parādīts jauns Tukšs vai Cons tiek deklarēts.

Iepriekš sniegtajā piemērā tipa mainīgos varētu instantizēt ar jebkuru Objekts. Jūs varētu arī norādīt konkrētāku tipa mainīgā augšējo robežu. Šādos gadījumos šo saistošo var norādīt tipa mainīgā deklarēšanas punktā ar šādu sintaksi:

  pagarina 

Piemēram, ja vēlaties savu Sarakstss saturēt tikai Salīdzināms objektus, jūs varētu definēt savas trīs klases šādi:

klases saraksts {...} klases mīnusi {...} klase Tukšs {...} 

Lai gan Java parametru veidu pievienošana sniegtu jums iepriekš norādītās priekšrocības, to darīt nebūtu vērts, ja tas nozīmētu ziedot saderību ar mantoto kodu procesā. Par laimi šāds upuris nav vajadzīgs. Ir iespējams automātiski tulkot kodu, kas ierakstīts Java paplašinājumā, kurā ir vispārīgi tipi, esošā JVM baitkodā. Vairāki sastādītāji to jau dara - īpaši labi piemēri ir Pica un GJ sastādītāji, kuru autors ir Martins Oderskis. Pizza bija eksperimentāla valoda, kas Java pievienoja vairākas jaunas funkcijas, no kurām dažas tika iekļautas Java 1.2; GJ ir pica pēctecis, kas pievieno tikai vispārīgus veidus. Tā kā šī ir vienīgā pievienotā funkcija, GJ kompilators var izveidot baitkodu, kas vienmērīgi darbojas ar mantoto kodu. Tas apkopo avotu bytecode, izmantojot veida dzēšana, kas aizvieto katra tipa mainīgā katru gadījumu ar šī mainīgā augšējo robežu. Tas arī ļauj deklarēt tipa mainīgos īpašām metodēm, nevis veselām klasēm. GJ vispārīgajiem tipiem izmanto to pašu sintaksi, kuru es izmantoju šajā rakstā.

Darbs progresā

Rīsu universitātē programmēšanas valodu tehnoloģiju grupa, kurā es strādāju, ievieš kompilatoru augšup savietojamai GJ versijai ar nosaukumu NextGen. NextGen valodu kopīgi izstrādāja profesors Roberts Kārtraits no Raisa datorzinātņu nodaļas un Gajs Stīls no Sun Microsystems; tas GJ pievieno spēju veikt tipa mainīgo izpildlaika pārbaudes.

Vēl viens potenciāls šīs problēmas risinājums, ko sauc par PolyJ, tika izstrādāts MIT. Tas tiek pagarināts Kornelā. PolyJ izmanto nedaudz atšķirīgu sintaksi nekā GJ / NextGen. Tas nedaudz atšķiras arī no vispārīgo veidu izmantošanas. Piemēram, tas neatbalsta atsevišķu metožu tipa parametru noteikšanu un pašlaik neatbalsta iekšējās klases. Bet atšķirībā no GJ vai NextGen, tas ļauj tipu mainīgos palielināt ar primitīviem tipiem. Tāpat kā NextGen, PolyJ atbalsta izpildlaika darbības vispārīgos veidos.

Sun ir izlaidis Java specifikācijas pieprasījumu (JSR), lai valodai pievienotu vispārīgus veidus. Nav pārsteigums, ka viens no galvenajiem mērķiem, kas uzskaitīti jebkuram iesniegumam, ir saderības uzturēšana ar esošajām klases bibliotēkām. Kad Java tiek pievienoti vispārīgi veidi, visticamāk, ka viens no iepriekš apspriestajiem priekšlikumiem būs kā prototips.

Ir daži programmētāji, kuri iebilst pret vispārīgu veidu pievienošanu jebkādā formā, neskatoties uz to priekšrocībām. Es atsaucos uz diviem šādu pretinieku argumentiem kā "veidnes ir ļaunas" un "tas nav orientēts uz objektu", un pievērsīšos katram no viņiem pēc kārtas.

Vai veidnes ir ļaunas?

C ++ izmanto veidnes sniegt vispārīgu veidu formu. Veidnes ir nopelnījušas sliktu reputāciju dažu C ++ izstrādātāju vidū, jo to definīcijas tipizētā veidā netiek pārbaudītas. Tā vietā kods tiek atkārtots katrā eksemplārā, un katra replikācijas tips tiek pārbaudīts atsevišķi. Šīs pieejas problēma ir tāda, ka sākotnējā kodā var būt tipa kļūdas, kas neparādās nevienā sākotnējā eksemplārā. Šīs kļūdas var izpausties vēlāk, ja programmas pārskatīšana vai paplašināšana ievieš jaunus piemērus. Iedomājieties izstrādātāja neapmierinātību, izmantojot esošās klases, kuras pārbauda, ​​kad tās apkopo paši, bet ne pēc tam, kad viņš ir pievienojis jaunu, pilnīgi likumīgu apakšklasi! Vēl trakāk, ja veidne netiks atkārtoti kompilēta kopā ar jaunajām klasēm, šādas kļūdas netiks atklātas, bet tā vietā tiks bojāta izpildošā programma.

Šo problēmu dēļ daži cilvēki saraujas, atgriežot veidnes, sagaidot, ka C ++ veidņu trūkumi attieksies uz vispārēju Java sistēmu. Šī līdzība ir maldinoša, jo Java un C ++ semantiskie pamati ir radikāli atšķirīgi. C ++ ir nedroša valoda, kurā statiskā tipa pārbaude ir heiristisks process bez matemātiska pamata. Turpretī Java ir droša valoda, kurā statiskā tipa pārbaudītājs burtiski pierāda, ka, izpildot kodu, nevar notikt noteiktas kļūdas. Tā rezultātā C ++ programmas, kurās ir veidnes, cieš no neskaitāmām drošības problēmām, kuras nevar rasties Java.

Turklāt visi redzamie vispārīgās Java priekšlikumi tieši veic parametrizēto klašu statisko tipu pārbaudi, nevis tikai to dara katrā klases instancēšanā. Ja jūs uztraucat, ka šāda nepārprotama pārbaude varētu palēnināt tipa pārbaudi, esiet drošs, ka patiesībā ir tieši otrādi: tā kā tipa pārbaudītājs pāriet tikai pa parametru kodu, atšķirībā no caurlaides katrai parametru veidiem, tiek paātrināts tipa pārbaudes process. Šo iemeslu dēļ daudzie iebildumi pret C ++ veidnēm neattiecas uz vispārējiem Java priekšlikumiem. Patiesībā, ja paskatās tālāk par nozarē plaši izmantoto, ir daudz mazāk populāru, bet ļoti labi izstrādātu valodu, piemēram, Objective Caml un Eiffel, kas atbalsta parametrizētus veidus ar lielu priekšrocību.

Vai vispārēja tipa sistēmas ir orientētas uz objektu?

Visbeidzot, daži programmētāji iebilst pret jebkuru vispārēju sistēmu, pamatojot to, ka, tā kā šādas sistēmas sākotnēji tika izstrādātas funkcionālām valodām, tās nav orientētas uz objektu. Šis iebildums ir viltus. Vispārīgie veidi ļoti dabiski iekļaujas objektorientētā sistēmā, kā to parāda iepriekš minētie piemēri un diskusija. Bet man ir aizdomas, ka šī iebilduma pamatā ir izpratnes trūkums par to, kā vispārīgos veidus integrēt Java mantojuma polimorfismā. Faktiski šāda integrācija ir iespējama, un tā ir mūsu NextGen ieviešanas pamats.

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