În Object Pascal, folosit de mediul de dezvoltare Delphi, există un mecanism pentru constructorii virtuali. Pentru fanii C ++, aceasta pare a fi o erezie teribilă, dar constructorii virtuali sunt foarte utili pentru crearea de instanțe de clase care nu sunt încă definite la etapa de generare a codului compilatorului. Această tehnologie vă permite să dezvoltați codul componentei fără a fi nevoie să implementați sălile de clasă.
Partea laterală a acestei flexibilități este posibilitatea creării accidentale a unei instanțe a unei clase abstracte, care va duce aproape în mod inevitabil la chemarea uneia dintre metodele abstracte. Dacă în C ++ apelul unei funcții pur virtuale necesită o destul de dexteritate și este aproape imposibil să o produci neintenționat, în Delphi acest lucru se realizează printr-o mișcare neatentă. Din păcate, mecanismele integrate ale Delphi pentru detectarea și procesarea metodelor abstracte oferă doar un minim de informații despre sursa erorii.
Constructori virtuali
Toate exemplele din acest articol, dacă nu este indicat altfel, au fost compilate și testate pentru Borland Delphi 5.
În combinație cu o referință la clasa de componente:
aceasta vă permite să creați orice componente, având o referință la clasa lor, chiar dacă este dezvoltată după compilarea următorului cod:
Această caracteristică este esențială pentru activitatea mecanismului de citire a formularelor din fișierele și resursele DFM. În plus, acesta poate fi util pentru codul de utilizator care nu este direct legat de VCL. Cele mai populare domenii de aplicare a unei astfel de funcționalități sunt serializarea obiectelor și înregistrarea unui plug-in. În plus, pe baza acestui mecanism și RTTI în Delphi 6 au fost implementate servicii web.
Metode rezumate
Acum, ia în considerare următorul cod:
La prima vedere, totul este în ordine. Există o clasă abstractă care declară o anumită funcționalitate și există un descendent care implementează această funcție. Presupunem să folosim acest lucru în felul următor:
(Codul real va fi, desigur, mai complicat). Cel mai probabil, obiectele vor fi create într-un singur loc, dar vor fi folosite într-un altul, dar esența problemei nu se va schimba.)
Care este problema? Și faptul că un programator de aplicații neglijent poate trece ușor la procedura noastră ca parametru o referință la o clasă abstractă:
Acest cod se va compila cu succes. Ce se întâmplă atunci când aplicația rulează? Cel mai simplu experiment va arăta că rezultatul este o excepție EAbstractError atunci când este apelată metoda DoSomeJob.
Se pare că totul este în ordine: intrusul este prins, justiția este restabilită. Dar nu. EAbstractError este o clasă extrem de neinformativă. Nu oferă nicio informație despre contextul și cauza erorii. Dacă sunteți dezvoltator și aplicația a emis un mesaj corespunzător, aveți șansa să petreceți ceva timp pentru a comunica cu debuggerul și execuția pas cu pas pentru a prinde clasa de intrusi. Dar dacă ați compilat biblioteca fără a depana informații și cod sursă, atunci programatorul aplicației poate ghici doar ce a făcut greșit.
Există, bineînțeles, o modalitate foarte simplă de a "depăși" problema - de a nu declara niciodată metode abstracte. VCL folosește foarte des definițiile metodei "goale". Cu toate acestea, aceasta nu este calea pentru programatori reali. Cel puțin pentru motivul că implementarea "goală" a procedurii are încă un sens, dar orice funcție ar trebui să revină la o anumită valoare.
O modalitate mai naturală este de a împiedica crearea de instanțe de clase abstracte, așa cum sa făcut, de exemplu, în C ++. Din păcate, compilatorul Delphi se va limita la un avertisment: "construirea unui exemplu de clasă ... care să conțină metode abstracte". Ieșirea acestui avertisment poate fi oprită de opțiunile de compilator corespunzătoare.
De regulă, programatorii îngrijitori monitorizează îndeaproape avertismentele emise de compilator. Dar, în situația descrisă mai sus, "nu va exista tunet" și nu va mai fi nici un motiv să fie botezat de programator.
Aplicație de testare
Vom ilustra tehnica utilizării caracteristicilor obiectului Object Pascal folosind un exemplu de aplicație simplă.
Programul nostru va fi extrem de simplu. Acesta va permite utilizatorului să introducă două numere întregi și să facă un set de operații aritmetice simple cu ele. Implementarea programului va include doar adăugarea și multiplicarea, dar vom avea grijă ca programatorii să ajute utilizatorul programului să țină la curent, dezvoltând adăugiri la program.
Pentru a testa conceptul, vom crea un pachet extensie care implementează două clase de operatori complexi întregi: TPowerOp este operatorul de exponentiere și TCnkOp este operatorul numărului de combinații.
În clasa TCnkOp, "uităm" să ignorăm una dintre metodele abstracte declarate în clasa de bază. Vom vedea că procesarea standard a unor astfel de erori nu oferă nicio informație despre cauzele erorii și vom construi procesarea noastră astfel încât să putem determina imediat ce clasă și ce metodă a rămas abstractă.
Obținerea mai multor informații
Pentru a afla mai multe despre ceea ce a condus la un apel abstract, trebuie să înțelegeți modul în care Delphi implementează metode abstracte.
Un handler standard
Dacă ați urmărit apelul la metoda abstractă TAbstractObject.DoSomeJob, atunci se va găsi un detaliu interesant: controlul este transmis procedurii de sistem _AbstractError:
Manager avansat
Din secțiunea anterioară, puteți trage două concluzii:
- Există o modalitate documentată de înregistrare a handlerului pentru apeluri abstracte.
- Deși mediul nu transmite parametri acestui manipulant, funcțiile pe care apelantul nostru le solicită nu afectează în niciun fel contextul apelului.
Rețineți codul de procedură TAbstractHandler.HandleAbstract - generează o excepție cu numele clasei ca textul mesajului. La prima vedere se pare că va întoarce întotdeauna șirul "TAbstractHandler", dar nu este. Faptul este că am denumit metoda TAbstractHandler.HandleAbstract pe un obiect al unei clase complet diferite! Codul real este foarte asemănător cu următorul:
În acest exemplu, textul excepției va conține "TAbstractObject". De obicei, astfel de apeluri duc la erori, dar dacă sunt respectate anumite reguli, ele sunt complet sigure. Versiunea "pesimistă" a acestor reguli este după cum urmează: este posibilă numirea unei metode "străine" numai dacă folosește numai câmpurile și metodele strămoșului comun al clasei sale "proprii" și "străine". În practică, există mai multă libertate, dar pentru cazul nostru este deja suficient. Metoda HandleAbstract utilizează numai metoda ClassName, care este disponibilă în TObject, care este garantat a fi strămoșul tuturor claselor Delphi.
Această tehnică nu funcționează atunci când apelați metoda clasă abstractă. În metodele de clasă, sinele se referă la o clasă, nu la un obiect, iar substituția folosită este nevalidă. Din păcate, nu văd o modalitate sigură de a face față acestei situații - este destul de dificil să distingem indicatorul VMT de pointer la indicatorul VMT.
Preempțiune precoce
Pentru a preveni crearea de exemple de clase abstracte, trebuie să răspundem întâi la întrebarea: "Este această clasă abstractă?". Răspunsul la această întrebare este simplu: "O clasă este abstractă dacă conține metode abstracte". Delphi în sine nu conține instrumente integrate pentru testarea metodelor de abstractitate, astfel încât aceste instrumente trebuie inventate independent.
Pentru a afla dacă metoda de clasă este abstractă, va trebui să sapi un pic în adâncurile întunecate ale modulului System cu depanare pas cu pas. După cum știm deja din secțiunea anterioară, încercarea de a apela o metodă abstractă ne conduce la procedura _AbstractError. Acum trebuie să urmărim calea care duce la această procedură.
Studiul structurii tabelului de metode virtuale (VMT) creat de compilator și RTTI în general este un proces interesant care poate oferi dezvoltatorului curios o mulțime de distracție. Pentru cei care nu vor să-și piardă timpul în pregătirea codului sistemului Delphi, aduc informațiile necesare în formă gata de utilizare.
Structura clasei Delphi
- Comparați referințele la o clasă pentru a verifica tipul obiectului.
- Apelați metodele clasei.
- Call constructori.
Din păcate, această funcție nu este suficientă pentru a căuta metode abstracte. Pentru o astfel de căutare, va trebui să privim sub capota clasei, și anume, pentru a vedea cum funcționează metoda TObject.ClassType. Implementarea, desigur, poate varia de la versiune la versiune. În Delphi 5 codul este extrem de laconic:
Cât de interesant! Unele dintre ele sunt mai puțin de zero. Judecând după numele constantelor, până la vmtAfterConstruction (offset -28) pointer la diferite date interesante sunt localizate. Apoi, există indicii pentru metodele virtuale declarate în TObject: AfterConstruction, BeforeDestruction, Dispatch, DefaultHandler, NewInstance, FreeInstance, Destroy. Apoi, există metode cu compensări non-negative. Astfel, pointerul situat la începutul obiectului se referă undeva "în mijlocul" VMT-ului. Și acest mijloc este exact locul de unde vor fi localizate metodele virtuale declarate în clasele descendente. VmtQueryInterface numelor constante, vmtAddRef și vmtRelease clar de ce a fost făcută - altfel descendenții TObject ar fi fost imposibil să se pună în aplicare interfață IUnknown.
Astfel, cele 4 octeți primite prin apelarea punctului TObject.ClassType la începutul tabelului de metode virtuale declarate descendenților TObject. Această ieșire poate fi considerată "sigură" atât timp cât Delphi acceptă compatibilitatea cu COM.
Metode rezumate
Acest cod necesită explicații.
După ce am primit un indicator al metodei clasei în variabila TAP, vom selecta un pointer la codul din ea folosind o distribuție documentată la tipul SysUtils.TMethod.
Cu toate acestea, am realizat aceste experimente pe o clasă care a fost compilată ca parte a aplicației noastre. În exemplul nostru, o parte din clase sunt situate într-un pachet separat, care este compilat într-un fișier separat de bibliotecă. Va fi o chemare la același _AbstractProc din aceste clase? Și dacă da, cum?
Pentru a răspunde la aceste întrebări, trebuie să știți cum Delphi implementează pachete componente componente dinamic conectate. O examinare detaliată a acestui subiect depășește domeniul de aplicare al acestui articol. Prin urmare, voi furniza imediat rezultatul aici, sărind descrierea cercetării mele pe fișierele .bpl.
Da, Delphi monitorizează cu strictețe că aplicația nu poate descărca două versiuni ale aceluiași modul în pachete diferite. Aceasta este, putem fi siguri că orice apel abstract ne va conduce la un singur _AbstractProc. Pentru a face acest lucru, el utilizează mecanismul tabelelor de import furnizate de formatul fișierelor Windows PE. În practică, aceasta înseamnă că poziția corespunzătoare din VMT va indica un fragment de cod (thunk) cu următoarea formă: