Az Object Pascalban az objektumok lehetnek Object vagy Class típusúak. Az Object típust a régi programokkal való kompatibilitás megőrzése végett benne hagyták a Delphiben, de a Class típus használata lényegesen több lehetőséget biztosít a programozónak. Míg az Object típusú objektumok statikus helyfoglalásúak, és lehetnek önállóak is (tehát nem származtatott), addig a Class típusú objektumok dinamikusan jönnek létre, és mindegyiknek van közös őse, a TObject. Az osztály típusú változók mindig pointerek egy objektumra, a címfeloldás automatikus. Az Object típus kompatibilitási okokból maradt a nyelv része, ezért használata nem ajánlott.
Konvenció, hogy a típusnevek „T”-vel kezdődnek, és a szóösszetételnél nagybetűt írunk (pl.: TMainForm).
Osztálydeklaráció csak a program, unit és library deklarációs részében adható meg, eljárás vagy függvény deklarációs részében nem. Az osztály deklarációját a class kulcsszó vezeti be, ezután adjuk meg először az adatmezőket, majd a metódusok fejlécét. A metódusok törzse nem adható meg az osztálydeklarációban, így előzetes deklarációként működnek.
Ezután (a unit implementation részében) adjuk meg a metódusok törzsét, az osztály nevét ponttal elválasztva a metódus nevétől.
Ezután létrehozhatjuk az objektum példányaira mutató pointereket.
Az objektumpéldányok használata esetén először a konstruktort kell meghívni, és ha már nincs szükségünk az adott példányra, fel kell szabadítani az általa lefoglalt helyet a Free metódus meghívásával. (A Free metódus csak akkor hívja meg a destruktort, ha az objektum nem nil, ellenkező esetben nem csinál semmit.)
Az objektumpéldány adatmezőire és metódusaira történő hivatkozásnál a ' . ' és a with kulcsszó használható, hasonlóan a rekord mezőire történő hivatkozáshoz. Itt azonban nem használhatjuk az xy^.___ formátumot.
Az Object Pascal osztálydeklarációja három különböző részt tartalmaz: az
adatmezők, a metódusok és a jellemzők megadása.
Az adatmezők olyan adatelemek, amelyek az adott osztály minden példányában megtalálhatóak. Ezek deklarálása és kezelése ugyanúgy történik, mint a rekord mezőinél.
A metódusok az adott osztályhoz kapcsolódó művelteket tartalmazzák. Ugyanahhoz az osztályhoz tartozó objektumpéldányok közösen használják a metódusokat, ezért minden metódus rendelkezik egy Self paraméterrel, amely megmutatja, hogy a változtatásokat melyik példány adatain kell elvégezni. Az osztályok két kitüntetett metódussal rendelkeznek, amelyek a konstruktor és a destruktor. A konstruktor tartalmazza az objektum létrehozásával és inicializálásával kapcsolatos műveleteket. A konstruktor függvényként viselkedik, visszatérési értéke az éppen létrehozott objektumpéldány címe. A destruktor pedig az objektumpéldány megszüntetésével kapcsolatos műveleteket tartalmazza. Ha nem írunk destruktort, akkor a Free metódus meghívásakor a TObject destruktora aktivizálódik.
A Delphiben ezen kívül léteznek olyan metódusok is, amelyek nem az objektum példányain fejtik ki a hatásukat, hanem magán az osztályon. Ezeket nevezzük osztálymetódusoknak. Deklarálásakor a metódus elé a class szó kerül (class function ...). Az osztálymetódusokban is van Self paraméter, de az nem osztály, hanem osztályreferencia típusú.
A Windowsszal való szorosabb együttműködés érdekében készíthetünk speciális üzenetkezelő metódusokat. Az üzenetkezelő metódus mindig egy eljárás (procedure) egy var paraméterrel, amelynek típusa valamelyik Windows-üzenetet leíró rekord típus (ezek a rekordok a Windows unitban találhatók). Példa üzenetkezelő metódus deklarációjára:
Az üzenetkezelő metódus érdekessége, hogy virtuális üzenetkezelő metódusnál nem kell kiírni az override kulcsszót, sőt a paraméter típusának se kell ugyanannak lennie. Pusztán az üzenet kódja gondoskodik a megfelelő metódus felüldefiniálásáról. Az üzenetkezelő metódusokat a direkt hívás mellett meghívhatjuk a Dispatch metóduson keresztül is, amelynek paramétere az üzenetet tartalmazó rekord. Az ablak-alapú VCL osztályok (jellemzően a formok, valamint a komponensek nagy része) az üzenet-feldolgozó ciklusukban meghívják a Dispatch-et, így egyszerűen adhatunk hozzájuk új üzenetkezelő metódust.
A vizuális komponens alapú programozás egyik eszköze. Egy objektum állapotát és viselkedését tulajdonságai határozzák meg. Tulajdonképpen csupán egy név, amelyen keresztül adattagot érhetünk el közvetlenül, akár privátot is, vagy metódust hívhatunk meg. Például a dátum objektum egy tulajdonsága lehet:
Ennek megfelelően a Month tulajdonság olvasása esetén közvetlenül az FMonth adattagot kell olvasnia a kódnak, míg írás esetén a SetMonth eljárás hívódik meg. De lehetne másként is:
A write direktíva elhagyása esetén a tulajdonság csak olvasható lesz. Ugyan ritkán fordul elő, de technikailag csak írható tulajdonságot is készíthetünk a read direktíva elhagyásával. Tulajdonság lehet szerkesztés alatti és csak futási idő alatti. Published mögött deklarálhatóak a szerkesztési idő alattiak, melyek elérhetők futási idő alatt is, de mindenképpen írhatónak és olvashatónak kell lenniük. Míg a csak futási idő alattiak lehetnek csak olvashatóak is, és a public, private, protected láthatósági jellemzőket kaphatják. A published tulajdonságokat szerkesztési idő alatt az Object Inspectorban szerkeszthetjük (vizuális programozás), egy-egy osztály vagy komponens további tulajdonságairól a help fájlokban tudhatunk meg többet, hiszen a csak futási idejűeket az inspector nem listázza.
Nem mindig adhatók át paraméterként, hiszen nem változók, nem tartozik hozzájuk memóriahely. Nincs minden VCL osztálynak tulajdonsága, a TComponent és a TPersistent leszármazottai rendelkeznek velük leginkább. Egy dfm fájl (formleíró) nem más, mint a formon elhelyezkedő komponensek published tulajdonságainak összessége.
Összefoglalva a tulajdonság (property) egy jó OOP mechanizmus, az egymásba ágyazás ötletének jól kigondolt megvalósítása. Egy név rejti el az osztályinformációk elérésének implementációját. Egy publikus adattag helyett inkább írhatunk egy propertyt, ha egy privát adattagot elérhetővé akarunk tenni. Nincs szükség Get és Set metódusok írására sem. És ha ezek után módosítunk az implementáción és áttérünk a közvetlen adatelérésről a metóduson keresztülire, ez kívülről nem látható, és nem kell megváltoztatnunk a tulajdonságot használó sorokat. Az egységbe zárás maximális kihasználása.
Ha a felhasználó például rákattint egy komponensre, akkor az egy eseményt generál. De nem is kell hozzá felhasználó, a rendszer is generál bizonyos eseményeket. Kattintás, ráhúzás, billentyű lenyomás, fókuszelvesztés, ezek mind-mind események, úgy ahogy esemény lehet egy komponens tulajdonságának megváltozása, vagy válasz egy metódushívásra.
A Windows üzenetvezérelt. Azaz lépten-nyomon úgynevezett message-eket küldözget a különböző objektumainak, például az ablakainak is (WinSight). Az események technikailag akkor következnek be, amikor egy-egy neki megfelelő üzenet érkezik a komponenshez. Megfeleltetés mégsem adható köztük, hiszen nem kezelünk minden üzenetet eseménnyel, és nem minden eseményhez adható egyértelműen üzenet sem. (A Delphi közvetlenül támogatja a Windows üzenetküldési mechanizmusát, könnyedén küldhetünk magunk által definiált üzeneteket és lekezelhetjük azokat.) Gyakorlatban egy komponensnek küldött üzenet ahhoz az ablakhoz érkezik, amelyik a komponenst tartalmazza. Ennek megfelelően egy komponens eseménykezelője általában annak a formnak egy metódusa, amelyiken a komponens rajta van, és nem a sajátja. Ezt a technikát delegation-nek nevezik és a Delphi komponens alapú modelljének az alapja.
Fontos tudni, hogy az események is tulajdonságok. Egy komponens tulajdonságokon, úgynevezett esemény-tulajdonságokon keresztül kezeli az eseményeit. Tulajdonképpen metódusmutatók; ha az object inspectorban szeretnénk megváltoztatni az értéküket, azaz metódust rendelnénk egy eseményhez, akkor az a forráskódból megjeleníti az azonos metódus-mutató típusú metódusok neveit. Ennek megfelelően több esemény osztozhat egyazon metóduson, ha azonos típusú metódust várnának, vagy akár futási időben is váltogathatjuk, hogy egy eseményhez melyik metódust rendeljük.
Írjunk például egy TDate osztályt, mely rendelkezik a megfelelő adattagokkal (év, hó, nap), melyeket propertyken keresztül érünk el, melyek egy-egy eljáráson keresztül módosítják azokat (Set, Get), és írunk neki egy eseményt is, amely mindig kiváltódik, ha változtatunk a dátum értékén (meghívjuk a Set, Get valamelyikét):
Az FOnChange privát mezőhöz az OnChange propertyn keresztül férhetünk hozzá, mely TNotifyEvent típusú. Ezek után már csak egy egyszerű metódus megvalósítására van szükség, amely módosítás esetén csak akkor hajtja végre az FOnChange -hez rendelt metódust, hogyha már rendeltünk hozzá valamit. És a Get, Set metódusoknak ezt az DoChange -et kell hívogatniuk:
Ez egy sima osztály volt, a tulajdonságai public-ban voltak. Egy published tulajdonságokkal (eseményekkel) rendelkező komponens eseménykezelését még egyszerűbb megvalósítani, csak néhány klikk és kész.
Tömb típusú jellemzőt is meg lehet adni, de ez nem úgy viselkedik, mint a tömb típus. Az index intervallum helyén egy paraméterlistát kell megadni, az író és olvasó metódusoknak pedig fogadniuk kell ezt a paramétert. Ebből a szempontból hasonlít a C++ kerek zárójel operátor felüldefiniálására.
Egy tömbjellemző után megadhatjuk a default kulcsszót (osztályonként csak egyszer), ilyenkor a megjelölt tömbjellemzőt elérhetjük magának az objektumnak az indexelésével is (pl. List.Items[x] helyett List[x]).
Mint azt már láttuk, egy adott osztály objektumait konstruktorok segítségével hozhatunk létre. A konstruktor elkészíti az objektumot, és elvégzi annak inicializálását is. Delphiben egy osztályhoz több különböző konstruktort hozhatunk létre, ezek neve tetszőleges lehet, és akármennyi paraméterük lehet. Deklarálásukhoz a constructor kulcsszót kell használni. A TObject osztály (minden osztály őse) konstruktorának neve Create, általában érdemes ezt a nevet használni.
Hasonlóan egy osztálynak lehet saját destruktora is. Ezek célja az objektum által lefoglalt erőforrások felszabadítása. Az alapértelmezett destruktor a Destroy virtuális metódus, emellett persze más néven is létrehozhatunk destruktort a destructor kulcsszóval. Egy dologra azonban figyelnünk kell. Delphiben egy objektum felszabadítása általában nem közvetlenül a destuktorának meghívásával történik, hanem a Free metódus hívásával. Ez először ellenőrzi, hogy az aktuális objektum (Self) létezik-e, azaz a rá mutató pointer nil vagy sem. Ha létezik, akkor meghívja annak Destroy metódusát. (Lásd még a Free metódus definícióját lentebb.) Emiatt, ha mi nem definiáljuk felül a Destroy-t, akkor a Free hívásával az ősosztály Destroy metódusa hívódik meg. (Ez különösen akkor jelent gondot, ha az osztály felszabadítását nem mi végezzük, hanem a Delphi. Tipikusan egy formon elhelyezett komponenst maga a form fog megsemmisíteni, mielőtt ő is megsemmisülne. Ezt a problémát a Free felüldefiniálásával nem lehet megoldani, mert ő egy statikus metódus.)
A Free metódus definíciója:
A destruktoroknál meg kell említeni a következő problémát: minden létrehozott objektumot meg kell semmisíteni, de ha egy már megsemmisített objektumot akarunk megsemmisíteni, akkor az kivételt vált ki. (Ezt általában az objektumok egyszeri megsemmisítésének problémájaként emlegetik.) Azt az előző bekezdésben már láthattuk, miért kell a Destroy destruktort felüldefiniálni. A másik lényeges dolog, hogy egy objektum felszabadítása után az objektumra mutató változót állítsuk nil-re. (Kivéve, ha a változó úgyis hatáskörön kívülre kerül.) Ha ezt nem tesszük meg, akkor egy esetleges újabb Free hívás esetén az meg akarja hívni a Destroy metódust, ez azonban kivételt vált ki, mert az objektum már megsemmisült. (Persze ilyet ritkán írunk, de – főleg bonyolultabb programoknál – mégis megtörténhet.) Felmerülhet a kérdés, hogy a Free metódus miért nem állítja nil-re az adott hivatkozást. A válasz egyszerű: mert nem tudja. A metóduson belül ismerjük az objektum memóriacímét (Self), de nem tudjuk a rá hivatkozó változó memóriacímét. Emiatt nem is tudjuk megváltoztatni annak értékét.
A Delphiben két osztályoperátort használhatunk, melyek arra szolgálnak, hogy elkerüljük a direkt típuskonverzióból származó hibákat. Ezt futásidejű típusinformáción (RTTI) alapuló technikával valósítják meg, melynek lényege, hogy minden objektum ismeri saját és szülőjének típusát, és ezek az információk lekérdezhetők. A két operátor a következő:
is: ez az operátor
dinamikus típusellenőrzésre szolgál, segítségével lekérdezhető, hogy
egy adott típusú objektum a megadott osztályhoz (vagy annak leszármazottjához)
tartozik-e.
Pl.:
as: típuskonverzióra
használható, egy adott objektumot úgy kezelhetünk, mintha a megadott
osztályhoz tartozna, ha nem sikerül a konverzió, akkor kivétel lép fel.
Pl.:
Bár a két RTTI operátor nagyon hasznos eszköz, ahol használatuk elkerülhető polimorfizmussal, ott inkább azt használjuk, egyrészt mert az elegánsabb, másrészt sokkal gyorsabb kódot eredményez, mert nem kell bejárni a teljes osztályhierarchiát.
Az Object Pascal objektumhivatkozási modellen alapul, melynek lényege, hogy egy adott osztály példányait tároló változók valójában nem magát az objektumot tárolják, hanem csak egy mutatót (hivatkozást), amely arra a memóriaterületre mutat, ami az objektumot valójában tárolja. Ennek a megoldásnak rengeteg előnye van, például a veremben (Call Stack) csak a hivatkozás számára kell helyet foglalni, maga az objektum a dinamikus memóriában (Heap) tárolódhat.
A fent leírtak miatt, amikor egy változót deklarálunk, nem készül el az objektum a memóriában, csak maga az objektumhivatkozás. Az objektumok példányait nekünk kell létrehoznunk valamelyik konstruktorának meghívásával. Ha mi hoztunk létre egy objektumot, akkor azt nekünk kell felszabadítanunk is. Ezeket a lépéseket láthattuk a fejezet bevezető példájában is. Ugyancsak a modell következménye, hogy amikor egy objektumnak értékül adunk egy másikat, akkor a Delphi csak a hivatkozást másolja (nem a teljes objektumot). Ahhoz, hogy az objektumot valójában megkettőzzük, létre kell hoznunk egy új objektumot, majd az összes mezőt át kell másolnunk. Erre a másolásra a VCL legtöbb osztályának van egy Assign nevű metódusa.
Alapértelmezésben az adatmezők, metódusok korlátozás nélküli hozzáférhetőségűek. Azonban a Delphiben azt az elvet próbálták követni, hogy az adatmezők csak a metódusokon keresztül legyenek láthatóak a külvilág számára. Ennek érdekében az objektumok definiálását érdemes a unitok interface részében elhelyezni, és itt megadni a különböző láthatósági jogköröket, melyek a következők lehetnek:
A private és protected direktívák szokatlan láthatósági szabályai miatt az újabb verziókban bevezették a strict private és strict protected kulcsszavakat, amelyek az osztályt deklaráló egységben levő, az osztályhoz nem tartozó dolgoknak letiltják a tagokhoz való hozzáférést.
Az osztályokban a különböző jogkörök megadása tetszőleges számban és sorrendben előfordulhat.
Amikor a Delphi formot készít, a saját metódusainak és komponenseinek definícióit a formosztály elejére teszi, még a public és private kulcsszavak elé. Ezek a mezők és metódusok publishedek (mivel ez az alapértelmezés). Igaz, hogy az osztálydefiníció későbbi részeiben is elhelyezhetünk újabb published mezőket, azonban az Object Inspectorban csak azok a metódusok és komponensek jelennek meg az egyes listákban, melyeket a kezdeti published részben deklarálunk.
Egy objektumtípushoz több objektumpéldány is tartozhat. Objektumpéldány deklarálására nincsenek megkötések, ezt bárhol megtehetjük. Minden példány rendelkezik saját adatterülettel, a metódusokat azonban közösen használják. Azt hogy az adott metódus éppen melyik példányon fejti ki hatását, a metódus nem látható Self paramétere mutatja. Az objektum példányairól fontos tudni, hogy a hagyományos memóriafoglaló és - felszabadító műveletek (new, dispose) nem használhatóak.
Az Object Pascalban minden osztálynak egy közös őse van, a TObject osztály. Ha nem adjuk meg az adott osztály ősét, akkor ez automatikusan a TObject közvetlen leszármazottja lesz.
A származtatott osztály az ősosztály minden tulajdonságát örökli, ezek azonban felüldefiniálhatóak, valamint az új osztályhoz adhatunk újabb mezőket és metódusokat is.
Nincs lehetőség a többszörös öröklődésre, azaz minden osztálynak csak egyetlen közvetlen őse lehet. Egy osztályra megtilthatjuk a származtatást, ha class sealed-ként deklaráljuk (v.ö. Java: final class).
Osztály származtatása esetén ügyelni kell arra, hogy az új osztály konstruktora elvégezze az öröklött mezők inicializálását is. Ezt megtehetjük az örökölt konstruktor meghívásával is. Ha a közvetlen ős osztály metódusait szeretnénk használni, ezt megtehetjük úgy is, hogy a metódus neve elé az inherited kulcsszót írjuk.
A származtatott objektumpéldány helyettesítheti az ős objektumpéldányt, viszont fordított esetben ez nem tehető meg.
A Pascal függvényei és eljárásai általában statikus (vagy más néven korai) kötésűek. Ez azt jelenti, hogy az adott hívást a fordító vagy a szerkesztő (linker) oldja fel, mégpedig úgy, hogy felcseréli azt egy (a metódus címére utaló) hívással. Az objektumorientált nyelvek egy másfajta kötést is támogatnak, az úgynevezett dinamikus (vagy más néven késői) kötést. Ebben az esetben a meghívott metódus címe csak futási időben derül ki. Ennek a technikának az előnyét nevezzük polimorfizmusnak. A polimorfizmus azon alapul, hogy mikor egy adott változón (pontosabban a változó által hivatkozott objektumon) hajtunk végre metódushívást, az, hogy valójában melyik metódust hívtunk meg, a változó által aktuálisan mutatott objektum típusától függ. Mivel egy osztálytípusú változó az adott osztály minden öröklött osztályát is tartalmazhatja, a Delphi csak futás közben tudja eldönteni, hogy a változó által mutatott objektum melyik osztályba tartozik.
Nézzünk egy példát:
A függvények megvalósítása:
Ezen deklarációk után a következő programsorok érvényesek:
Delphiben a dinamikus kötésű metódusokat a virtual vagy a dynamic kulcsszóval kell definiálni. A két kulcsszó hatása ugyanaz, különbség a késői kötés megvalósításában van.
Virtuális metódusok esetén a késői kötés egy virtuális metódus táblán (VMT, vagy vtable) alapulnak, mely nem más, mint egy metóduscímeket tartalmazó tömb. Virtuális metódus hívásakor a fordító olyan kódot generál, amely az objektum virtuális metódus táblájának n-edik rekeszében lévő címre ugrik. (n értéke az adott osztály őseitől és a metódusok számától függ) Ez gyors metódushívást tesz lehetővé (persze lassabb, mint a statikus kötés), azonban minden leszármazott osztály minden virtuális metódusa elfoglal egy helyet a VMT-ben, még akkor is, ha nem definiáltuk felül. Az azonos metóduscímek sokszori eltárolása pedig sok memóriát igényelhet.
A dinamikus metódushívások az adott metódusra jellemző, egyedi érték alapján működnek. Ennek előnye, hogy csak akkor kell eltárolnunk egy metódus bejegyzést a leszármazottban, ha az felüldefiniálja az adott metódust. Hátránya viszont, hogy a megfelelő metódusmutató megkeresése az osztályhierarchiában általában lassabb, mint a VMT egy elemének kivétele, így a dinamikus metódushívás lassabb a virtuálisnál. A metódusok külön vannak felsorolva, és ez a lista csak az újonnan bevezetett vagy felüldefiniált metódusokat tartalmazza, az örökölteket nem.
Hívás: VMT-ben keres, ha nincs, a dynamic listában keres, ha nincs, ősének dynamic listájában keres, és így tovább (ez a lassabb végrehajtás oka).
Az, hogy mikor melyik technikát alkalmazzuk, ránk van bízva, de érdemes néhány dolgot betartanunk:
Láttuk, hogy késői kötés esetén az override kulcsszóval írhattunk felül metódusokat. Ezt azonban csak akkor tehettük meg, ha az ősben virtuálisnak (virtual) vagy dinamikusnak (dynamic) definiáltuk. Az ősben statikusként definiált metódusok esetén (az ős megváltoztatása nélkül) nincs mód a késői kötésre.
A felüldefiniálás szabályai a következők: egy osztály statikusan definiált metódusa minden leszármazottjában statikus lesz, egészen addig, míg el nem fedjük egy ugyanilyen nevű virtuális metódussal. (Ezután az ebből származó osztályokban már virtuális lesz, ám azt ne felejtsük, hogy ha egy korábbi ősosztály típusú változónak hívjuk meg az ilyen nevű metódusát, akkor az eredeti statikus metódus fog meghívódni.) A virtuálisnak definiált metódusok azonban késői kötésűek maradnak minden leszármazottban, ezt nem lehet megváltoztatni.
Statikus metódus újradefiniálásakor az alosztályban létre kell hozni egy ugyanilyen nevű metódust, amelynek a paraméterezése megegyezhet az eredetiével, de el is térhet attól. Virtuális metódusok esetén azonban a paraméterezésnek meg kell egyeznie, és használnunk kell az override kulcsszót. Annak, hogy a Delphiben bevezették ezt a kulcsszót (sok nyelvben nem kell kiírni, pl. C++), két oka van:
Ha elfelejtjük kiírni az override kulcsszót, akkor a fordító figyelmeztetést ad, hogy a metódus elrejti az ősben levő virtuális metódust. Ha viszont pont ez a szándékunk, akkor a reintroduce kulcsszóval jelezhetjük, így nem kapunk figyelmeztetést. Ugyanerre a kulcsszóra van szükség, ha egy virtuális metódust egy leszármazott osztályban szeretnénk túlterhelni (az overload kulcsszóval együtt használva).
Sokszor előfordul, hogy egy virtuális metódust csak azért definiálunk, hogy az osztálydeklaráció teljes legyen, viszont értelmes törzset nem tudunk adni neki. Ilyenkor használhatjuk az absztrakt metódusokat, melyeket az abstract kulcsszóval definiálhatunk:
Ezek teljes metódusok, nem előzetes deklarációk. Ha megpróbálunk egy ilyen metódusnak definíciót adni (kifejteni a metódust), akkor a fordító hibát jelez. Object Pascalban lehetőség van absztrakt metódussal rendelkező osztályból példányt létrehozni, bár a 32 bites Delphi fordítója ad egy figyelmeztető üzenetet. (Például C++-ban absztrakt osztályból – amelyeknek van tisztán virtuális, azaz absztrakt metódusa – nem lehet egyedeket létrehozni.) Ha le akarjuk tiltani a példányosítást, akkor használhatjuk a class abstract kulcsszót. Ha egy absztrakt metódust meghívunk, akkor a Delphi futási hibát generál, és leállítja az alkalmazást. (Ez egy olyan súlyos hibának számít, hogy nem is keletkezik belőle – lekezelhető – kivétel.) Ezeket a metódusokat a származtatott osztályban mindenképpen újra kell definiálni. Absztrakt metódus csak virtuális vagy dinamikus metódus lehet.
Delphiben nincs az lehetőség az osztályok közötti többszörös öröklődésre, de az interfész segíthet, mert egy objektum több felületnek megfelelhet. Az objektumnak meg kell felelnie az interfész elvárásainak, de ha többet is tud, az nem baj.
Az interfész egy kezelő felület, amelyen keresztül egységes módon használhatunk különböző objektumokat. Tulajdonképp egy kizárólag virtuális függvényeket tartalmazó absztrakt osztályhoz hasonlítható, amelyet a felületnek (interfésznek) megfelelő osztályoknak meg kell valósítaniuk. A felületek nem tartalmaznak tagváltózókat. A felületek valójában nem osztályok, bár nagyon hasonlítanak rájuk. Teljesen önálló elemekként kezelhetjük őket, önálló tevékenységi körrel:
Minden felület egy Globálisan egyedi azonosítóval rendelkezik (GUID, Globally Unique IDentifier), amely a felület deklarációjában szerepel. Ezt a Delphi szerkesztőben Ctrl+Shift+G billentyűk lenyomásával állíthatjuk elő automatikusan. E nélkül is lefordul persze, de erre szükség van a felületek lekérdezéséhez és a dinamikus as átalakítás használatához - a felületek előnye éppen az, hogy futási időben rugalmasan kezelhetjük az objektumainkat. A GUID azért került be az interfész nyelvi elemei közé, mert elsősorban a COM (Component Object Model) interfészeinek Delphiben való felhasználására hozták létre ezt a típust, a COM-ban pedig minden interfész rendelkezik egy GUID-dal.
A felület használata: a felületet megvalósító osztály létrehozásával. Amikor egy osztályt deklarálunk, az ősosztály megadása után felsorolhatunk egy vagy több interfészt, amelyet meg szeretnénk valósítani. A fordító nem fogad el olyan osztályt, amely csak egy interfészből öröklődik, de ez megkerülhető, ha a TObject-ből származtatunk.
Egy interfész implementálásakor nem csak az interfészt valósítjuk meg, hanem egy különleges osztályt is, amely az IInterface által megkövetelt alapvető feladatokat végzi el (implementálja az IInterface-t). Ez a TInterfacedObject osztály. A GUID-hoz hasonlóan ez a követelmény a COM-mal való együttműködést szolgálja. Az IInterface a COM-beli IUnknown interfésznek felel meg.
A felületek tagfüggvényeit statikus vagy virtuális tagfüggvényekkel valósíthatjuk meg az osztályban. A származtatott osztályokban a virtuális metódusokat az override kulcsszóval bírálhatjuk felül. Ha statikus metódusokat szeretnénk felüldefiniálni, akkor újra be kell vezetnünk a felület típust a leszármazott osztályban és a tagfüggvények új verzióit rendelni a felület függvényeihez.
Ez után példányosításkor az objektumot egy felületváltozóhoz rendeljük (nem kell objektum változó) és azon keresztül használjuk.
Értékadáskor a Delphi automatikusan ellenőrzi, hogy az objektum megvalósítja-e a kérdéses felületet (az as művelet segítségével). Tulajdonképpen a fenti értékadás helyett ezt is írhattuk volna:
Mindkét esetben a Delphi meghívja az objektum _AddRef függvényét, amely megnöveli a hivatkozások számát. Amikor viszont a Flyer1 változó hatályát veszti, akkor a _Release tagfüggvényt hívja meg, amely csökkenti a hivatkozások számát, és ha az eléri a 0-t, akkor megsemmisíti az objektumot. Vagyis a Delphi nyilvántartja, hogy hány interfész változó kapcsolódik az objektumhoz, és ha már egy sem, akkor felszabadítja (hisz ekkor már nem érhető el az objektum és nem is tudnánk használni).
Figyelem!
Ha felület alapú objektumokat használunk, akkor tanácsos csak
objektumváltozókkal vagy csak felületváltozókkal kezelni azokat. A két
módszer együttes használata megzavarja a Delphi hivatkozásszámlálóját,
és memóriahibákhoz vezethet.
Bizonyos szintig az interfészeket is lehet polimorfizálni. Lehetőség van olyan függvény írására, amely interfészeken végez műveleteket.
Amikor paraméterként adunk át egy interfészt, akkor a Delphi automatikusan növeli, és utána csökkenti a hivatkozásszámlálóját. Ha ilyenkor nem megfelelően van inicializálva ez a számláló, akkor abból hiba lehet. Leggyakoribb hiba, amikor csak felveszünk pl. egy TAirplane típusú váltózót, ilyenkor a hivatkozásszámláló nulla marad, a függvény hívásakor 1-re nő, a végén pedig nulla lesz, és automatikusan meghívódik a felszabadító funkció (az interfész felszabadító művelete). Ez által az eredeti objektum törlődik (felszabadul), és a program további részén már nem használhatjuk.
Legyen egy másik interfész is az állatokhoz:
Ami ebben az érdekes, az az, hogy vehetünk egy emlőst (TMammal alosztály), amely megvalósítja az ICanFly interfészt is, mint pl. egy denevér.
Példa a használatra:
Ugyanez akkor is működik, ha a Denevért IMammal-ként vesszük fel, a következő módosítással:
Itt használjuk ki, hogy az objektum mindkét interfésznek megfelel és az as utasítással jelezzük is. Az objektumokat konténerekben ill. listákban tárolva is használhatjuk. Ilyenkor az elemek különböző típusúak lehetnek, és nem biztos, hogy megfelelnek a felület elvárásainak, mégis szeretnénk pl. egy egyszerű ciklussal végigmenni rajtuk és futtatni egy műveletet (amit az objektum típusától függően vagy lehet, vagy nem).
Ha meg akarjuk hívni egy ciklusban a Fly vagy FlyAway metódust az összes ICanFly-t implementáló objektumra, akkor gond lehet. Közvetlenül nem hívhatjuk meg a Fly metódust, hisz van olyan elem a listában, amely „nem tud repülni”, és ha az as konverziót használjuk, akkor lehet, hogy nem sikerül és kivétel keletkezik ("Interface not supported").
Ellenőriznünk kell, hogy az interfész támogatva van-e. Ehhez az interfész QueryInterface metódusát használhatjuk (minden interfész típusnak van ilyen, lásd fent a TInterfacedObject leírását). Ennek két paramétere van: a konvertálandó interfész típus, és az eredmény interfész.
Az Assigned(Fly1) meghívásával is ellenőrizhetjük a QueryInterface metódus hívásának eredményét, de a függvénynek van visszatérési értéke, amely szintén jó erre. Amennyiben a QueryInterface megtalálja az interfészt, akkor nullát ad vissza, amennyiben nem, akkor egy hibakódot (E_NoInterface). Ezt kihasználva így is átírhatjuk a fenti kódot:
Ebből láthatjuk, hogy az interfészek használatának számos előnye van. Vannak viszont olyan alkalmazások (pl. DirectX használata), ahol kikerülhetetlen az interfészek használata.
A Delphi minden verziója támogatta a futásidejű típusinformációk lekérdezését, sajnos azonban a nyelv ezen része dokumentálatlan maradt. Remélhetőleg ezen leírás elolvasása után az érdeklődőknek lesz egy alap fogalmuk a RTTI-ben rejlő lehetőségekről.
Magyarra fordítva futásidejű típusinformáció, azaz reflection. Aki más nyelvekből már ismeri a fogalmat annak a következő mondatok nem tartalmaznak újat, aki viszont nem ismeri, annak álljon itt egy rövid összefoglalás a technológiáról, a Delphi szemszögéből nézve.
Az RTTI egy adatstruktúra amit a fordító generál a program fordítása során. A cél, hogy legyen elérhető többféle típus információja futási időben is. Alapesetben ezek az információk a fordítóprogram számára hozzáférhetőek fordítási időben. Az RTTI-vel viszont a programozó is hozzáférhet ezen adatokhoz a program futása során.
Az RTTI jelenlétének első kulcsszavai – amiket valószínűleg már ismer az aki programozott Delphiben – az is és az as. Az is kulcsszóval futási időben vizsgálhatjuk az objektumok típusát, az as kulcsszóval pedig ellenőrzött típus-kasztolást lehet végrehajtani. Az RTTI-t használja ki az ObjectInspector is, az osztályok published részében deklarált mezők információinak lekérdezésekor. Ebben a leírásban ennél mélyebbre ásunk...
Ha meghívjuk egy osztály ClassInfo metódusát akkor visszakapunk egy pointert egy RTTI blokkra. Minden egyéb típust – amihez a fordító képes információt generálni – a TypeInfo függvény paraméterének átadva megkaphatjuk ezeket az információkat. A TypeInfo függvényt meghívva a fordítóprogram legenerálja az információs blokkot. Ha a paraméter egy osztály akkor ez a blokk eleve létezik a lefordított bináris állományban.
Típusok amiről a fordító nem képes információs blokkot generálni: mutatók, eljárások lokális típusai. A tömbök, rekordok, interfészek Delphi 3-tól már támogatottak. Delphi 4-től a 64bites integer és a dinamikus tömbök is bekerültek. Típusinformáció akkor generálódik ha a forrásban van egy hivatkozás a TypeInfo függvényre az adott típussal, vagy ha a típusra hivatkozás történik valamelyik osztály published szekciójában. Egy osztály ClassInfo metódusát meghívva ugyanaz játszódik le, mint ha átadnánk az osztályt a TypeInfo paramétereként, az eredmény mindkét esetben egy pointer a megfelelő RTTI blokkra. A különbség, hogy a TypeInfo fordítási időben fut le, ezért csak ismert típusokra alkalmazható, tehát osztály referencia nem adható meg.
Mivel a fentebb említett függvények egy PTypeInfo típusú mutatót adnak vissza, az RTTI blokk használatához ismernünk kell annak szerkezetét. A TypeInfo unitban van definiálva a TTypeInfo nevű rekord, aminek a szerkezete:
A Kind mező a TTypeKind felsorolási típus egy elemét tartalmazza, ez írja le, hogy a vizsgált típus milyen fajtájú. Egy pár lehetséges értéke:
tkInteger | Egész típus |
tkFloat | Minden lebegőpontos típus a Real-t kivéve |
tkString | Régi típusú sztring például String[10] |
tkClass | Osztály típus |
tkMethod | Eljárás vagy függvény metódus típus |
tkArray | Tömb típus (Delphi 3-tól) |
tkRecord | Rekord típus (Delphi 3-tól) |
tkInterface | Interfész típus (Delphi 3-tól) |
A rekord long, string valamint variáns típus esetén nem tartalmaz további információt. Float esetén további altípusba való tartozást is megállapíthatunk. Rövid sztringekre a maximum hossz van letárolva, diszkrét/osztály/metódus/interfész típusokra további információkat is megtudhatunk.
Koncentráljunk most az osztálytípus esetén visszaadott információkra. Ebben az esetben találunk egy újabb PTypeInfo típusú bejegyzést ami az ős adatai tartalmazza, a published szekcióban deklarált tulajdonságok számát és a nevét a unit-nak amiben az osztály deklarálva lett. Ez megint csak olyan méretű, hogy beleférjen a sztring, ezért van kommentben a következő mező ami az összes tulajdonságról tartalmaz információt. A szerkezete:
Jelen esetben a kikommentezett mezőhöz hozzáférhetünk, hiszen csak azt jelzi, hogy az előtte lévő mezőnek milyen is a szerkezete valójában.
A rekord főbb mezői tartalmaznak mutatót a tulajdonság típusának RTTI blokkjára, a tulajdonság getter és setter metódusaira, és nevére. A metódus-típust leíró blokk szintén kielégítő mennyiségű információt tartalmaz. Megtudhatjuk, hogy a metódus eljárás vagy függvény, és hogy hány paramétere van. A kikommentezett mező pedig a paraméterekről tájékoztat, kitérve a var vagy const módosítóra, a paraméter nevére illetve típusára. A szerkezete:
Ezen információk birtokában már érthető, hogy az ObjectInspector hogyan is végzi a munkáját. Azért tudja megkönnyíteni a fejlesztő dolgát, mert minden részletet ismer. Gondolom szintén nem tűnik mágiának többé az Events fül működése sem. Látható, hogy a TTypeData rekord ellát minket minden szükséges információval, csak győzni kell kibányászni a releváns adatokat a trükkös – változó hosszúságú sztringeket tartalmazó – struktúrából.
Most nézzük meg, hogy a TypeInfo unit milyen függvényekkel segít a fentebb részletezett struktúrák kezelésében.
Ezek a függvények egy PTypeInfo vagy PPropInfo típusú mutatót várnak paraméterként.
TTypeData típusú leírás lekérése:
Mivel a tulajdonságokon műveleteket végző függvények PPropInfo mutatót várnak, természetesen számos módja van, hogy szerezzünk egyet. A GetPropInfo egy TPropInfo-val tér vissza a paraméterben átadott nevű tulajdonság adataival feltöltve. A GetPropInfos pedig egy tömböt ad ebből a típusú rekordokból konstruálva. A GetPropList pedig a paraméterben meghatározott típusú tulajdonságok adatait és számát adja vissza.
TypInfo függvény | Típus amin használható |
GetOrdProp,SetOrdProp | sorszámozott vagy osztály |
GetStrProp, SetStrProp | sztringek |
GetFloatProp, SetFloatProp | lebegőpontos típusok |
GetVariantProp, SetVariantProp | variáns típusok |
GetMethodProp, SetMethodProp | metódusok |
GetInt64Prop, SetInt64Prop | 64-bites Integerek |
A Get/SetOrdProp egy objektum-referenciát és a tulajdonság információsblokkjára mutató pointert vár és Longintet ad vissza. A többi metódus hasonlóképpen működik, de azok a megfelelő típusú értékkel térnek vissza.