Objektum Orientált Programozás v0.7a
Hernyák Zoltán
E másolat nem munkapéldánya.
használható
fel
szabadon,
a
készülő
jegyzet
egy
A teljes jegyzetről, vagy annak bármely részéről bármely másolat készítéséhez a szerző előzetes írásbeli hozzájárulására van szükség. A másolatnak tartalmaznia kell a sokszorosításra vonatkozó korlátozó kitételt is. A jegyzet kizárólag főiskolai oktatási vagy tanulmányi célra használható! A szerző hozzájárulását adja ahhoz, hogy az EKF számítástechnika tanári, és programozó matematikus szakján, a 2001/2002-es tanévben a tárgyat az EKF TO által elfogadott módon felvett hallgatók bármelyike, kizárólag saját maga részére, tanulmányaihoz egyetlen egy példány másolatot készítsen a jegyzetből. A jegyzet e változata még tartalmazhat mind gépelési, mind helyességi hibákat. Az állítások nem mindegyike lett tesztelve teljes körűen. Minden észrevételt, amely valamilyen hibára vonatkozik, örömmel fogadok. Eger, 2001. szeptember 1. Hernyák Zoltán
1
Strukturált programozás Program = adat + algoritmus. A megoldandó probléma szerkezetét kell feltárni, és ezt kell leképezni a programra. Hierarchikus programozásnak is nevezték. A 60-as években Böhm és Jacopini kimondja sejtését, miszerint bármely algoritmus leírható az alábbi 3 vezérlési szerkezet véges sokszori alkalmazásával: • szekvencia • szelekció • iteráció Mills bebizonyítja, hogy minden program felépíthető a fenti 3 vezérlési szerkezetek felhasználásával úgy, hogy a program egységek szekvenciája, de egy egységen belül tetszőleges szerkezet lehet. Egy egységnek csak egy bemenete és egy kimenete van.
Objektumorientált programozás – OOP 1969-ben Alan Kay diplomamunkájában felvázolja, hogy hogyan kellene személyi számítógépeket készíteni, ezeknek a gépeknek mit kellene tudniuk. Az egyetem elvégzése után a Xerox cégnél helyezkedik el, ahol kidolgozza konkrét elképzeléseit a személyi számítógépről, és kitalálja az objektumorientált programozást, mint módszert. Ez 1972-ben történt: akkoriban még a lyukkártyás adatbevitel volt a legelterjedtebb. Kay szerint a személyi számítógépnek grafikus felhasználói felülettel kel rendelkeznie és a fő beviteli egysége az egér lenne. Leírja, hogy a felhasználó ikonokkal, menürendszerrel, ablakokkal dolgozik. Megtervez egy programozási nyelvet, Smalltalk-nak nevezi el. Ez az első és máig is létező objektumorientált programozási nyelv, amelynek napjainkban is készülnek újabb és újabb változatai, de az alapelvek mindvégig ugyanazok maradtak. A Windows 3.1ben már megtalálhatjuk Alan Kay elképzeléseit.
Objektumorientált programozási nyelvek 1980-as évektől az OOP az aktuális programozási filozófia. Több nyelv is megteremtette a lehetőséget az objektumorientált programozásra, azonban el kell különíteni a tisztán OOP nyelveket azoktól, amelyekben a szokásos eljárás-orientált programozás az alapvető, az objektumorientált programozás csak egy új eszköz, egy lehetőség, amit az alapdolgokon felül lehet használni. Ettől ezek a nyelvek még eljárás-orientáltak maradnak. Egy nyelvet OOP nyelvnek tekinthetünk, ha • támogatja az objektumok használatát, mint adatabsztrakció (pl. Modula 2., Ada) • minden objektum hozzá van rendelve egy objektum típushoz - osztályhoz. • az osztályok képesek örökölni az attribútumokat a szülőosztálytól • az objektumok üzenetekkel kommunikálnak egymással • az osztályokhoz rendelt metódusok az osztályra jellemző módon működik (akkor is, ha az egy örökölt metódus) • támogatja a metódusok címének dinamikus (futásidő alatti) meghatározását. Néhány tisztán OOP nyelv: • Smalltalk • Eiffel Nyelvek OOP eszközökkel kiegészítve: • C++ • Turbo Pascal • Object Pascal • OOP Cobol • Objective PL/1 • Lisp • ...
2
Objektumorientált fejlesztőeszközök • Delphi • VisualAge • Visual C++ • CLOS • ...
Turbo Pascal és az OOP kapcsolata Turbo Pascal 5.5 1989. május: Anders Heilsberg vezetésével a Turbo Pascal-t négy új alapszóval egészítették ki (object, constructor, destructor, virtual), amelyek segítségével lehetővé vált objektum-orientált programok írása. További érdekesség, hogy a help-ekbe az eljárások használatát bemutató példaprogramok kerültek, amelyek programírás közben könnyedén átmásolhatóak a forrásszövegbe. Turbo Pascal 6.0 A fordító beépített assemblert tartalmaz, a forrásszövegben elhelyezhetünk assemly nyelven megírt betéteket. Megjelent a PRIVATE kulcsszó, amivel megvalósíthetjuk az adatelrejtés elvét az objektumok belsejében. Az "Extended syntax" ({$X+}) bevezetésével feloldották a Pascal nyelvű fordítók hagyományos szigorúságát, használatával függvényeket meghívhatunk eljárásként amennyiben nincs szükségünk a függvény visszatérési értékére. Turbo Pascal 7.0 Megjelent a PUBLIC kulcsszó, melynek segítségével az objektum definiálása során lehetővé vált explicit módon a publikusság meghatározása, és lehetőség nyílt felváltva PRIVATE és PUBLIC részek deklarálása. Újdonság mindhárom IDE-ben a Syntax highlighting, ami a program elemeinek különböző színnel való kiemelésével gyors felismerést tesz .
3
Objektum-orientált programozás alapfogalmai Az objektumorientált programozás a természetes gondolkodást közelítő programozási mód, amelyet a programozás során a valódi világ hatékonyabb leírására használhatunk. Az objektumorientált módon megírt programok sokkal strukturáltabb, modulárisabb és absztraktabb, mint egy hagyományos módon megírt program. Ezek által egy OO program sokkal könnyebben bővíthető, karbantartható. Az OO nyelvet három fontos dolog jellemez: • Egységbezárás • Öröklés • Többrétűség Az objektumorientált programozásban az eljárásokat és a függvényeket közös, összefoglaló szóval metódusoknak hívjuk.
Egységbezárás (encapsulation) Az adatokat, és a hozzájuk tartozó metódusokat egy egységként kezeljük, és elrejtjük őket a külvilág elől. Ezek együtt képeznek egy objektumot. A metódusok implementációs része nem látszik kívülről, csak a specifikáció. Sőt, létezhetnek olyan metódusok is, amelyek egyáltalán nem látszódnak kívülről. Tehát egy objektumnak vannak attribútumai: adatelemekből áll, ezek az adatelemek valamilyen szerkezetet alkotnak és meghatározzák az objektum pillanatnyi állapotát és az objektumnak vannak metódusai, amelyek műveleteket tudnak végezni az objektum adatain. Ha kívülről valamilyen hatás éri az objektumot (üzenet érkezik), akkor az objektum metódusai megváltoztatják az objektum adatait, ezek a metódusok mondják meg, hogyan viselkedik az objektum a külső hatásra. Mindez a valós világ szemléletesebb leírását szolgálja. Egy objektum mindig ismeri az adatait, azokat kezelni (lekérdezni, megváltoztatni) csak az objektum saját metódusaival lehet (szabad). Egyes nyelveknél lehetőség van az objektum adatainak kívülről történő közvetlen megváltoztatására is (pl. Pascal), de kerüljük a használatát, mert nem igazodik a OOP elvekhez és később gondot okozhat. Az azonos attribútumokkal és metódusokkal rendelkező objektumok együttese az objektumosztály (vagy osztály). A programozás során nem az objektumokat (objektum példányokat) hozzuk létre először, hanem az osztályokat definiáljuk, és a meglévő osztályokból hozzuk létre az objektumpéldányokat. Tehát az adatok és a metódusok elsődlegesen az osztályhoz kötődnek. Amikor definiálunk egy osztályt, akkor megmondjuk, hogy annak az osztálynak milyen változói (mezői) lesznek, és ezeket a változókat milyen metódusok (eljárások, függvények) kezelik. Ezek után létrehozhatunk ebből az osztályból egy vagy több objektumot (példányt, konkrét változót), ezek mindegyike külön-külön tartalmazni fogja az osztályban deklarált adatszerkezetet és a hozzájuk tartozó metódusokat. Hasonló ehhez a Pascalban a típusdeklaráció és a változódeklaráció. Az osztály feleltethető meg a típusnak, a példány pedig a változónak.
Öröklés (inheritance) Egy definiált osztályból származtathatok egy másik osztályt úgy, hogy a leszármazott osztályban (alosztály) ugyanúgy megtalálható az ősosztály (szülőosztály, szuperosztály) összes attribútuma és metódusa. Az így létrehozott alosztály szintén lehet újabb osztálynak az őse. Az alosztályban módosíthatom az örökölt metódusokat, újabbakat tehetek melléjük és az örökölt adatszerkezetet is bővíthetem. Igazi OOP nyelv lehetőséget teremt örökölt metódusok hatásának felfüggesztésére, ki lehet jelölni, mely metódusokat akarom az örökléssel átvenni. Örökléssel kapcsolatot teremtünk két osztály között: az alosztály bővebb, mint a szülőosztály, mert az alosztály tartalmaz mindent, amit a szülőosztály és ezen felül még tartalmazhat mást is, amiket ebben az osztályban definiálunk. Egy ősből több leszármazottat is létrehozhatunk, amelyeket aztán különböző módon bővíthetünk. Ezek a kapcsolatok az osztályok egy hierarchiáját adják, amit egy irányított gráffal tudunk leírni, ahol a kapcsolat iránya mindig az alosztálytól mutat a szuperosztály felé.
4
Ha a nyelv a származtatásnál csak egy őst enged meg, akkor ez a hierarchia egy fastruktúrát alkot. Ezt a struktúrát öröklődési gráfnak ill. öröklődési fának nevezzük. A program tervezésénél kell megszervezni ezt a hierarchiát, figyelembe véve azt, hogy az öröklődési gráfban korábban szereplő lévő osztályok (az ősök), a rákövetkezőknek valamilyen általánosítása, hiszen bármely leszármazottra azt mondhatjuk, hogy ez olyan mint az ős, de egy dologban valami mást, valamivel többet tud. Ha így kezdjük felépíteni a hierarchiát, akkor ez egy felülről lefelé történő programozás: megfogalmazzuk a problémát általánosan, majd egy-egy dolgot konkretizálva egyre jobban eljutunk a speciális problémához; ezek a lépések tükröződnek az öröklődési gráfban. Ha felfelé mozgunk az öröklődési gráfban: általánosítás, ha lefelé: specializáció. Az általános problémaosztályoknak az az előnyük, hogy később, olyan speciális probléma megoldására is felhasználható, amely abból az általános problémából ered csak más specializáció során. Ez nagyon jól működik a OOP során, mert egy igazi OO nyelvben adott egy előre elkészített osztályhierarchia, és a programozó feladata annyi, hogy megkeressük a konkrét probléma megoldásához felhasználható osztályokat, és azokból újakat származtatva kiegészítem a szükséges attribútumokkal és metódusokkal a célnak megfelelően. Ez a kód újrafelhasználhatóságát jelenti, annak egy nagyon hatékony módszere.
Többrétűség (polymorphism) Ha származtatunk egy alosztály, akkor ez az alosztály örökli az ős összes metódusát. Ezeket a metódusokat megváltoztathatjuk, de a nevük ugyanaz marad: ugyanannak a metódusnak más-más osztályban más a viselkedése. Ha nem akarunk egy metódust megváltoztatni, akkor nem kell az alosztály deklarációjában felsorolni, ebben az esetben ez a metódus ugyanúgy fog viselkedni ebben az osztályban is, mint a szuperosztályban. Ha az alosztály egy példányánál hivatkozunk egy metódusra akkor két eset lehetséges: • •
ha a metódus szerepel az osztály deklarációjában, akkor egyértelmű, hogy az fog végrehajtódni. ha a metódus nem szerepel az osztály deklarációjában, akkor ez egy örökölt metódus, az öröklődési gráfban kell visszafelé megkeresni, hogy melyik szuperosztálynál történt a deklaráció, és az ott leírt kódot kell végrehajtani.
A második esetben felmerül a kérdés, hogy mi történik akkor, ha megtaláljuk az örökölt metódus leírását, és ebben szerepel hivatkozás egy osztálybeli B metódusra. Ugyanis a végrehajtás során ebben a pillanatban az ősosztály metódusában vagyunk, márpedig egy ősosztály nem ismeri a belőle származtatott osztályokat. Melyik B metódust kell végrehajtani? Az ős osztálybeli B metódust vagy a leszármazott osztály B metódusát? Ennek eldöntésére megkülönböztetjük a metódusokat, vannak statikus metódusok, amelyek címe már a fordításkor belekerül a lefordított kódba, ezt korai kötésnek hívjuk (early binding). Ebben az esetben az ősosztálynál leírt metódus az ősosztálybeli metódust hívja. A metódusok másik fajtája a virtuális metódusok. Ha a B metódust virtuálisnak deklaráljuk, akkor a fordító a metódusra való hivatkozáskor nem fordítja a kódba a metódus címét, hanem futásidőben megnézi, hogy melyik osztálytól jutott el az épp aktuális osztály kódjáig, és a hívó (tehát a leszármazott) osztály virtuális B metódusát fogja végrehajtani. Ezt, a futásidőben történő összerendelést késői kötésnek vagy késői összerendelésnek (late binding) nevezzük. Ezzel elérhető, hogy egy korábban definiált metódus olyasvalamit fog csinálni, amit csak egy későbbi osztályban írunk meg. Gondoljunk vissza a kód újrafelhasználhatóságára! Használhatunk mások által megírt kódot, a kód megváltoztatása nélkül, hiszen ha gondoltak arra, hogy később valami mást is kell majd csinálni, akkor meghagyták a lehetőséget úgy, hogy nekünk csak egy származtatott osztályban egy virtuális metódust kell átírni, és az osztálybeli objektum már úgy fog működni, ahogy azt mi szeretnénk.
5
OOP lépésről lépésre Első objektumunk - Verem unit Tegyük fel, hogy egy szoftverfejlesztő cégnél azt a feladatot kapjuk, hogy írjunk olyan programmodult egy nagyobb projekt kapcsán, melynek segítségével veremkezelést valósíthat meg a másik programozó kollégánk. A veremben egész számokat fog tárolni, és tegyük fel, hogy legrosszabb esetben sem többet, mint 100 db. A projekt fejlesztése Turbo Pascal-ban zajlik. Hogyan kezdenénk neki? Mivel modulról van szó, első ötletünk természetesen a unit. unit Verem1; interface procedure VeremInit; procedure Berak(X:integer); function Kivesz:integer; function VeremUres:boolean; function Veremtele:boolean; var vm : integer; T : array [1..100] of integer; implementation procedure VeremInit; begin vm:=0; end; procedure Berak(X:integer); begin if not VeremTeli then begin inc(vm); T[vm]:=X; end; end; function Kivesz; begin if not VeremUres then begin Kivesz := T[vm]; dec(vm); end else Kivesz := 0; end; function VeremTeli; begin VeremTeli := (vm=100); end; function VeremUres; begin VeremUres:= (vm=0); end; begin end.
6
A megoldásunk bár nekünk nagyon is tetszhet, nem biztos hogy osztatlan sikert arat a szóban forgó cég vezetősége körében. Nagyon sok baj van a megoldásunkkal, ennek ellenére többször is fogunk rá utalni, így hát nem végeztünk haszontalan munkát. Kezdjük sorban megtárgyalni a megoldásunk hibás pontjait:
Első hiba : biztos hogy ez jól működik ? Mivel ezen modult mi fejlesztettük, ezért ezen modul működéséért mi vállaljuk a felelősséget. De vajon merjük-e vállalni, hogy a verem valóban jól fog működni. Nagyon sok olyan tényező van, amely rajtunk kívülálló módon okozhat hibát a unit működésében. Az első az, hogy a „vm'' és a „T'' változók látszanak a uniton kívül is. Ez azért veszélyes, mert mi van akkor, ha programozó kollegánk hozzápiszkál ezen változókhoz? Ha elállítja a „vm'' változót, nem tudjuk garantálni, hogy valóban teljesülni fog a FIFO1 adatkezelési elv. Mellékes, bár fontos megjegyzés, hogy egyébként sem szerencsés unit-on belül olyan változót deklarálni, amely a külső modulok felé is „látszik'', mert ott akkor ugyanolyan nevű változók deklarálásával némi zavart idézünk elő. Mit tegyünk? A megoldás kínálja magát, tegyük át ezen két változó deklarációját az implementation szakaszba. Az ott deklarált változók tudottan nem látszanak a unit-on kívül.
Második hiba : ki fogja inicializálni a vermet? Ezek után a verem működését már csak egy módon lehet elrontani, ha a kolléga „elfelejti'' meghívni a VeremInit eljárásunkat, e módon meggátolva, hogy a veremkezelő eljárások (pl. Berak) működése garantált legyen. Mit tegyünk? A megoldás ismét kínálja magát. A VeremInit eljárást tegyük be a unit inicializációs részébe, a unit végén található begin ... end. közé. ... begin VeremInit; end. A unit végén lévő inicializációs utasítások a tényleges főprogram végrehajtásának elkezdése előtt hajtódnak végre. Így mire a kolléga kiadhatná az első utasítását, addigra a verem már alaphelyzetbe állt.
Harmadik hiba : csak egy verem lehet Amennyiben a programozó kollégánk több vermet is szeretne használni valamely, erre már nem kínálkozik megoldás, hiszen egy unitot csak egyszer lehet felhasználni egy másik modulban. Nem írhatja: uses verem,verem;. Mit kínálunk hát megoldásként? Némi töprengés után jöhet a megoldási javaslat: a verem unitot módosítjuk oly módon, hogy minden, a veremmel kapcsolatos eljárás kap még egy paramétert, egy rekordot.
1
LIFO : Last In, First Out: „Ami utoljára be, először ki'', a verem mint összetett adatszerkezet kezelési elve
7
unit Verem2; interface type RVerem = record VM : integer; T : array [1..100] of integer; end; procedure procedure function function function
VeremInit(var V:RVerem); Berak(var V:RVerem,X:integer); Kivesz(var V:RVerem):integer; VeremUres(var V:RVerem):boolean; Veremtele(var V:RVerem):boolean;
implementation procedure VeremInit(var V:RVerem); begin V.vm:=0; end; procedure Berak(var V:RVerem,X:integer); begin if not VeremTeli(V) then begin inc(V.vm); V.T[V.vm]:=X; end; end; function Kivesz(var V:RVerem); begin if not VeremUres(V) then begin Kivesz := V.T[V.vm]; dec(V.vm); end else Kivesz := 0; end; function VeremTeli(var V:RVerem); begin VeremTeli := (V.vm=100); end; function VeremUres(var V:RVerem); begin VeremUres:= (V.vm=0); end; end.
8
Vagy, a „with'' utasítás segítségével: unit Verem3; interface type RVerem = record VM : integer; T : array [1..100] of integer; end; procedure procedure function function function
VeremInit(var V:RVerem); Berak(var V:RVerem;X:integer); Kivesz(var V:RVerem):integer; VeremUres(var V:RVerem):boolean; Veremtele(var V:RVerem):boolean;
implementation procedure VeremInit(var V:RVerem); begin with V do begin vm:=0; end; end; procedure Berak(var V:RVerem;X:integer); begin with V do begin if not VeremTeli(V) then begin inc(vm); T[vm]:=X; end; end; end; function Kivesz(var V:RVerem); begin with V do begin if not VeremUres(V) then begin Kivesz := T[vm]; dec(vm); end else Kivesz := 0; end; end; function VeremTeli(var V:RVerem); begin with V do begin VeremTeli := (vm=100); end; end; function VeremUres(var V:RVerem); begin with V do begin VeremUres:= (vm=0); end; end; end.
9
De mi a helyzet az első és második hibával? El tudja rontani a kolléga a mi veremkezelési elveinket? El tudja rontani azzal az egészet, hogy „elfelejti'' inicializálni a vermet? Sajnos mindkettőre szomorú bólogatás a válasz. Mivel a vermet szimbolizáló rekordot e pillanattól kezdve neki kell deklarálnia a saját moduljában, így hozzáférhet azok mezőihez minden további nélkül. A másik hibával sem állunk túl jól. Ugyanis mivel a változó nem a uniton belül van deklarálva, így nem tudjuk a unit inicializálós részén alaphelyzetbe állítani. Program Gonosz_Programozo_Kollega_Foprogramja; uses Verem; var V:RVerem; begin V.vm := -1000; Berak(V,20); {"Run time error", :-] } end. Hibák még mindig vannak a unit-os megoldásban, de addig ne kezdjük őket sorolni tovább, amíg az eddig megadottakat meg nem oldjuk.
A megoldás - az objektum Mivel eluralkodik rajtunk a „csak azért is én vagyok a jobb programozó'' érzés, nem hagyjuk magunkat. Kezdjük el tanulmányozni, mit nyújt az OOP ilyen esetekre. Az első, amit megtanulunk, hogy egy objektumot hasonlóan kell deklarálni, mint egy rekordot. Ilyen módon hibátlan az alábbi deklaráció: type TVerem = object VM : integer; T : array [1..100] of integer; end; Ezzel lényegében már egy objektumot kaptunk (mint a neve is mutatja). Használni is tudjuk, amennyiben szükséges: var V:TVerem; begin V.vm := 0; V.T[1]:=100; end. Mint látjuk, az objektum ezen formájában semmiben nem más, mint egy egyszerű rekord. Az objektum mezőire is a minősítő operátor („.'') segítségével hivatkozhatunk (v.vm), sőt, a „with'' kulcsszót a Pascal kiterjesztette objektumokra is, így a fenti kis (bugyuta) példát az alábbi formában is írhattuk volna: var V:TVerem; begin with V do begin vm := 0; T[1]:=100; end; end.
10
Az objektumtípust (type ...=object) innentől kezdve objektum-osztálynak, vagy röviden osztálynak nevezzük. Az osztály a típus. A TVerem tehát egy osztály. Ez a fejlettebb OOP támogató nyelvekben már jobban látszik: a legtöbb nyelven az „object'' kulcsszó helyett a „class'' kulcsszót kell használni a típus definiálásakor. Pl. Delphi2-ben a fentieket így kellene írni: type TVerem = class VM : integer; T : array [1..100] of integer; end; Ettől még nem kerültünk közelebb a problémák megoldásához, de haladjunk tovább. Az object azonban nem egy alternatív szó a record -ra, hanem sokkal több annál. Egy objektum nem csak mezőket tartalmazhat, hanem eljárásokat és függvényeket is. Ezeket közös néven metódusoknak nevezzük. A metódusok fejrészét az osztály deklarálásának helyén kell megadni: type TVerem = class VM : integer; T : array [1..100] of integer; procedure Berak(X:integer); function Kivesz:integer; function Ures:boolean; function Tele:boolean; procedure Init; end; Ezzel egy kompaktabb deklarálást kapunk. Először is nem csak az látszik, hogy egy veremhez kell két változó (mező), hanem egy verem akkor teljes, ha a fentebb felsorolt 5 metódus is szerepel3. Az is látszik, hogy a veremmel kapcsolatban más művelet nincs! Hogyan kell ezek alapján a komplett unitot megírni? unit VeremOOP; interface type TVerem = class VM : integer; T : array [1..100] of integer; procedure Berak(X:integer); function Kivesz:integer; function Ures:boolean; function Tele:boolean; procedure Init; end; implementation procedure TVerem.Init; begin vm:=0; end;
2
A Delphi szintaktikája a Pascal-éval nagyon rokon sok helyen a verem-hez még egy műveletet deklarálnak, a function Teteje:integer függvényt is, amely megadja a verem tetején lévő elemet anélkül, hogy azt ki is venné a veremből
3
11
procedure TVerem.Berak(X:integer); begin if not Teli then begin inc(vm); T[vm]:=X; end; end; function TVerem.Kivesz; begin if not Ures then begin Kivesz := T[vm]; dec(vm); end else Kivesz := 0; end; function TVerem.Teli; begin Teli := (vm=100); end; function TVerem.Ures; begin Ures:= (vm=0); end; begin end. Mint látjuk, a Verem1 unithoz hasonló szerkezet kaptunk. Ami jól látszik, hogy a metódusok törzsének kifejtésekor a metódus azonosításakor az osztály neve, és a metódus neve együtt szerepel (pl. function TVerem.Kivesz). A metódusok belsejében további metódusok hívása szerepelhet: ilyen pl. a Berak-beli Teli hívása. Ez természetes az objektumok esetén, a Pascal tudni fogja, hogy a TVerem.Berak-beli Teli csakis a TVerem.Teli lehet4. A másik, ami látszik, hogy a metódusok belseje viszont leginkább a Verem1 unit-beli formákhoz hasonlít. Például a TVerem.Kivesz belsejében a „not VeremUres'' hívása megegyezik a Verem1beli móddal (nincs paramétere a VeremUres-nek). Nem esünk esetleg újra ugyanabba a csapdába, hogy nem lehet több vermünk? A választ az objektum használata során kapjuk meg. Először is fontos megértenünk, hogy a fenti deklaráció során még csak típust deklaráltunk. Vermünk még nincs, csak a lehetőség, hogy lehessen. Pont, mint pl. a rekordok esetén. A type RVerem = record .... end; deklarációval még csak egy típust kaptunk. Hogy konkrét rekordunk legyen ahhoz deklarálni kell egy rekord típusú változót. Hogy objektumunk legyen, deklarálni kell egy osztály típusú változót: var OOPVerem : TVerem; Mi a helyzet ezzel a változóval? Mint egy fentebbi példában láthattuk, ezen változó sok szempontból olyan, mint egy rekord, és ugyanúgy lehet használni is - el lehet érni a mezőit (vm.T) egyszerű módon. De mit tegyünk a metódusokkal? A válasz - ugyanazt, mint a mezőkkel. Ha egy ilyen veremmel dolgozni akarunk, akkor az alábbi formát használhatjuk: 4
mielőtt nagyon elbíznánk magunkat :-), ezzel még sok bajunk lesz, lásd öröklődés,virtuális metódusok fejezeteket
12
program OOPVerem_Teszt; uses Verem3; var OOPVerem : TVerem; begin OOPVerem.Init; OOPVerem.Berak(10); if OOPVerem.vm=1 then writeln('A verem jól működik !'); end. A metódusokra is a „.'' minősítő operátorral hivatkozhatunk, ezek segítségével hívhatjuk meg ezen eljárásokat, és függvényeket. Ha a fenti programban véletlenül olyan eljáráshívást írtunk volna, hogy Init;, akkor a Pascal egy egyszerű, hagyományos eljárást indított volna el, amely a procedure Init; formában került volna deklarálásra. A OOPVerem.Init; esetén a Pascal „megnézi'', hogy az OOPVerem valójában egy TVerem típusú objektum, így a fenti eljáráshívás a TVerem.Init hívásaként értelmezi. De mi a helyzet a TVerem.Init metódus belsejében lévő vm:=0; utasítással? Mi az a vm változó ilyenkor? A válasz: az OOPVerem objektumbeli vm mező. A módszer olyan, mintha a TVerem.Init megkapná paraméterként az OOPVerem változót cím szerint, és az eljárás belsejében with OOPVerem do begin sor is lenne5. Egyelőre fogjuk fel úgy, hogy a metódusok számára a világ az adott objektumra szűkül le, számára nem létezik más, egy metódus belsejében az objektum változói (mezői) olyanok, mintha globális változók lennének. Számára a vm egyértelműen az objektum mezőjét jelenti, és automatikusan tudja, melyik objektumét. Nézzünk egy újabb példát: var OV1,OV2:TVerem; begin OV1.Init; OV1.Berak(10); OV2.Init; end. Mint láthatjuk, az adott osztályból több objektumot is készíthetünk. A továbbiakban ezt példányosításnak nevezzük, az objektum-osztályból készült változót pedig objektum-példánynak, vagy röviden objektumnak nevezzük. Tehát van két objektumunk: OV1, OV2. Az OV1.Init metódus hívásakor az OV1 objektum vm mezőjét állítja nullára, az OV1.Berak metódus szintén az OV1 objektumon fog dolgozni, és az ő vm és T mezőivel végez munkát. Az OV2.Init viszont már az OV2 vm mezőjét állítja nullára. Ez talán már érhető is eddig (valójában, ha nem gondolkodunk el rajta, a fenti dolgok természetesek, és egyértelműek :-)). Gondoljuk azonban tovább. A „Berak'' metódus meghívja a „Teli'' metódust, hogy ellenőrizze, hogy a verem nem telt-e meg eddig. A „Teli'' metódus is a vm mező alapján dönt. De milyen vm alapján? A válasz természetes, ugyanazon objektum mezője alapján, így a OV1.Berak-beli Teli metódus hívása értelemszerűen megegyezik a OV1.Teli meghívásával, vagyis a Teli-ben a vm szintén az OV1.vm mező lesz6. Levonhatjuk a következtetést, a fenti mechanizmus automatikus, de mivel teljesen természetes, és logikus, nekünk nem nagyon kell vele foglalkoznunk.
5 6
valójában pontosan erről van szó, lásd a „SELF - láthatatlan paraméter'' című részt itt megint a „Self'' a kulcsszó, lásd a megfelelő fejezetet
13
Az objektum finomítása - adatrejtés Ha eddig értjük, térjünk vissza a megoldandó problémáinkra. Az egyik, hogy a gonosz programozó kollégánk ne tudja elrontani a vermünk működését azzal, hogy belepiszkál a mezőkbe. El kellene rejtenünk őket. Itt sajnos nem jöhet szóba az implementation rész, mint az elrejtés tipikus helye. Ugyanis az objektumosztály definiálását kénytelenek vagyunk a interface részben végezni, hogy a külvilág számára a típus elérhető legyen, és tudjon később példányosítani. Sajnos, a típus definiálását nem vághatjuk ketté, nem tehetjük meg, hogy az interface részben „elkezdjük'' a definiálást a publikus részekkel, majd az implementation részben folytatjuk a rejtett (privát) részekkel. Ez egyben ellentmondana annak az elvnek, hogy az objektum egyben tartalmazza az objektum adattároló részeinek (mező), és a rajta műveletet végző eljárások és függvények (metódusok) definícióit. A megoldás az, hogy az objektum-osztály definiálásakor közöljük, hogy mi az, amit a külvilág számára szánunk az objektumból, és mi az, amit nem: type TAdvVerem = object private VM : integer; T : array [1..100] of integer; public procedure Berak(X:integer); function Kivesz:integer; function Ures:boolean; function Tele:boolean; procedure Init; end;7 Mint látjuk, a private és public kulcsszavakkal jeleztük ezen szándékunkat. A private kulcsszó után következő objektum-részeket a külvilág nem érheti el, számukra az objektum úgy viselkedik, mintha nem is létezne vm és T mezője. A public után következő dolgok (jelen esetben metódusok) viszont hozzáférhetőek lesznek. E szempontból a public rész úgy viselkedik, mintha egy unit interface része lenne, a private-ban definiált dolgok viszont az implementation rész lenne. E miatt szokták a public részben definiált részeket az objektum interfészének is nevezni. Amit most kaptunk, az megfelel az egységbezárás elvének, sőt, annak megspékelt változatával, az adatrejtés elvével. Ezen elv szerint az objektum mezőit a külvilág sohasem érheti el, minden művelet az objektum metódusain keresztül végezhető el8. Lássuk, hol tartunk! Mit tehet nem túl szimpatikus programozó kollégánk most? Program Gonosz_Programozo_Kollega_Foprogramja; uses Verem; var V:TVerem; begin V.vm := -1000; { nem megy, szintaktikai hibát kap, a V objektumnak nincs vm mezője, részünkről a mosoly :-) } V.Berak(20); { hibás lehet, részéről a mosoly :-( } end.
7
Ez a forma nem működik a TP v6.0-ban, csak a TP v7.0-ban ! ezen elv túlzásba vitelével is sok a gond, a probléma legszebb megoldása a Delphi „property''-jei, lásd a megfelelő fejezetet
8
14
A V.Berak ponton még ő a jobb, sajnos, mivel nem inicializálta az objektumot, ezen a ponton még akár "Run Time Error"-t is kaphat a program. Sajnos egyelőre megteheti, hogy nem inicializálja az objektumot. ( )
Az objektum finomítása - constructor, destructor Hogyan kényszerítsük rá kollégánkat, hogy addig ne használja az objektumot, amíg azt nem inicializálta? Majd minden objektumnál probléma az inicializálás. Korán megtanultuk, hogy ne használjunk inicializálatlan változókat. Mivel majd minden objektumnak vannak mezői, ezért az objektumoknál ez állandó probléma, hogy ki és mikor fogja inicializálni ezen mezőket9. A fentiek megoldására egy speciális metódus-csoportot hoztak létre az OOP tervezői. Azon metódusok, amelyek elsődleges feladata az objektum-példány alaphelyzetbe hozása (inicializálása), konstruktor nevet kapták. A konstruktorok tehát közönséges eljárások, de speciális feladattal. Honnan ismerjük meg a konstruktorokat? Hogyan tudunk ilyeneket definiálni? type TAdvVerem = object private VM : integer; T : array [1..100] of integer; public procedure Berak(X:integer); function Kivesz:integer; function Ures:boolean; function Tele:boolean; constructor Init; { ez egy konstruktor !!!! } end; constructor TVerem.Init; begin vm := 0; end; Mint láthatjuk, a konstruktorokat könnyű megismerni, hiszen ezen metódus definiálásakor nem a procedure, hanem a constructor szó szerepel. Ennek ellenére a konstruktorok közönséges eljárások, használatukban legalábbis: begin OV1.Init; end. Hogyan tudjuk kikényszeríteni a konstruktor használatát? Sajnos, rossz hírem van a Pascal-rajongók táborának. Turbo Pascal-ban sehogy. De tisztább OOP-t használó nyelvekben (Java, Delphi, C++) igen! Ugyanakkor elmondhatjuk, hogy ez alapelv az OOP-ben, hogy minden objektumot használatba vétele előtt a konstruktorán keresztül inicializálni kell. Ha ezt nem teszi meg egy programozó, akkor ugyanolyan hibát ejt, mintha egy file-ból olvasna anélkül, hogy azt a file-t megnyitotta volna olvasásra. A virtuális metódusok10 használata esetén a kényszer erősebb! Ezen problémát ennyivel tudjuk jelenleg elhárítani magunktól! A konstruktorokon kívül a másik, jellemző feladat az objektumok használata befejeztével a „tisztogatás11'' műveletet elvégezni. Ezen metódusok, amelyek az objektum által lefoglalt erőforrásokat 9
ezzel kapcsolatban lásd a „nyelvi különbségek'' c. részt lásd később 11 clean-up 10
15
(memóriafoglalás, fileok bezárása, hálózati kapcsolatok megszakítása, stb...) szabadítják fel, közös néven desktruktoroknak nevezzük. A destruktorokat szintén könnyű felismerni, és készíteni, mivel ezen eljárások neve nem procedure, hanem destructor. A verem objektum esetén ilyen tisztogatásra nincs szükség, ezért a destruktorok bemutatására álljon itt egy másik példa: készítsünk olyan objektumot, amely egy text file kezelését valósítja meg a metódusain keresztül: type TTextFile = object private f : Text; public constructor Letrehoz(DosFileNev:string); procedure Kiir(S:string); destructor Lezar; end; constructor TTextFile.Letrehoz(DosFileNev:string); begin Assign(F,DosFileNev); Rewrite(f); end; procedure TTextFile.Kiir(S:string); begin Writeln(f,s); end; destructor TTextFile.Lezar; begin Close(f); end; Használata: var T:TTextFile; begin T.Letrehoz('C:\proba.txt'); T.Kiir('Egy próba kiírás a file-ba'); T.Lezar; end; A konstruktorok és destruktorok használata, használhatósága az erősebb OOP nyelveken (Java, Delphi) jobban kidomborodik. Később találkozunk még velük. Tegyük most félre az eddigi példáinkat, és kezdjünk el alaposan elmélyedni az OOP azon tulajdonságaiban, amelyek már messzebbre mutatnak náluk.
Az OOP nagy tulajdonsága - az öröklődés Az eddigiek is rávilágítottak az OOP néhány előnyére, de mindezen példák erősen építettek arra, hogy a kollégáink igen gonosz emberek, és ezért kell nekünk az OOP. Most lássuk, mit nyújt az OOP akkor nekünk, ha nem gonoszak a kollégák.
16
Az OOP kódtakarékos A cím azt sugallja, hogy az OOP során kevesebb programsort kell leírnunk ugyanazon problémamennyiség megoldásához. Az eddigi ismereteinkből erre semmi sem utal, a metódusokat ez előtt is, eztán is meg kell írnunk. Hogy lesz ebből kódtakarékosság? A válasz a tervezésben, és az öröklődésben rejlik. A tervezéstől minden vérbeli programozó megborzong (:-)). Egy igazi programozó nem tervezi a programját - legalábbis nem látszik. Egy profi úgy tűnik, a feladat megértésének pillanatában billentyűzetet ragad, és elkezdi a sorokat gyártani. Valójában ők is terveznek, a fejükben összeáll az algoritmus, részfeladatokra bomlik - kialakulnak a szükséges eljárások és függvények, átlátják, azoknak milyen paramétereik legyenek, hogy minél kevesebb betűt kelljen leütni ( :-) ). Hogy ők miért ilyen gyorsan látják át ezeket ? A legtöbbjük egyébként is átlag feletti zseni, de az ok mégis inkább az, hogy sok hasonló problémával találkoztak már, és az adott feladat rutinból megy nekik. Az OOP tervezés nem csak ennyi! Ezt fontos megérteni! Egy OOP program nem csak annyi, hogy egyszerű eljárások és függvények helyett most metódusokat írunk. Ha valakinek ez csak ennyi, akkor még nem OOP programozó, csupán az eljárások és függvények deklarálására, és aktivizálására kissé már szintaxist használ, mint a többiek. Az OOP programozás 80%-ban arról szól, hogy ne kelljen sokat programoznunk, mégis tökéletesen tesztelt, működőképes, stabil, robosztus programot kapjunk. Egy jól felépített OOP vázon ülő fejlesztő eszközön történő programírás inkább hasonlít a szövegszerkesztésre, a programozó (vagy inkább felhasználó) elmerült arccal kattintgat az egérrel, és aránylag kevés programsort ír le, akkor is leginkább már mások által elkészített, és tesztelt metódusokat hívogat. A fejlesztés ezen típusát, amikor a programozó elkészített objektumokat használ, és azok segítségével old meg feladatokat, időnként „drótozásnak'' is nevezik. A programozó feladata a különféle objektumokat (mint chip-eket) különböző metódusaik összekapcsolása révén működésre serkenteni (mint a chip-ek lábainak összekötése révén egy részegységekből összerakni egy komoly elektronikus eszközt). Egy igazi OOP rendszerben csak ritkán kell új objektumokat készíteni, a rendszerrel szállított, és utólag hozzáillesztett objektumok olyan tömeggel vannak, olyan sokoldalúak, olyan általánosak, hogy csak speciális igények kielégítése végett kell tényleges programozói munkát végezni. A Delphi-hez mellékelt objektumosztály-gyűjtemény (library) olyan bőséges, hogy egy nagy méretű (poszter nagyságú) lapon fér csak el a felsorolása - nem túl nagy méretű betűkkel írva. Az ilyen objektumok felhasználásával a fejlesztés és tesztelés ideje lerövidül - de ezért persze valahol meg kell fizetni: az ilyen programok sebessége sokszor nem túl optimális, és a készült program hossza is elég nagy lehet. Arról nem is beszélve, hogy a mellékelt objektum-library alapos ismeretén alapul – melyet megszerezni sok időt igényel. Az eddig leírtak szintén nem igényelnek feltétlenül OOP programozási technikát, ez eddig megoldható egy igen nagy méretű unit-gyűjtemény elkészítésével is. Miért jobb mindezt mégis OOP-ben készíteni? Hogyan segít más módon az OOP a programozás felgyorsításában, a kódolási idő lerövidítésében? Vegyünk egy újabb példát: készítsünk el egy objektumot, amely egy grafikus ponttal végez különböző műveletet. Kivételesen nem dolgozunk ki minden metódust, csak a lényegre koncentrálunk. Egy grafikus pont kezeléséhez tárolnunk kell a pont koordinátáit, és a színét. Azért fontos, hogy ezeket az objektum tárolja, hogy a pont „emlékezzen'' saját magára, és bármikor képes legyen újrarajzolni saját magát.
17
type TPont = object x : integer; y : integer; szin : byte; constructor Init; procedure Rajzol; procedure Torol; procedure Mozgat(UjX,UjY:integer); ... end; constructor TPont.Init; begin x := 0; y := 0; szin := black; end; procedure TPont.Rajzol; begin PutPixel(x,y,szin); end; procedure TPont.Torol; begin PutPixel(x,y,black); end; procedure TPont.Mozgat(UjX,UjY:integer); begin Torol; {m1} x := UjX; {m2} y := UjY; {m3} Rajzol; {m4} end; Most pedig – örülvén a pont objektumunknak – vágjuk újabb nagy fába a fejszét: most egy kört akarunk kezelni. A kör sok szempontból hasonló a ponthoz, a középpont koordinátáján és a színén kívül van sugara is. A középpont koordinátája egy x,y pár. Ezek már adva vannak a pont esetén is. Egy amatőr OOP programozó az alábbi módon kezdene neki: (nulláról indulva) type TKor = object x : integer; y : integer; sugar: integer; szin : byte; constructor Init; procedure Rajzol; procedure Torol; procedure Mozgat(UjX,UjY:integer); ... end; Egy profi azonban kihasználná a hasonlóságot a két objektum között: type TKor = object(TPont) sugar : integerr; ... end;
18
A fenti esetben az object(TPont) azt jelenti, hogy ezen új objektum írását nem nulláról kezdjük, hanem továbbfejlesztjük a már meglévő pont objektumot. Ez esetben azt mondjuk, hogy a TKor osztály a TPont osztály leszármazottja, míg a TPont az TKor szülője. Ez a leszármazott-szülő kapcsolat az öröklődésre utal, a gyermek mindent örököl a szülő tulajdonságaiból, de azokat továbbfejlesztheti. Lássuk, mi az, amit örököl, és továbbfejleszthet: Egy származtatott objektum minden adatmezőt örököl a szülőjétől. Így a fenti példában a TKor osztály példányainak van x,y,szin mezői, de nekik lesz még sugar mezőjük is, ami a szülő osztályba sorolható objektumoknak nem. Ez az öröklődés egyoldalú, és a gyermek nem válogathat az öröklődés során - egyszerűen mindent örököl. Mi a helyzet az örökölt adatmezők megváltoztatásával? A válasz egyszerű: nincs rá mód! A gyermek objektum az adattagokat „ahogy van - úgy van''12 elv alapján örökli, itt változtatásnak helye nincs, csak továbbfejlesztésnek. Ennek persze oka van, ezt a kompatibilitással foglalkozó részben fogjuk tárgyalni.
Az OOP nagy tulajdonsága - a sokalakúság Mit örököl még? Mi a helyzet az örökölt metódusokkal? Az öröklés során azonban nem csak az adattagok öröklődnek, a metódusok is - beleértve a konstruktorokat, és a destruktorokat is. Az objektum jelen formájában persze a bővített adatmezőket nem kezeli - az örökölt metódusokban nincs hivatkozás a sugar mezőre, az örökölt konstrukor nem inicializálja, stb. Nyilván az adattagok bővítésének akkor lesz meg a haszna, ha a metódusokat is kiegészítjük olyanokkal, amelyek kezelik az új mezőket. Kezdjük a konstruktorral. Készítsünk olyan konstruktort, amely ezen új mezőt is alaphelyzetbe állítja. Mi legyen a neve? Az „Init'' konstruktor-nevet már megszoktuk, de vajon használhatjuk-e? Hiszen ilyen nevű konstruktorunk már van, örököltük a „Pont”-tól. A válasz: használhatjuk nyugodtan. A helyzet hasonló, mintha egy programon belül van valamely nevű globális változónk, és egy eljáráson belül deklarálunk egy ugyanolyan nevű lokális változót! Mi fog ekkor történni? Az eljáráson belül a lokális változó „elfedi'' a globális változót, az újabb változat „eltakarja'' a régit. Az OOP-ben a helyzet ezzel rokon. Amennyiben egy származtatott objektumban egy ugyanolyan nevű metódust deklarálunk, az egyszerűen elfedi a régit. type TKor = object(TPont) sugar : integer; constructor Init; end; constructor begin x := y := Szin := sugar := end;
TKor.Init; 0; 0; black; 0;
Megfigyelhetjük, hogy a konstruktor kódjának nagy részét már megírtuk, van olyan metódusunk, amely a négy mezőből hármat inicializál, nekünk csak az új mezővel kellene törődnünk? Hogyan használhatjuk fel az örökölt konstruktor kódját?
12
as-is
19
constructor TKor.Init; begin Init; { figyelem, ez hibás !! } sugar := 0; end; Nyílvánvalóan nem így! Az ,”Init'' eljárás hívása melyik Init lesz? Ez egyértelműen az új Init lesz, a TKor.Init! Ez így rekurzív hívás lenne, méghozzá végtelen mélységben, így ezen konstruktor használata esetén a program elszállna... constructor TKor.Init; begin TPont.Init; sugar := 0; end; A megoldásban első látásra nincs semmi igazán meglepő - ez megint csak a szintaktikán elgondolkodó programozók számára nyújthat némi érdekességet: a TPont egy objektum-osztály, egy típus neve. Eddig a metódusok hívásánál egy objektum-példány nevét írtuk a metódus neve elé (pl. OV1.Berak). Jelen esetben ez a szintaxis mindösszesen a hívandó metódus pontos beazonosítására szolgál. A származtatott objektum konstruktoraiban gyakran használjuk fel az szülő osztály konstruktorait. De a fenti megoldás - bár működik - valójában elég kényelmetlen, hiszen „emlékeznünk'' kell, hogy a Kor őse a Pont (amelyet feltüntetünk a Kor deklarálásakor ... = object(TPont), és a konstruktor hívásakor is. Ennél kellemesebb megoldást kínál pl. a Delphi ezen problémára13. Térjünk vissza a Kor fejlesztéséhez. Mit kell még megváltoztatnunk? Természetesen egy kört másképp kell kirajzolni, és eltüntetni, mint egy pontot. type TKor = object(TPont) ... procedure Rajzol; procedure Torol; ... end; procedure TKor.Rajzol; begin SetColor(szin); Circle(x,y,sugar); end; procedure TKor.Torol; begin SetColor(black); Circle(x,y,sugar); end; Ismételten nem okoz az problémát, hogy ezen metódusok újraírása ugyanazon metódusnévvel történik meg. Nemsokára látunk példát, hogy a használat során egyértelművé válik, melyik metódus hívódik meg. De nézzük a harmadik metódust, a Mozgat-t. Azt is szükséges-e újraírnunk? Elvileg azt mondhatjuk, egy kört is ugyanúgy kell mozgatni a képernyőn, mint egy pontot, meg kell adni az új koordinátákat, a régi helyről le kell törölni, az új helyre ki kell rajzolni. Ezen felbuzdulva tehát beláthatjuk, hogy az OOP kódtakarékos, mert, lám, a körre ezt nem kell megírni, mégis működni fog a mozgatás körre is! 13
Delphi-ben a fentit az „inherited Init;” módon lehet írni, s az „inherited” a szülő osztályt jelenti, így mindig a „szülő osztálybeli Init” hívását jelenti, függetlenül annak nevétől.
20
Próbáljuk ki: var P:TPont; K:TKor; begin ... {initgraph, stb.} P.Init; P.Szin := white; P.Rajzol; P.Mozgat(10,10); K.Init; K.Sugar := 10; K.Szin :=yellow; K.Rajzol; K.Mozgat(20,20); ... {closegraph, stb.} end.
{p1} {p2} {p3} {p4} {k1} {k2} {k3} {k4} {k5}
{ itt figyeljünk }
Nézzük, mi történik, ha egy programon belül akarok kört, és pontot is használni. Ha valaki kipróbálja a fenti programot, látni fogja, hogy a pont jól működik. A „p1'' hatására elindul a TPont.Init konstruktor, és a P.x, P.y, P.szin megkapja a kezdőértékét. A „p2'' hatására a P.szin megváltozik (bár ez nem látszik a képernyőn még). A „p3'' hatására elindul a TPont.Rajzol, és a putpixel révén a pont megjelenik a képernyőn. A „p4'' hatására a TPont.Mozgat indul el, majd a TPont.Torol hatására a pont eltűnik a képernyőről, a P.x, P.y megkapja új értékét, és a TPont.Rajzol miatt a pont megjelenik az új koordinátán. Honnan tudja a Mozgat, hogy most egy pontot kell rajzolni, és a TPont.Rajzol és TPont.Torol metódusokat kell használni. Két helyről is! Az első, hogy a Mozgat egy P:TPont objektum révén lett aktivizálva (P.Mozgat). A másik okot mindjárt tárgyaljuk. Nézzük meg, mi történik a körrel: a „k1'' révén először a TKor.Init indul el, hiszen a konstruktort újradefiniáltuk, és a K.Init természetesen a TKor.Init-et jelenti (mivel a K egy TKor típusú objektum). A TKor.Init-ben először elindul a TPont.Init, és a K.x, K,y, K.Szin megkapja a kezdőértékét, majd visszatérünk a TKor.Init-be, és a K.sugar is megkapja a kezdőértéket. A „k2'' révén a K.sugar felveszi az új értékét, a „k3'' révén a K.szin is megváltozik. A „k4'' miatt elindul a TKor.Rajzol, és megjelenik egy kör a képernyőn. Számunkra a „k5'' lesz a legizgalmasabb. Ha valaki tényleg kipróbálta a fenti kis programot, látni fogja, hogy a dolog nem működik! Mi történt? Ha megpróbáljuk nyomon követni a programot, azt látjuk, hogy a „k5'' miatt elindul a
TPont.Mozgat, de ezen belül nem a TKor.Torol fog aktivizálódni az „m1'' ponton (mint várnánk), hanem a TPont.Torol! E miatt nem a kör fog letörlődi a képernyőről, hanem csak egy pont. Bár utána rendben történnek a dolgok tovább, a K.x, K.y megkapja az új értéket („m2'' és „m3'' sor), de az „m4''-es soron újra a TPont.Kirajzol fog végrehajtódni! Miért? A válasz valójában elég bonyolult, ugyanakkor nagyon egyszerű. A nagyon egyszerű válasz szerint amikor a TPont-t írtuk, nem figyelmeztettük az objektumot arra, hogy a Rajzol és Torol metódusok később újraírásra kerülhetnek az fejlesztés következő fokozataiban. Így a Pascal az „m1'' és „m2'' sorokhoz fixen hozzárendelte a TPont.Rajzol és TPont.Torol metódusokat. Ebből levonhatjuk azon következtetéseket, hogy az öröklődés sem feltétlenül fog minden automatikusan működni. De vajon ez már így is marad? Mit tegyünk? Egy kezdő OOP programozó sóhajtana egyet, s azt mondaná: no, akkor írjuk újra az öröklődés ezen szintjén a Mozgat metódust is:
21
type TKor = object(TPont) ... procedure Mozgat(UjX,UjY:integer); ... end; procedure TKor.Mozgat(UjX,UjY:integer); begin Torol; {x1} x := UjX; {x2} y := UjY; {x3} Rajzol; {x4} end; Mire végez, észreveszi, hogy az újraírt metódus szóról szóra megegyezik az örökölttel. A dolog kissé gyanúsan néz ki e miatt, de működik. Ha a fenti kis program „k5''-s sorát nézzük, ennek hatására mostmár a TKor.Mozgat fog aktivizálódni (hiszen a K objektum egy TKor típusú, és a TKor osztályban létezik Mozgat metódus). Ezen TKor.Mozgat-n belül az „x1'' során a TKor.Torol fog meghívódni, mivel a TKor osztály számára ez az aktuális, elfedve az elavult verziót ugyanezen nevű metódusból. Az „x4'' során is a TKor.Rajzol hajtódik végre. De hát hol is lesz azon alapelv, hogy az OOP kódtakarékos? A megoldás persze nem is ez, hanem a virtuális metódusokban rejlik! Mit csinált volna az előző esetben egy profi OOP programozó? Mint fent említettem, a TPont írásakor nem figyelmeztettük az objektumot, hogy ezen két metódusból a származtatás során újabb verziók keletkezhetnek! Van erre lehetőség? Igen! type TPont = object x : integer; y : integer; szin : byte; constructor Init; procedure Rajzol; virtual; procedure Torol; virtual; procedure Mozgat(UjX,UjY:integer); ... end; A virtual kulcsszó kiírása a metódusok neve mögött, az objektum definiálásakor éppen ezt teszi. A virtuális metódusok segítségével lehet megvalósítani azon vágyunkat, hogy használhassuk az örökölt metódusokat - azok újraírása nélkül - a leszármazott objektumokban is. Amennyiben egy metódusról az öröklődés valamely szintjén egyszer kinyilvánítottuk ezen figyelmeztetést, úgy ezen figyelmeztetést kötelesek vagyunk a származtatott objektumokban is feltüntetni: type TKor = object ... procedure procedure ... end;
Rajzol; virtual; Torol; virtual;
Vagyis, ha egy metódus egyszer virtuálissá vált, akkor az örökké az is marad!14
14
Ez azért nem teljesen igaz. Pl. Delphi-ben van rá mód, hogy ezt megszüntessük. De gyakorlatilag erre szinte soha nincs szükség! Ha igen, akkor már a tervezéskor óriási hibát ejtettünk!
22
Nézzük, mi történik ebben az esetben? A helyzet most tehát az, hogy a Mozgat metódushoz nem nyúltunk a TPont objektum készítése során, csupán mindkét objektumban a Rajzol és Torol metódusok deklarálásakor a virtual kulcsszót is kiírtuk? A „p1'' … „p4'' sorok működése nem változott meg! Miért, hiszen van fejlettebb Rajzol és Torol eljárás!? De nem ebben az esetben! Jelen esetben a Mozgat eljárás hívását a P.Mozgat(10,10); programsor váltotta ki, márpedig a P egy TPont. Egy TPont típusú objektum számára nincs ennél fejlettebb változat a fenti két metódusból! A második esetben viszont a „p1'' sor végrehajtása a TKor.Torol meghívását fogja jelenteni! Hiszen jelen esetben a K.Mozgat(20,20) váltotta ki a végrehajtást, és a TKor típusú objektumokban van fejlettebb Rajzol (és Torol)! A metódusok ezen tulajdonságát, hogy ugyanazon metódus többféle viselkedést tud produkálni, együtt tud működni a jövőbeni fejlesztésekkel is, az OOP egy másik, nagyon fontos elvének nevezzük, sokalakúságnak (polymorhysm).
Ahogy a virtuális metódusok működnek Hogyan lehet a virtuális metódus hívását megvalósítani? Akinek vannak assembly ismeretei, az tudja, hogy az eljárás hívása a call utasítással történik, amely mögé meg kell adni a hívandó eljárás memóriabeli címét! A processzor „elugrik'' ezen memóriacímre, és végrehajtja az ott felfedezett programsorokat, majd a ret utasítás hatására visszatért a hívás (call) utáni programsorra, és folytatja a végrehajtást. Ebbe a koncepcióba nem nagyon férnek bele az előző szakaszban megtárgyalt ismeretek! Hiszen pl. a „m1'' sorhoz (Torol;) milyen assembly utasítást generál a Pascal fordító? Hiszen ugyanezen assembly utasítás hol a TPont.Torol, hol a TKor.Torol eljárást hívja meg attól függően, hogy egy TPont vagy egy TKor objektumból hívtuk meg? Amennyiben a virtual kulcsszót nem használjuk a metódusok deklarálásakor, úgy a compiler a fordítás során egyetlen, fix helyre mutató call assembly utasítássá alakítja át a metódushívást, mindig a TPont.Torol eljárás hívásaként értelmezve ezen sort. Ezt a technikát korai kötésnek (early binding) nevezik. Ekkor a metódushívás-összerendelés fordítási időben zajlik le. Amennyiben használjuk a virtual kulcsszót - ezzel figyelmeztetve az objektumot (valójában a fordítót), hogy ezen metódusok hívásával vigyáznia kell, mert bár most még csak a TPont-t írjuk, és még nem tudjuk mi lesz a jövőben, milyen leszármazottjai lesznek a TPont-nak, de ezen metódusokból keressen újabb változatot, verziót. Ekkor a fordító nem tudja meghatározni, melyik konkrét metódus hívódik majd meg a program futtatása során erről a pontról, ezért ide egy elég bonyolult mechanizmus fordítódik le egy egyszerű call assembly utasítás helyett - ezen mechanizmus megkeresi a legfejlettebb verziót az aktuális szituációban. Ezt a technikát késői kötésnek (late binding) nevezzük. Hogyan lehet ezt megvalósítani? Amikor egy objektumot definiálunk, és használjuk a virtual kulcsszót valamely metódusra, a fordító felkészül a fenti problémás esetekre – a késő kötések feloldásának megkönnyítésére - és készít egy táblázatot. Ezt a táblázatot Virtuális Metódus Táblának nevezik, és a nevének megfelelően a táblázat soraiban az adott objektumosztályban található virtuális metódusok memóriacímeit tartalmazza (egy memóriacím általában 4 byte tárkapacitást igényel).
23
Nézzünk most egy példát: type TElso = object ... procedure A; procedure B; virtual; procedure C; virtual; procedure D; end; type TMasodik = object(TElso) ... procedure A; procedure C; virtual; procedure E; virtual; end; procedure TElso.D; begin A; {w1} B; {w2} C; {w3} end; var T1:TElso; T2:TMasodik; begin T1.D; {s1} T2.D; {s2} end. A TElso osztályhoz tartozó VMT tábla az alábbi bejegyzéseket tartalmazza: TELSO.VMT metódus B = TElso.B metódus C = TElso.C A TMasodik osztályhoz tartozó VMT tábla az alábbi bejegyzéseket tartalmazza: TMASODIK.VMT metódus B = TElso.B metódus C = TMasodik.C metódus E = TMasodik.E Ilyen VMT tábla minden objektumosztályhoz készül (tehát nem minden példányhoz, hanem osztályhoz)! Vagyis ha egy osztályból több példányt is készítek, azok ugyanazon a VMT-n osztoznak. Ez memóriatakarékos megoldás. Mellesleg működik is, hiszen egy osztály objektumainál a VMT tábla egyébként is egyformán néz ki. Ez alapján a fent említett mechanizmus nagyon egyszerűvé vált – amikor ilyen típusú eljárás meghívására kerül sor, nem egyszerűen egy call utasítást kell meghívni, hanem a fenti táblázatból ki kell venni a megfelelő sort (memóriacímet), és oda kell elugrani. A második esetet vizsgáljuk alaposabban meg. Először is ebben a VMT-ben szerepel az elj B is, holott azt nem definiáltuk felül a származtatás során. Ugyanakkor erre szükség is van, hiszen a „w2'' sorban a fordító szintén késői kötést fog alkalmazni, és ki akarja venni a megfelelő sort a táblázatból -
24
így ezen sornak benne kell lennie a táblázatban. Ez pedig nem memóriatakarékos15, de jelen pillanatban ez van! Ugyanakkor a második táblázatban egy bejegyzéssel több szerepel, mert egy - eddig nem szereplő új virtuális metódus került bele a rendszerbe: az „E'' metódus. E szinttől kezdve - ha a TMasodik-ból próbálok származtatni – azon osztályok VMT táblájában már ez a bejegyzés is mindig szerepelni fog. Megállapítható tehát, hogy a gyermek-objektum VMT táblája mindig legalább olyan hosszú, mint a szülő osztályé, és a táblázatának elején szereplő bejegyzések lényegben megegyeznek. Ez újabb adalék lesz a kompatibilitás c. fejezethez. Másrészről, ha nagyon hosszú az öröklődési lánc - a befejező osztálynak nagyon sok őse van - akkor ezen osztályhoz tartozó VMT tábla már nagyon nagy méretű is lehet! De ezen veszteségért kárpótol bennünket az, hogy cserébe nem kell az örökölt metódusokat újraírni (és tesztelni)! A fenti példában az „s1'' sor hatására elindul a TElso.D, majd azon belül a „w1'' hatására a TElso.A, a „w2'' miatt a TElso.B, a „w3'' miatt a TElso.C. A második esetben az „s2'' sor miatt elindul a TElso.D, majd azon belül a „w1'' hatására - no melyik is? A fenti példában az A metódust nem virtuálisként definiáltuk, így a fordító korai kötést fog alkalmazni, és nem veszi figyelembe, hogy az A metódust mi felüldefiniáltuk. Így marad a TElso.A hívásánál. A „w2'' hatására is marad a TElso.B-nél, mert bár a B-ről azt mondtuk, hogy virtuális lesz, de a TMasodik-ban nem definiáltuk felül (ez ugyanis nem kötelező)! E miatt - bár itt már a késői kötés lesz használva - de ugyanazon eljárás kerül meghívásra mindkét esetben (ez egyébként látszik is a TMasodik VM tábláján). A „w3'' hatására, pedig a TMasodik.C fog meghívódni, hiszen ez virtuális metódus volt, és felüldefiniálásra is került.
A konsturktorokról mégy egyszer - VMT Honnan tudja a futtató rendszer, hogy melyik VMT táblában kell keresnie a késői kötések feloldásakor? A válasz triviálisan hangzik, pedig nem az! Azt gondoljuk (amely alapvetően helyes is), hogy a memóriában eltárolódik a VMT tábla valahol16, az objektum-példányokban pedig van egy pointer (mutató), amely tárolja, hogy a sok VMT tábla közül (ne feledjük - minden osztályhoz egy darab), melyik tartozik jelen objektumhoz. Ezt az összerendelést a fordító könnyedén el tudja végezni már fordításkor, hiszen ő látja az objektum típusát, és ő generálta le a VMT táblákat is, akkor el tudja végezni az összerendelést fordítási időben! Ebben a gondolkodási technikában nagyon sok az igazság, de van valami, ami ezt megzavarja. type TPont = object x : real; procedure Akarmi; end; Ha megnézünk a fenti Turbo Pascal-beli objektum memóriaigényét (sizeof fv), akkor láthatjuk, hogy egy ilyen objektum-példány számára a rendszer 6 byte-t használ el, ami az x mező, mint real tárigénye. Változtassuk meg egy picit: type TPont = object x : real; procedure Akarmi;virtual; end; Ha most megnézzük ezen objektum tárigényét, 0 byte-t kapunk. Ez borzasztóan meglepő dolog tárgyalására később visszatérünk - de ha ezen problémát megoldjuk végre, azt fogjuk kapni, hogy 15 16
A dinamikus metódusok használatával ugyanezen probléma memóriatakarékosabb módon oldható meg. Turbo Pascal esetén ez az adatszegmens
25
ezen objektum tárigénye 8 byte lesz. Holott nem adtunk hozzá újabb mezőt! A növekedés oka azon láthatatlan pointer memóriaigénye, amely a VMT-t ezen objektumhoz rendeli. Egy ilyen pointer Turbo Pascal esetén 2 byte (short pointer), hiszen a VMT helye fixen az adatszegmens, és a szegmensen belüli offszet-cím csak 2 byte. Mielőtt az előbb említett (0 byte) problémát megmagyaráznánk, vegyünk egy másik esetet: type PPont = ^TPont; TPont = object x : integer; procedure Akarmi;virtual; end; var P:PPont; Ezen felállás mellet a P változóhoz nem lehet fordítási időben hozzárendelni ezen VMT tábla memóriacímét, hiszen maga az objektum is majd csak futási időben fog keletkezni. Amikor a New eljárással a P számára memóriát allokálunk, akkor a Heap-n belül most már nem 6 byte kerül lefoglalásra, hanem majd 8 byte, mert egyrészt el kell férnie a real-nek, másrészt ezen - nem általunk deklarált - VMT-re mutató kis pointernek. Vagyis az objektum ezen mezője a HEAP-n fog elhelyezkedni - hogy hol, az majd csak futás közben derül ki. Ezen oknál fogva az objektum-példány és a VMT összerendelése nem minden esetben történhet fordításkor, dinamikus objektumok esetén csak futás közben. A Turbo Pascal tervezői ekkor azt mondták, hogy nem fognak két technikát is elkészíteni - akkor maradnak ezen utóbbinál: minden objektum futás közben kapja meg a saját VMT címét! De mikor? Mivel az objektum-példány és a VMT összekapcsolása szintén az objektum-példány inicializálási problémaköréhez tartozik, ezért ezen összekapcsolást a konstruktorok végzik el! Nem kell aggódni, ezt nem nekünk kell leprogramozni - a kódot a pascal fordító készíti el - számunkra szintén láthatatlanul, mindössze azt kell tudnunk, hogy ezen kis kódrészlet akkor fog lefutni, amikor mi az objektum valamely konstruktorát meghívjuk. Ebből sok dolog következik! Az első, hogy azért kaptunk 0 byte-t az előző esetben az objektum méretére, mert ezen kis kódrészlet még nem került lefutásra - az objektumunk még a „nagyon inicializálatlan” ( :-) ) állapotban volt - még a VMT összerendelést sem kapta meg17. A másik, hogy olyan osztály készítésekor - amelynek vannak virtuális metódusai, kötelető konstruktorának is lennie, még akkor is, ha a konstruktor üres, vagyis egy – utasítások nélküli - begin ... end párosból áll: type TPont = object x : real; procedure Akarmi;virtual; constructor Init; end; constructor TPont.Init; begin end; Amíg egy konstruktort meg nem hívunk, az objektum nem működik, nem használható – hiszen a VMT tábla nem került inicialitzálásral Ebben az állapotban nem hívhatunk meg virtuális metódust, sem olyan metódust, amelyben virtuális metódus hívására kerül sor18. Ez már viszont nyomós érv a mellett, hogy az objektumot a gonosz programozó kollegánk ☺ valamely konstruktorával inicializálni kénytelen legyen! 17
hogy ettől miért lett 0 a mérete, lásd következő fejezet mivel az objektum kódja gyakran nem ismert (csak az interface), ezért lényegében nem tudhatjuk, melyik metódus belsejében van további virtuális metódushívás – így az objektum ezen állapotában lényegében nem hívhatunk meg semmilyen metódust.
18
26
Virtuális metódus elrejtése Mi történik akkor, ha az öröklődés következő szintjén felüldefiniálok egy virtuális metódust, de nem virtuálisként? Egyes OOP támogató nyelveken (pl. Pascal) ez egyszerűen nem megengedett. Ha egy metódus egyszer virtuális lett, akkor „örökké'' annak kell maradnia. Más nyelveken ez azért megengedett. Pl. Delphi-ben ilyet megtehetünk, bár fordításkor figyelmeztető üzenetet kapunk, kivéve ha a metódus fejrésze után a reintroduce kulcsszót is kiírjuk (ezzel jelezve a fordítónak, hogy nem feledékenyek vagyunk, tudjuk mit csinálunk), akkor ez a figyelmeztető üzenet is eltűnik. Amennyiben az öröklődés újabb szintjén visszahozzuk ugyanezen metódust, újra virtuálisként, az már nem ugyanaz lesz! Ennek oka a fordító egyszerűségében rejlik. Amikor egy „új'' virtuális metódust vezetünk be, amely a közvetlenül felette levő szinten nem volt deklarálva, a VMT táblában új bejegyzés készül, és nem ugyanazon a pozíción, mint a régi. Így a „régi'' öröklődési szinteken lévő hívások ezen „új'' verziójú metódusokat nem találják meg. Delphi-ben ilyet csak a „class'' jellegű objektum-osztályokban működik. Az „object'' jellegű osztályok Pascal-kompatibilis jellemzőkkel bírnak, vagyis nem engedik meg a fenti műveletet (a virtualitás megszüntetését).
A VMT-ről még egyszer Hogy miért kaptunk az előző példában 0 byte-t az objektum méretére a konstruktor hívása előtt? Nos, a VMT tábla valójában egy picivel bonyolultabb az előzőekben megadottnál: VMT kontroll összeg (16 bites egész) kontroll összeg negatív előjellel az objektum tárigénye (mérete) byte-okban első virtuális metódus mem.címe második virtuális metódus mem.címe ... stb ... Nos, a VMT elején egy kontroll összeg található - egy 16 bites egész szám. Hogy mennyi, nem érdekes. A második bejegyzés ugyanezen számot tartalmazza, csak negatív előjellel. Erre azért van szükség, mert a konstruktorokat többször is le lehet futtatni, de az összekapcsolást a VMT táblával elég egyszer elvégezni - a legelső konstruktor futásakor. A rendszernek el kell tudnia dönteni, hogy az objektum példány ezen szempontból már inicializált állapotban van, vagy sem. Ezt a rendszer úgy állapítja meg, hogy ellenőrzi, hogy a „kis mutató a VMT-re” tényleg a VMT-re mutat-e, vagy sem. Akkor mutat a VMT-re, ha az a hely, ahova mutat a memóriában, az ott található 2 byte (16 bit), és a következő 2 byte összeadása 0-t eredményez! Ha igen, akkor ez a mutató egy VMT tábla elejére mutat - hiszen annak a felépítése garantálja ezt. Ha nem, akkor az objektum még nem kapta meg a VMT tábla összerendelését, ekkor az összerendelés elvégzésre kerül. Az objektum mérete azért került a VMT-be, mert az objektumok méretének meghatározása nem egy egyszerű probléma19 - mivel sokszor a típusát sem könnyű meghatározni. Viszont minden objektumhoz - az „élete'' során nem megváltozó módon - hozzárendelődik a VMT, így célszerű itt tárolni az objektum méretét. S ha már a VMT-ben ez megtalálható, úgy a sizeof is innen veszi ki. Pontosabban innen venné ki, de ez csak akkor sikerül neki, ha az objektum inicializálva van (lefutott a konstruktora). Mivel előző példánkban ez még nem történt meg, így a sizeof nem tudta megállapítani az objektum méretét - ezért 0-t adott vissza. 19
lásd objektumok kompatibilitása fejezetet
27
A dinamikus metódusok Mint emlékszünk, a VMT az adott osztály minden virtuális metódusának címét tartalmazza, az örökölt - öreg, régi - és a vadonatújan újraírt, vagy jelen osztályban először definiált metódusokét is. Amennyiben az objektumnak már sok őse volt, az objektumot sokszor bővítettük, sok fajta virtuális metódusa van, akkor a VMT elég nagy méretű lehet. Ugyanakkor az öröklési lánc utolsó (legspeciálisabb) osztályainak a VMT-je szinte teljesen egyforma is lehet. Felmerülhet az igény, hogy memóriatakarékosabb megoldást keressünk a VMT helyett. Ezen problémára nyújthat megoldást a DMT, a dinamikus metódus tábla. Nem kell megijedni, a dinamikus metódusok valójában a hagyományos értelemben virtuálisak, minden igaz rájuk, amiről eddig szó volt. Egyetlen különbség van - a késői kötés feloldásának megvalósítása. A dinamikus metódusok deklarálása során a metódusnak egyértelmű azonosító sorszámot kell adni amely az adott öröklődési láncon belül egyértelmű azonosító kód! Az adott öröklődési láncon belül minden virtuális metódusnak különböző kódszáma kell legyen - de az azonos nevűeknek - azok minden verziójában - ugyanazon kódúnak kell lennie. Sok programnyelvben ezeket az azonosítókat a fordító program osztja ki, rendeli hozzá a metódusokhoz. Az osztályokhoz ebben az esetben egy olyan táblázat generálódik hozzá, amely sokban hasonlít a VMT-hez, két különbséggel: type TElso = object ... procedure A; procedure B; dynamic; procedure C; dynamic; procedure D; end;
{hozzárendelt azonosító = 1} {hozzárendelt azonosító = 2}
type TMasodik = object(TElso) ... procedure A; procedure C; dynamic; {hozzárendelt azonosító = 2} procedure E; dynamic; {hozzárendelt azonosító = 3} end; A TElso osztályhoz tartozó DMT tábla az alábbi bejegyzéseket tartalmazza: TELSO.DMT ŐS DMT tábla =
metódus B = TElso.B metódus C = TElso.C A TMasodik osztályhoz tartozó DMT tábla az alábbi bejegyzéseket tartalmazza: TMASODIK.DMT ŐS DMT tábla = metódus C = TMasodik.C metódus E = TMasodik.E Mint látjuk, a TMasodik DMT táblájában nincs benne az metódus B címe, csak azon virtuális metódusok címeit tartalmazza, amelyeket ezen a szinten, ebben az osztályban újradefiniálunk, vagy frissen most vezettünk be.
28
Hogy működik a késői kötés feloldása? procedure TElso.D; begin A; {w1} B; {w2} C; {w3} end; var T1:TElso; T2:TMasodik; begin T1.D; {s1} T2.D; {s2} end.
A „w1'' sor korai kötés, feloldása fordítási időben történik. A „w2'' sor esetén a fordító, aki a dinamikus metódusok kódjait kiosztja, fordításkor leszögezi, hogy a „1” kódú metódust kell meghívni. Az „s1'' sorban a T1-hez rendelt TElső DMT táblában kezdi a keresést. Mivel talál a táblázatban „1” azonosítójú metódust, megtalálja annak memóriacímét, és végrehajtja a metódus, az eljárás hívását. A „w3'' sor esetén hasonlóan működik. Az „s2'' sorból kiindulva, a „w2'' sorban a T2-höz rendelt TMasodik DMT táblában kezdi el a keresést, a „1” kódú metódus memóriacíme után. Mivel ez nincs benne a megadott DMT táblában, folytatja a keresést az ős DMT táblában! Ez a TElso DMT táblája, melynek címe megtalálható a TMasodik DMT táblájában. Mivel a TElso DMT táblájában megtalálható a „1” metódus memóriacíme - mely nem más, mint a TElso.B - elindítja azon eljárást. A „w3'' sor esetén a TMasodik DMT táblájában megtalálja a „2” kódú metódus címét, így elindítja azt - a TMasodik.C-t. Így oldódik meg a legújabb verziójú metódus meghívása. Amennyiben valami hiba esetén a DMT táblákban visszafele lépkedve sem található meg egy metódus, a keresés leáll az öröklődési lánc kiinduló osztályának DMT táblájában, hiszen ott a visszalépés nem lehetséges, ott az ős DMT tábla nincsen. Mint láthattuk, a DMT táblák használata kevesebb memóriát köt le, de a keresés miatt a használata lassabb. A programozási nyelvek ajánlása szerint a két módszert felváltva használjuk - a sűrűn használt metódusok esetén a hagyományos virtuális metódus-technikát használjuk. Itt fontos a sebesség. A ritkán hívott metódusok esetén a dinamikus feloldási technikát használhatjuk.
Az adatejtésről még egyszer Az adatrejtésről már hallottunk, ennek lényege, hogy az objektumok adatmezőihez a programok ne tudjanak hozzáférni, csak metódusokon keresztül. Ezáltal elérhető lesz az a hőn áhított cél, hogy az objektum csak üzeneteken keresztül – csak metódusain keresztül - változtassa meg állapotát. Gondoljunk bele, mi történne azzal a bizonyos TPont objektummal, ha a mezőit publikussá tennénk: type TPont = object x : integer; y : integer; szin : byte; constructor Init; procedure Rajzol; procedure Torol; procedure Mozgat(UjX,UjY:integer); ... end;
29
Ez esetben a gonosz programozó kollégánk az alábbi kóddal meg tudná zavarni az objektum működését: var P:TPont; begin P.Init; P.Mozgat(10,10); P.x:=20; {c1} P.Mozgat(30,30); {c2} end. A „c1'' sor mindenképpen kellemetlen számunkra, mert az objektum x mezője felveszi az új értéket, de a grafikus képernyőn ez nem látszik, hiszen a pont újrarajzolása nem következett be. A „c2'' még rosszabb, mert a Mozgat során letörlődne a régi helyéről a pont, de jelen esetben ez a x=20, y=10 helyről töröl, hiszen ez van eltárolva, a fehér pötty viszont a 10,10 koordinátán van még mindíg! Vagyis a mozgatás során a fehér pötty nem törlődik le, a végén viszont a 30,30 koordinátára kirajzolódik egy újabb fehér pötty. Az objektum belső állapota, és a külvilág már nincs szinkronban! A belső állapot szerint a pont a 20,10-en van, a külvilág szerint a 10,10-en. Hogy ez ne következhessen be, megtiltjuk a programozók számára, hogy objektum belsejébe:
belepiszkáljanak az
type TPont = object private x : integer; y : integer; szin : byte; public constructor Init; procedure Rajzol; procedure Torol; procedure Mozgat(UjX,UjY:integer); ... end; Ezáltal, az adatrejtés segítségével meggátoltuk a kollégák gonosz szándékú beavatkozását! De ezzel kényelmesen hátradőlhetünk? Nem igazán, mert hogyan lehet ekkor megvalósítani azt, hogy a pont mozduljon egy pixelponttal jobbra? begin ... P.Mozgat(P.x+1,p.y); ... end. Sajnos így nem! Ugyanis az adatrejtés miatt a p.x+1 kifejezést a fordító nem fogadja el, azt állítván, hogy ilyen mező nincs az objektumban. Ez egyébként persze nem igaz, de mivel az x mező privát eléréssel rendelkezik, így a külvilág számára tényleg nem létezik!
30
Mit tegyünk? var x:integer; P:TPont; begin ... x:=10; P.Mozgat(x,10); ... x:=x+1; P.Mozgat(x,10); ... end. Jelen felállásban ez az egyetlen megoldás, hogy a programozó kolléga is eltárolja az X értékét, hogy tudja, hol a pont. Ez nyílván dupla tárolást jelent, hiszen az objektumban is el van tárolva a pont aktuális X koordinátája. A megoldás tehát nem túl szerencsés! type TPont = object private x : integer; y : integer; szin : byte; public ... function GetX:integer; end; function TPont.GetX:integer; begin GetX := x; end; var P:TPont; begin ... P.Mozgat(P.GetX+1,10); ... end. A megoldás tehát az, hogy elrejtjük az X mezőt, hogy a külvilág ne tudja megváltoztatni, de biztosítunk hozzá kiolvasási lehetőséget (GetX fv). Sokszor van arra szükség, hogy ne csak kiolvasni lehessen egy mezőt, hanem be is állítani: type TPont = object private x : integer; … public ... procedure SetX(UjX:integer); end; procedure TPont.SetX(UjX:integer); begin Mozgat(UjX,Y); end;
31
var P:TPont; begin ... P.Mozgat(10,10); P.SetX(P.GetX+1); ... end. Jelen esetben, ha csak az X értékét akarjuk megváltoztatni, az Y-t nem, akkor használhatjuk a SetX metódust, amely mellesleg szintén a Mozgat-t használja a beállításhoz. Ekkor lényegében biztosítottuk a mezőhöz való hozzáférés mindkét szintjét újra: az olvasást (read) a GetX segítségével, és az írást (write) a SetX segítségével. Ez utóbbi most már ellenőrzött körülményeket teremt az íráshoz, nem fordulhat elő, hogy az X értéke a nélkül változna meg, hogy a külvilág erről ne értesülne - hiszen a Mozgat miatt az X koordináta megváltoztatása egyúttal az új koordinátán történő megjelenítéssel is jár! Gondoljunk bele, mivel jár az adatrejtés: • kódhossz-növekedés: minden elrejtett adatmezőhöz két metódus írása tartozik, egy Get és egy Set típusú. Ez a program hosszában látszik is! • futásidő-növekedés: az ilyen rejtett mezők elérése (olvasás) nem egyszerűen egy memóriaterület kiolvasása, hanem egy függvény hívásával jár. A mező írása sem egyszerűen egy változó értékadása, hanem egy sok tevékenységet elvégző eljárás hívásával történik. Ez mindenképpen lassabb, mint a hagyományos megoldás! • objektum-külvilág konzisztencia: cserébe annyit nyerünk, hogy az objektum és a külvilág mindig ugyanazon állapotot tükrözi, az objektumot nem lehet „elrontani''. S ez néha komoly előny! Nagyon komoly előny!
A self paraméter Hogyan működnek az objektumok metódusai? Miért nem kell az A megoldás - az objektum fejezetben bemutatott with segítségével „kinyitni'' az objektumot, hogy címezhessük a mezőket? Hogyan tudja eldönteni több objektum-példány esetén, hogy most éppen melyiknek a mezőivel dolgozunk a metódusokon belül? A válasz a fenti kérdésekre egy rejtélyes self paraméter. A „self'' angol szó „én''-t, „maga''-t jelent magyarul. A self valójában egy mutató, amely az adott objektum által lefoglalt memóriaterületre mutat. ... procedure TPont.SetX(UjX:integer); begin Mozgat(UjX,Y); end;
{ plusz egy „self” is }
var P,R:TPont; begin ... P.SetX(10); {n1} R.SetX(10); {n2} ... end. A fenti példában a SetX metódusnak - és minden metódusnak - van az UjX-n kívül még egy paramétere, egy self:pointer paramétere. Ezen self jelen esetben a P változó területére fog mutatni, és a TPont.SetX fenti sora valójában Mozgat(UjX,Self^.Y) alakú, s ilyen módon persze Mozgat(UjX,P.Y) hajtódik végre. Az „n2'' sorból kiindulva a self az R változó területére fog mutatni, és a SetX sora továbbra is Mozgat(UjX,Self^.Y) alakú, s ezen esetben a
32
Mozgat(UjX,R.Y) hajtódik végre. Vagyis a P objektum belsejében (metódusaiban) a self mindig a P-t jelenti (saját magát), az R objektum belsejében pedig magát az R-t. A metódusoknak tehát mindig egyel több paraméterük van, mint látszik, és a metódusok belsejében a mezők hivatkozása mindig self^.mezo alakú. Mindez persze azért „láthatatlan'', hogy a szintaxis egyszerűbb legyen. Miért jó, ha tudjuk mindezt? Mert a „self'' felhasználható saját céljainkra! Normális esetben egy konstruktorban a mezőknek sorban kezdőértéket adunk, mindegyiknek egyesével - a típusuknak megfelelően. Amennyiben az osztály készítése közben újabb mezőre lesz szükségünk, akkor azt fel kell vennünk a deklarációs részen - valószínűleg a private szakaszban -, majd a konstruktor kódjába is be kell írni, hogy ezen mezőt is kezdőértékkel lássuk el. De mi van akkor, ha szeretnénk egy olyan univerzális konstruktort írni, amely az objektum mezőit kezdőértékkel tölti fel, függetlenül attól, melyek azok. Így újabb mező felvételekor megúsznánk a konstruktor kódjának módosítását: constructor TAkarmi.Create; begin fillchar(self^,sizeof(self^),0); end; A fenti fillchar utasítás az első paraméterében megadott memóriaterületet a második paraméterben megadott hosszan 0-val tölti fel. Ez boolean típusú mező esetén false értéket jelent, egész típus esetén 0 kezdőértéket, string esetén üres stringet, stb...20 A self paraméternek még lesz jelentősége a „vizuális fejlesztőeszközökről'' szóló fejezetben.
Az objektumok kompatibilitása Objektum - mint paraméter Lehet-e objektumokat eljárások paramétereként átadni? procedure FaligTol(var G:TPont); { eltol a falig } begin while G.GetX>0 do begin G.Mozgat(G.GetX-1); delay(10); end; end; var P:TPont; K:TKor; begin ... FaligTol(P); {k1} FaligTol(K); {k2} ... end. A fenti példában látható, hogy egy TPont objektum paraméterként átadható egy közönséges eljárásnak is. Természetesen ugyanezen módszerrel egy objektum átadható egy másik objektum metódusának is paraméterként. Jelen példában cím szerinti paraméterátadást használunk, A meglepő nem is a „k1'' sor, hanem a „k2'' sor. Ugyanis a „k2'' sor működik! Nem okoz fordítási hibát! 20
Az igazság az, hogy (nem véletlenül) mélyen hallgattam a „valós” típusnál ez mit jelent !
33
Ha alaposabban belegondolunk, érhető is! A TKor-nek szintén van Mozgat és GetX metódusa (ezt az öröklés garantálja), így minden olyan helyen nyugodtan használható, ahol egy TPont típusú objektum használható. De ha alaposabban belegondolunk, akkor a FaligTol eljáráson belül a TKor lefokozásra került, vissza lett butítva egy TPont színvonalára. Vagyis a FaligTol-n belül nem használhatók második esetben sem a TKor speciális, frissen bevezetett metódusai. Ha megpróbálnánk meghívni egy ilyet, a fordító hibát jelezne, hiszen a G objektum számára egy TPont típusú objektum, és csak a TPont publikus metódusait és mezőit ismeri. A fentiek ismeretében megállapíthatjuk, hogy az öröklődési láncban később lévő elemek mindig használhatók bármelyik ősük helyett, a fordító azokkal mindig kompatibilis típusnak fogadja azt el. Hogyan lehet ez? A választ már megadtuk - az öröklődés biztosítja, hogy a származtatott osztálynak mindene meglegyen, ami az ősének megvan: mezők, metódusok. Biztos ez? A mezőket az öröklődés során semmilyen formában nem tudjuk megváltoztatni. Nem tehetjük meg, hogy egy mezőt egyszerűen nem öröklünk. Mindent öröklünk. Ugyanakkor az örökölt mezők típusait sem változtathatjuk meg, s értelemszerűen nem definiálhatunk ugyanolyan nevű mezőt még egyszer - elfedve a régit, az örököltet. A mezőkkel tehát semmi baj nem lehet. A metódusokat az öröklődés során azonban megváltoztathatjuk. Amennyiben a metódus virtuális, úgy csak a kódját változtathatjuk meg, a paraméterezését nem. Ebben az esetben nincsen különösebb probléma. A virtuális metódusok ebben az esetben is jól működnek: amennyiben egy virtuális metódus kerül meghívásra, úgy a késői kötés miatt mindig az adott objektumban definiált virtuális metódus kerül végrehajtásra. Amennyiben a metódus nem virtuális, úgy megváltoztathatjuk a paraméterezését is. De a nem virtuális metódusok esetén a korai kötés van érvényben, és hogy melyik kerül meghívásra - azt mindig a deklarált típus (a lokális paraméterlista szerinti típus) dönti el.
Az objektum mérete A helyzet bonyolódik, amennyiben egy ilyen eljárásban, amely paraméterként kap meg egy objektumot - megpróbáljuk megállapítani az objektum méretét, pl mert ki akarjuk írni lemezre: procedure LemezreIr(var G:TPont); var f:File; begin assign(f,'akarmi.dat'); rewrite(f,1); blockwrite(f,G,sizeof(G)); close(f); end; var P:TPont; K:TKor; begin ... LemezreIr(P); {k1} LemezreIr(K); {k2} ... end.
34
Mennyi lesz a G mérete a fenti példában? A G-ről a fordító azt hiszi, hogy TPont típusú, de ha lekérdezzük a méretét a „k2'' sorból kiindulva, akkor kiderült, hogy a méretét korrekt módon határozza meg, egy TKor objektum méretnyi byte-t fog kiírni a lemezre. Ugyanis az objektum mérete a VMT táblájában van eltárolva, amely a második esetben egy TKor VMT táblája lesz! Nézzünk a fentiekre egy komplex példát: type TElso=object X:integer; procedure A; procedure B;virtual; procedure C(k:integer); constructor XX; end; type TMasodik=object(TElso) Y:integer; procedure A; procedure B;virtual; procedure C(o,p:integer); end; constructor TElso.XX; begin end; procedure TElso.A; begin writeln('TElso.A'); B; end; procedure TElso.B; begin writeln('TElso.B'); end; procedure TElso.C(k:integer); begin x:=k; writeln('TElso.C - egy parameterrel'); end; procedure TMasodik.A; begin writeln('TMasodik.A'); B; end; procedure TMasodik.B; begin writeln('TMasodik.B'); end; procedure TMasodik.C(o,p:integer); begin x:=o; Y:=p; writeln('TMasodik.C - ket parameterrel'); end;
35
procedure Akarmi(var X:TElso); begin X.A; X.B; X.C(1); writeln('Size = ',sizeof(X)); end; var a1:TElso; b1:TMasodik; BEGIN a1.XX; b1.XX; Akarmi(a1); Akarmi(b1); END. A fenti kis programban az alábbiak kerülnek kiírásra: / 1/ / 2/ / 3/ / 4/ / 5/ / 6/ / 7/ / 8/ / 9/ /10/
TElso.A TElso.B TElso.B TElso.C - egy parameterrel Size = 4 TElso.A TMasodik.B TMasodik.B TElso.C - egy parameterrel Size = 6
{ { { { {
!!! !!! !!! !!! !!!
korai kötés } } } korai kötés } }
A /1/ sor az Akarmi(a1) hatására elinduló X.A írja ki. Ez valójában a Telso.A, mivel az X típusa a deklaráció szerint (és a valóságban is) TElso. A TElso.A elindítja a B eljárást, amely a korai kötés miatt csakis a TElso.B lehet. A /3/ sorban lévő újbóli kiírást az Akarmi-ben lévő X.B hívása eredményezte. A /4/ kiírást az Akarmi.C hívása okozta. Ez szintén csakis a TElso.C lehet, hiszen az X típusa jelen esetben TElso a formális paraméterlista, és az aktuális paraméterlista értelmében is. Az X mérete (sizeof) 4, mivel van egy integer mezője, ami 2 byte, és van egy short pointer-e a VMT táblájára, ami szintén 2 byte. Az érdekesebb kiírások a /6/../10/ sorok. Ekkor az X típusa ténylegesen már TMasodik, de a formális paraméterlista értelmében TElso. Ki győz? A TElso.A hívódik meg a Akarmi-ből, hiszen a korai kötés eseteiben csakis a formális paraméterlista győzhet (fordításkor csak ez áll rendelkezésre). Ugyanezen oknál fogva a /9/-ben is a TElso.C hívódik meg. Ugyan a TMasodiknak van ennél újabb C metódusa - ráadásul más paraméterezéssel - de ez mit sem számít a korai kötés esetén. A /7/ és /8/ esetén látszik, hogy a késői kötés minden körülmények között működik: az X ugyan a formális paraméterlista szerint TElso típusú, de a késői kötés nem e szerint működik, hanem a VMT alapján, és az X VMT táblája nem más, mint a TMasodik.VMT, így a B hívása már a TMasodik.B lesz. E miatt a /8/ sor értelemszerűen a TMasodik.B hívását jelenti. A /7/ ugyanezen oknál fogva lesz TMasodik.B, bár a /7/ kiírását a TElso.A metódus belsejéből lett kezdeményezve, de a TElso.A hívásakor a self nem lesz más, mint az X, ami viszont a TMasodik.VMT-jét tartalmazza, így a self is a TMasodik.VMT-t használja majd, így a késői kötés feloldásakor (ami a metódusok belsejében a self-n alapul) szintén a TMasodik.B fog meghívódni. A /10/ pedig azért mutat 6 byte-ot méret gyanánt, mert az X egy objektum, amely méretének meghatározása a VMT-n alapul, s amely jelen esetben a TMasodik.VMT. A TMasodik objektum pedig éppen egy integerrel több, mint a TElso, így a hossza is 2 byte-al több.
36
Futás közbeni típusellenőrzés Sűrűn szoktunk olyan eljárásokat írni, amelyek valamely objektumot kapnak paraméterként meg. Ilyenkor a paraméter számára megállapítunk egy olyan típust, amellyel a hívás során minden objektum kompatibilis lesz – egy közös ős-típust. A Delphi esetén ilyet mindig lehet találni - ott minden objektum-osztály végső soron - elkerülhetetlenül - egy közös őstől származik: a TObject típusból. Ezért Delphi-ben egy procedure Akarmi(var X:TObject)stílusú eljárást bármely objektummal meghívható. Arra is gyakran van szükség, hogy az eljáráson belül eldöntsük, hogy most konkrétan milyen típusú objektummal is hívtuk meg - hogy az eljáráson belül kihasználhassuk annak legbővebb, legjobb lehetőségeit – valami hasonló módon: procedure LemezreIr(var G:TPont); begin if "G egy TPont" then ...; if "G egy TKor" then ...; end; A fentihez hasonló típusellenőrzésekre a Turbo Pascal-ban semmilyen lehetőség nincs. Delphi-ben az
is kulcsszót lehet használni: procedure LemezreIr(var G:TPont); begin if G is TPont then ...; if G is TKor then ...; end; Az is bal oldalára egy objektumot kell írni, a bal oldalra pedig egy osztálynak a nevét. Hogyan dönthető el egy objektumról, hogy megfelel-e egy típusnak? Sokkal könnyebben, mint először gondolnánk. Ugyanis akkor felel meg, ha az adott objektumhoz rendelt VMT tábla ugyanaz, mint az adott típushoz tartozó VMT tábla. Ezt pedig egy igen egyszerű vizsgálattal meg lehet határozni: a két VMT tábla ugyanazon memóriacímen helyezkedik-e el?! if G is TPont then ...; if G.VMT = TPont.VMT then ...;
{ szintaktikailag nem helyes !}
Természetesen ez utóbbi sor szintaktikailag nem helyes, a VMT táblához ily módon nem lehet hozzáférni :-). A fenti típusellenőrzés csak nagyon ritkán fordul elő objektumok belsejében. Ugyanis ezen technika során a típusok neveit bele kell építeni (írni) a kód belsejébe. Ugyanakkor ha egy újabb típust vezetünk be, azt is be kell(ene) írni. Ez pedig sérti az univerzalitást, az objektumok fejlesztői ilyeneket nem írnak. A végfelhasználást, az alkalmazást készítő programozó azonban írhat ilyeneket, ő a program utolsó fázisát is látja, tisztában van/lehet vele, hogy több objektum-típus ezen a ponton nem bukkanhat fel. A Turbo Pascal-ban csak egy kezdetleges módszer létezik a típusellenőrzésre – a typeof függvény. A „typeof” a paraméterként megadott „dolog”-hoz tartozó Virtuális Metódus Tábla memóriacímét adja vissza. Ezen „dolog” lehet egy osztály-típus neve, vagy egy objektum-példány neve. var p1:TElso; begin if typeof(TElso)=typeof(p1) then writeln(’Ez triviálisan igaz.’); end.
37
Futás közbeni típuskonverzió Amennyiben a fent ismertetett típuselenőrzés működik, úgy azt valamire használni is kellene :-). A típusellenőrzéssel még csak félúton vagyunk a célig - maga a típusellenőrzés önnmagában nem túl hasznos dolog: type TEgy = object procedure A;virtual; end; tpye TKetto = object(TEgy) procedure A;virtual; procedure B; end; procedure Akarmi(var G:TEgy); begin if G is TEgy then G.A; if G is TKetto then G.B; end; A fenti példa nem működik! Ugyanis hiába ellenőrzzük le, hogy G típusa valójában TKetto, a fordító számára a G egy TEgy típusú objektum (a paraméter-deklaráció alapján), így a G.B metódushívást nem engedi, a TEgy-nek nincs B metódusa! Mi tudjuk, hogy van B metódusa, de a fordító nem! Mit tegyünk? A válasz a típuskényszerítés. A típuskényszerítés már Turbo Pascal-ban sem ismeretlen fogalom, bár egy átlagos Pascal programozó nem biztos hogy ismeri, vagy alkalmazta valaha is: var ora,perc,mperc:word; Start:longint; begin Start := ora*3600 + perc*60 + mperc; end. A fenti kis kód szeretné kiszámítani, hányadik másodpercénél tartunk a napnak. Azonban az ora word típusú, a 3600 egy számkonstans, amelyet a Pascal fordító a fenti esetben szintén word típusú számként fog majd fel, és elvégzi a két word között a szorzás műveletet, melynek eredménye azonban nem biztos, hogy el fog férni egy word-ben, csak ha az ora értéke 19-nél kisebb. Ha ora=19, akkor a részeredmény 19*3600=68400 lenne, amely nem fér el egy word-ben, és „range check error''-t eredményez. A programozó ezt nem biztos hogy észreveszi, ha este 6 óra után sosem teszteli a programját, akkor neki ez a hiba sohasem jön elő. De amennyiben a felhasználó futtatja a programot este is, úgy neki hibát jelez a program. A nappali műszakos programozó viszont sosem tudja produkálni a hibát, és ilyenkor a programozó javasolni szokta a kedves felhasználó gépének vírustalanítását, a processzor, a memória, majd a teljes gép cseréjét, és a felhasználó sürgős informatika továbbképzését (mert biztos már megint össze-vissza nyomkod valamit :-) ). A megoldás csak az lehet, ha az ora változót nem word-re deklaráljuk, hanem longint-re. Ekkor ugyanis a fenti szorzást a pascal longint-ek között végzi el, amelyben az eredmény biztosan el fog férni. Vagy van más megoldás is? ... Start := longint(ora)*3600 + perc*60 + mperc; ... A fenti sorban típuskényszerítést alkalmaztunk. Az ora változót meghagytuk word típusúnak, csak a szorzás idejére longint-ra változtattuk a típusát, kényszerítettük a fordítót, hogy a részeredmény készítésekor az ora változóban lévő értéket, mint longint típust fogja fel.
38
Hasonlóan lehet objektumokkal is elvégezni a típuskényszerítést: ... if G is TEgy then G.A; if G is TKetto then TKetto(G).B; ... A TKetto(G) a fordítónak azt jelzi, hogy egyetlen utasítás erejéig a G-t, mint egy TKetto típusú objektumot fogja fel, s ily módon fogadja el a B metódus hívását. Delphi-ben van erre más mód is - más mód a típuskényszerítésre. Ez csak szintaktikailag más technika, a jelentése, a működése teljesen ugyan ez: ... if G is TKetto then (G as TKetto).B; ... Az as kulcsszóval jelezzük, hogy a G-t, mint TKetto típusú objektumot kell felfogni.
A típuskonverzió veszélyei A típusellenőrzés és a típuskonverzió egymástól független technikák, egymástól elkülönülten is használhatók. Ha azonban a típusellenőrzés nélkül használjuk a típuskonverziót - jól gondoljuk meg: type THarom = object(TEgy) procedure C; end; procedure Akarmi(var G:TEgy); begin if G is TEgy then G.A else (G as TKetto).B; end; A fenti példa mindaddig jól működik, amíg csak e két típust használjuk. De ha egy THarom típusú objektummal hívjuk meg a fenti eljárást, amely nem TEgy típusú (emiatt az else ágra kerül a végrehajtás), de nem kompatibilis a TKetto-vel sem (a miatt nem fog sikerülni az as még akkor sem, ha van neki B metódusa), az else ágon található típuskonverzió, és metódushívás nem sikerülhet. A hiba oka, hogy úgy használtuk a típuskonverziót, hogy előtte nem használtuk a típusellenőrzést. Hagyományos programozási nyelveken ilyen esetben „run time error'' generálódik, és a program leáll.
A konstruktorok és destruktorok virtualitása A konstruktorok virtualitása A Turbo Pascal-ban a konstruktorok virtualitása nem megengedett: oka magától értetődő! Ha lehetőség lenne virtuális konstruktorokat készíteni, akkor a konstruktorok hívásának feloldására a késői kötést kellene használni, amely a VMT táblán alapul, amelyet a konstruktorok rendelnek hozzá az adott objektumhoz! Ez tehát olyan ördögi kör, amelyet nem lehet feloldani. Viszont a származtatott objektumokban természetesen jogunk van ugyanolyan nevű konstruktorokat definiálni. Hogy melyik kerül meghívásra, azt a korai kötés miatt a fordításkori típus dönti el!
39
Hogy ez mennyire így van, lássunk egy igen ravasz példát: type TElso=object X:integer; procedure B;virtual; constructor Create; end; type TMasodik=object(TElso) Y:integer; procedure B;virtual; end; constructor TElso.Create; begin end; procedure TElso.B; begin writeln('TElso.B'); end; procedure TMasodik.B; begin writeln('TMasodik.B'); end; procedure Akarmi(var X:TElso); begin X.Create; X.B; writeln('Size = ',sizeof(X)); end; var a1:TElso; b1:TMasodik; BEGIN a1.Create; b1.Create; Akarmi(a1); Akarmi(b1); B1.B; END. A fenti kis példa alaposan összezavarja a b1 változót: TElso.B Size = 4 TElso.B Size = 4 TElso.B Miért a fentiek jelennek meg? Bár a b1.Create során a b1-hez hozzárendelődik a TMasodik.VMT, de a Akarmi-ben a szintén meghívódik a konstruktor, amely minden esetben a TElso.Create lesz, mely újra hozzárendeli az X-hez a VMT táblát - a TElso.VMT-t! Így az Akarmi(b1) során a b1-hez is a TElso.VMT rendelődik hozzá, így az Akarmi-n belüli X.B metódushívás - bár a késői kötés hajtódik végre - a TElso.B lesz, és a sizeof is 4-t ad vissza. A cím szerinti paraméterátadás révén a hatás „tartós'' marad, és a főprogramba visszatérve a b1.B is a TElso.B lesz !
40
A destruktorok virtualitása A destruktorok virtualitása megengedett, és ezen szempont szerint a desktuktor tényleg olyan, mint egy közönséges metódus - ha kell, akár mint egy közönséges virtuális metódus.
Absztrakt metódusok Az általános jellegű objektum-osztályok készítése közben sokszor találkozunk olyan problémával, hogy valamely metódusnak egyszerűen nincs általános alakja, annak mindig csak az adott projekt környezetében van értelmes megoldása. Ugyanakkor a metódust az általános metóudsokból meg kellene hívni. Vagyis mégiscsak el kellene készíteni a metódust - de mégsem tudjuk. Ekkor készítünk absztrakt metódust. Pl: type TVerem = object ... procedure Hiba_TeleVan;virtual; procedure Berak(X:integer); ... end; procedure TVerem.Berak(X:integer); begin if not Tele then begin ... end else Hiba_TeleVan; end; procedure TVerem.Hiba_TeleVan; begin { ... most mi legyen ? hibauzenet kiirasa a kepernyore ? de mi van, ha grafikus alkalmazast ir a felhasznalo? Hangjelzest adjunk? ... }; Abstract; end; A hibát kijelző eljárást nem tudjuk általánosan megírni, ugyanakkor el kell készíteni a metódus fejének tervét (nincs paramétere, és úgy hívják: Hiba_TeleVan), hogy a Berak-ból meg lehessen hívni. Az abstract; egy eljáráshívás, amely „Run-Time Error”-t generál. Ennek mi az értelme? Az, hogy a felhasználót mintegy rákényszerítjük, hogy ezen metódust definiálja felül, mielőtt a veremobjektumot használni kezdené, különben azt kockáztatja, hogy ha túl sok elemet tesz a verembe, akkor a programra leáll: type TSajatVerem = object(TVerem) procedure Hiba_TeleVan;virtual; end; procedure TVerem.Hiba_TeleVan; begin { ezt az eljárást most a végfelhasználó-programozó írja, aki tudja, hogy az alkalmazás karakteres, vagy grafikus, és hibaüzenetet akar kiírni, vagy hangjelzést adni, vagy maga kezeli le a hibás esetet, vagy ... } end; Mivel ezen metódus virtuális, ezért a felüldefiniálás után működni fog az új verzió, lám, a Berak metódust meg tudtuk írni általánosan, pedig nem tudjuk, mit kell tenni általában, ha betelt a verem.
41
A fentiek miatt az absztrakt metódusok mindíg virtuálisak, hogy a felhasználó felül tudja majd a konkrét esetben definiálni. A Delphi-ben ennél egyszerűbb absztrakt metódust definiálni: type TVerem = object ... procedure Hiba_TeleVan;virtual; procedure Berak(X:integer);abstract; ... end; procedure TVerem.Berak(X:integer); begin if not Tele then begin ... end else Hiba_TeleVan; end; Az abstract kulcsszót már az osztály definíciójában fel tudjuk tüntetni, így magát az eljárást meg sem kell írnunk (annak törzse úgyis „üres'' lenne). Ez segít a programozóknak, hogy az osztály definíciójának olvasása közben felfedezzék az abstract metódusokat (ez egyébként csak a kód olvasásából, ill. a dokumentációból derült volna ki), valamint segít a fordítónak is felismerni, hogy ezen osztály tartalmaz absztrakt metódust is. Vannak nyelvek (pl. a Java), melyekben a fordító megakadályozza, hogy olyan osztályból származtassunk konkrét példányt, amelyben vannak absztrakt metódusok. Ezzel a fordító kényszeríti rá a programozót arra, hogy ezeket ténylegesen felül is írja!
Események - absztrakt metódusok Eljárásmutatók A hagyományos Pascal-ban is lehet ún. eljárásmutatókat használni: type TMinMax = function (X1,X2:integer):integer; A fenti példában egy típust definiálunk, melynek neve TMinMax. Ez egy olyan függvényt jelöl, amelynek két integert kell érték szerint átadni, és egy integert ad vissza. Elmondhatjuk, hogy minden olyan függvény, amely megfelel a fenti mintának, ilyen típusú. function SajatMin(A1,A2:integer):integer; far; begin if A1
42
vagy függvény kódjának belépési pontjára (ha nagyon egyszerűen akarnánk fogalmazni, akkor a kezdő begin sorra ☺ ). Ilyen eljárásmutatóknak tehát lehet NIL értéket adni értékül, ill. meg lehet vizsgálni, hogy értékük NIL-e (ez utóbbit nem lehet „if FMin=NIL then …” módon vizsgálni, mert a feltételvizsgálatban kiírt FMin ekkor nem az FMin változót jelentené, hanem a mögöttes függvény hívását eredményezné, mintha a „if SajatMin=NIL then …”-t írnánk). Ezen túl értékül lehet adni nekik egy - a típusnak megfelelő paraméterezésű - függvény vagy eljárás nevét (ezen eljárásnak a FAR módosítóval jelöltnek kell lennie)! Innentől kezdve ezen változó szintén erre az eljárásra mutat, s mint ilyen, rajta keresztül meg lehet hívni ezen eljárást. Ilyen módon lehet pl. megírni egy rendezőalgoritmust általánosan, melynek belsejében a vektor két elemének összehasonlítása (kisebb-e) nem egy „<” jel alapján történhet meg, hanem egy megfelelő eldöntő fv segítségével (melyet futás közben lehet változtatni). A többi ugyanis általánosan megírható: a vektoron végig kell menni, és időnként elemeket cserérlni. Persze erre ritkán van szükség, de hasonló módon lehetett Turbo Pascal-ban megoldani például azt, hogy eljárás lefusson a program befejezésének végén. unit Pelda; interface ... implementation Var DefExit
: Pointer;
Procedure ProgramExit; Far; {meghívódik a program futásának végén} Begin ... ExitProc := DefExit; End; begin DefExit ExitProc ... end.
:= ExitProc; := @ProgramExit;
Ennek során az ExitProc, System unitbeli globális változó értékét használjuk fel. Az ExitProc változó egy paraméterek nélküli eljárásra mutató pointer. Nem típusos pointer - mivel az őt használó programkód assembly-ben íródott meg, és ott ilyen eljárásra mutató pointer típus nem létezik -, de szerepe ugyanez: eljárásra mutat, s amely eljárásra mutat, az az eljárás fog a program befejeztével meghívódni. Ennek értékét eltároljuk a unit inicializációs részének elején a DefExit változónkban, majd értékül adjuk egy saját eljárásunk címét (a @ az „eljárás címe''-t adja vissza). Így gondoskodunk arról, hogy a ProgramExit eljárásunk a program végén meghívódjon. Ezen eljárásban végrehajtjuk azon utasításokat, amelyeket szerettünk volna végrehajtani, majd az ExitProc-ba visszahelyezzük a régi értéket, így gondoskodva arról, hogy az eredeti (System unitbeli) kiléptető eljárás is végrehajtásra kerüljön.
Metódusmutatók A metódusmutatók lényegében eljárásmutatók, amelyek valamely objektum valamely metódusára mutatnak. Hogy miben mások mégis? Egyetlen apró, ám lényeges dologban: pontosan abban, amiben egy metódus különbözik egy eljárástól - a láthatatlan SELF paraméter kezelésében. Metódusmutatókat hagyományos Pascal-ban nem készíthetünk. Más nyelveken, pl. Delphi-ben viszont igen (sőt, igen gyakoriak), mégpedig az alábbi módon:
43
type TEsemenyErtesito = procedure (Sender:TObject) of object; var P:TEsemenyErtesito; procedure Akarmi(Sender:TObject); begin ... end; procedure TVerem.Akarmi(Sender:TObject); begin ... end; var V:TVerem; begin ... P := Akarmi; P := V.Akarmi; end;
{b1} {b2}
{figyelem, ez szintaktikailag hibás!}
Ezen típus most egy olyan „eljárásra'' mutat, amely egy objektum része „of object”), vagyis metódus, és egy TObject típusú paraméterén kívül e miatt van egy láthatatlan SELF paramétere is. A fenti esetben a b1 értékadás szintaktikai hibás, az Akarmi egy közönséges eljárás, mely nem része egyetlen objektumnak sem, így nem fogad self paramétert, s mint ilyen, valójában nem egyezik a paraméterezése a P típusában meghatározott paraméterezéssel. Ezzel szemben a b2 utasítás helyes.
Metódusmutatók és az absztrakt metódusok Az absztrakt metódusok sok szempontból kényelmetlen megoldást kínálnak a náluk megjelölt problémakörre. A programozót arra kényszerítjük, hogy osztályokat írjon, ne pedig használja őket. Ugyanakkor, ha megnézzük az absztrakt metódusok használatát, akkor legtöbb esetben ezek a példában megjelölt szerepet töltenek be: valamely esemény bekövetkeztekor (pl. betelik a verem) kerülnek meghívásra. A mai OOP nyelvekben ilyen esetekben nem absztrakt metódusokat alkalmaznak a programozók, hanem vagy metódusmutatókat, vagy eljárásmutatókat: type TVeremEsemenyErtesito = procedure (var V:TObject) of object; type TVerem = class public Betelt : TVeremEsemenyErtesito; procedure Berak(X:integer); constructor Init; ... end; constructor TVerem.Init; begin ... Betelt := NIL; End;
44
procedure TVerem.Berak(X:integer); begin if not Teli then begin ... end else begin if Assigned(Betelt) then Betelt(self); end; end; ... .... .... procedure TForm1.SajatVeremBetelt(var V:TOBject); begin { ... a V-vel jelölt verem betelt, csinaljunk valamit ...} end; var IntVerem:TVerem; Form1:TForm1; begin ... IntVerem.Betelt := Form1.SajatVeremBetelt; ... end. A használat jól látszik, a programozónak meg kell írni a saját hibakezelőjét, és a verem objektum példány Betelt mezőjét rá kell állítani ezen eljárásra. De ha nem ír saját hibakezelőt, a verem akkor is működni fog, legfeljebb „betelt'' esetben nem történik semmi azon kívül, hogy az elem nem kerül eltárolásra a verembe. De mivel sok esetben ez elég is, előfordulhat, hogy a programozó nem fogja megírni a fenti hibakezelőt. Mint látjuk, a metódusmutatók segítségével fenntartjuk azon lehetőséget, hogy az objektum írását be tudjuk fejezni általános esetre, de a specializáció felé fenntartjuk a nyitottságot. A sors furcsa fintora, hogy a fenti példa a virtuális metódusok kiváltására is szolgálhat! Ha visszaemlékszünk, az absztrakt metódusok mindig virtuálisak. A verem objektumban definiált Berak eljárás mindig a megadott eljárást fogja hívni - ami bárhonnan nézzük is, igen emlékeztet a késői kötés feloldására. Ugyanakkor mivel itt nincs VMT tábla, meg DMT tábla - a kötés késő feloldása gyorsabb, mint a virtuális metódusok kezelése esetén! Hogy ez milyen hasznos eszköz - arra még a vizuális fejlesztőeszközök fejezetben visszatérünk.
Egyéb megoldások A metódusmutatókkal gyakran valamely kevező esetben hívunk meg egy másik metódust. Ezeket gyakran eseménykezelőknek nevezzük. Máskor azonban (mint a fenti példában is) kedvezőtlen esetben hívunk meg egy másik metódust. Ezeket hibakezelők-nek nevezzük. Ilyen hibakezelőt másképpen is meghívhatunk – kivételek segítségével. Amikor az objektum valamely metódusa számára kezelhetetlen esettel találkozik, akkor dob egy hibát, majd ennek következtében a program „viharos” sebességgel visszaszáguld a hívási lánc azon pontjáig, ahol a hibakezelő algoritmus található. Delphi-ben a kivételkezelést a TRY … EXCEPT … END, TRY … FINALLY … END kulcsszavakkal lehet megvalósítani. C++-ban a throw … catch kulcsszavakkal.
45
Osztálymezők és osztálymetódusok Osztálymezők A vegyes nyelvekben - melyek alapvetően nem OOP nyelvek, de van bennük OOP programozási támogatás - a mai napig szükség van hagyományos eljárások és függvények írására, valamint objektumokon kívüli, globális változók deklarálására, és ezek használatára objektumok belsejéből. Például szeretnénk olyat csinálni, hogy megszámolni, hogy a verem objektumból hány példány készült. Mondjuk egy memóriakezelő objektumot írunk, amely a HEAP memóriát kezeli, memóriát allokál és szabadít fel. Ezen objektumból eleve nem lehet kettő darab (mert HEAP-ból is csak egy van). Hogy ezt megakadályozhassuk, a második ilyen objektum létrehozását egyszerűen nem engedjük meg. A fenti példához sajnos globális változót kell definiálnunk, amely kívül áll minden objektum-példányon, belőle csak „egy'' van, akárhány objektumot is készítünk. És a konstruktorban ezen számlálót növelnünk kell egyel: unit UVerem; interface type TVerem = object constructor Create; .... end; implementation var VeremCounter : integer; constructor TVerem.Create; begin inc(VeremCounter); ... end; begin VeremCounter := 0; end. Láthatjuk, hogy nem csak globális változóhoz kellett folyamodnunk, hanem a unit inicializációs részébe21 is kellet hagyományos programozási részt helyeznünk, hogy a globális változónknak legyen kezdőértéke. Sem a Pascal-ban, sem a Delphi-ben nincs erre (egyelőre) külön módszer. Ezekben a nyelvekben a fenti módon lehet egy ilyen problémát megoldani. De sem a Pascal, sem a Delphi nem tisztán OOP nyelv, csak OOP lehetőségeket tartalmazó nyelv. Mivel azonban ezen „változó'' semmilyen formában nem része az objektumnak, ezért a külvilág számára ehhez úgy lehet OOP módon hozzáférni, ha az osztályon belül készítünk egy metódust (függvényt), amelyen keresztül le lehet kérdezni az értékét. Ez viszont elég logikátlan technika, ugyanis deklarálni kell egy objektum-példányt, létre kell hozni, aztán erre a példány segítségével meghívni ezen függvényt, hogy elérjük azt a változót, amely amúgy sem az objektum része!22 Hogyan lehet ezt egy tisztán OOP nyelvben megoldani? A tisztán OOP nyelvekben nincs olyan fogalom, hogy globális változó, és lehetetlen olyan programkódot írni, amely nem valamely osztály metódusán belül van23. 21
a unit végén található begin...end.páros Ezen fából-vaskarika problémára ad megoldást majd az osztály-metódus, lásd lejjebb. 23 ezekben a nyelvekben külön probléma a főprogram megírása, amelyben az első objektumot kell létrehozni, és elindítani annak az első metódusát ☺ 22
46
Ezen nyelvekben - mint amilyen a Java is - osztály-változókkal lehet megoldani. Az osztály-változó olyan „része'' az objektumoknak, amelyek hagyományos értelemben mezői az objektumpéldányoknak24, de közösek: vagyis bármely objektum-példányban hivatkozunk rá, az mindig ugyanazon memóriaterületre való hivatkozás lesz. class TVerem = { ... public static int Szamlalo = 0; ... }25 A fenti példában a Szamlalo mező publikus, vagyis a külvilág felé nyitott, integer típusú, neve Szamlalo, kezdőértéke 0. Már csak a static kulcsszót nem magyaráztuk meg. Ez a kulcsszó éppen a fentieket jelenti, vagyis a Szamlalo mező osztály-változó lesz. Ha több verem objektumot deklarálunk, Szamlalo mezőből akkor is csak egyetlen egy lesz a memóriában – egy közös. Ha bármelyik példány módosítja ezen mező értékét, a többi példány is ezen módosított értéket „látja'' attól a pillanattól kezdve.
Osztálymetódusok Visszatérve a fent megjelölt problémára - hogy az ilyen tulajdonságú mezők eléréséhez metódust kell írni, objektum példányt kell létrehozni, és rajta keresztül elérni a mezőt -, illene erre valami intelligensebb megoldást találni. Erre valók az osztály-metódusok. Ezek jellemzője, hogy bár kötődnek egy osztályhoz, de a nélkül is meg lehet őket hívni, hogy konkrét példányt készítenénk. Pascal-ban ilyet deklarálni sajnos nem lehet. Delphi-ben az osztály-metódusokat a metódus előtti „class'' kulcsszó jelzi: type TVerem = class public class function GetCounter:integer; ... end; Ezen metódusokat az alábbi módon lehet meghívni: begin ... I := TVerem.GetCounter; ... end. Vagyis a metódus előtt nem egy objektum példány neve áll, hanem az osztály neve! De ezen metódusok a hagyományos szintaktikával is hívhatók, hiszen ezek tényleges metódusok, az osztály részei: var T:TVerem; begin ... I:=T.GetCounter; ... end.
24 25
szintaktikai szempontból ugyanúgy lehet rájuk hivatkozni A példa Java nyelven készült.
47
Vagyis az osztály-metódusok használhatóak.
csak
többletszolgáltatást
nyújtanak:
példányosítás
nélkül
is
Osztályhivatkozások < Még nincs kidolgozva >
Objektumok „rokonsága'' - interface Felmerül a kérdés, hogy ha két különböző objektum-osztályunk van – amelyek külön fejlesztési utat jártak be, teljesen különböző öröklődési fájuk van - mennyiben kompatibilisek egymással. A válasz első közelítésben - semennyire. Volt már róla szó, hogy ha egyik osztály a másikból származik, akkor kompatibilis az ősével köszönhető annak, hogy ezen „fejlettebb'' osztálynak mindazon mező és metódus része, ami az ősének megvan. Ha két különböző osztályunk van, a fordító szerint azok nem kompatibilisek egymással. Ez könnyű döntés, könnyen lehet rá fordítóprogramot írni. Ugyanakkor a két objektumosztály lehet valamilyen szinten „egyforma'', lehetnek ugyanazon nevű és típusú mezőik, lehetnek ugyanazon nevű, és paraméterezésű metódusaik. Miért ne lehetne akkor a két osztály „valamilyen'' szinten kompatibilis egymással? Lehet. A megoldást az interface-k biztosítják! Az alább közölt példák szintaktikailag nem lesznek tökéletesek, de most a lényegre koncentrálunk. Az interface első pillantásra úgy néz ki, mint egy objektumosztály: type IRepulniTud = interface procedure Repulj(Magassag,Hossz:integer); end; Az interface valójában felfogható egy olyan osztálynak, amelynek minden metódusa abstract. Vagyis az interface metódusait nem kell kifejteni! A deklaráláskor nem kell a metódusok mögé írni az abstract kulcsszót. Kérdés persze, hogy akkor mi haszna van egy interface-nek? Interface-k segítségével írhatjuk le, hogy két osztály részben egyforma. Pontosan az egyforma részeket kell az interface-ben deklarálni. type TRepulogep=class(TMotorosGep,IRepulniTud) ... procedure Repulj(Magassag,Hossz:integer); end; type TSas = class(TMadar,IRepulniTud) ... procedure Repulj(Magassag,Hossz:integer); end; A fenti példában a TRepulogep osztály egyenesen a TMotorosGep osztályból van származtatva, és implementálja a |TRepulniTud interface-t is! A TSas a TMadar osztályból van származtatva, de szintén implementálja a TRepulniTud interface-t. Így az alapvetően két külön osztály ezen interface erejéig rokon lesz! Egy interface implementálása annyit jelent, hogy az interface-ben megadott minden metódust az adott osztályban adott névvel és paraméterezéssel szerepeltetni kell!
48
Hogyan lehet ezt kihasználni? procedure AfrikabaRepit(var X:IRepulniTud); begin ... X.Repulj(1000,1000000); ... end; Mint látjuk, írhatunk olyan eljárást, amelynek paraméterként egy interface típusú dolgot adhatunk át. A többi már kitalálható - az adott interface-t implementáló osztály példányai kompatibilisek az adott interface-el. Vagyis a fenti eljárás el tud repíteni afrikába egy repülőt, és egy sas-t is! Egy osztály deklarálásakor megadhatunk legfeljebb egy őst (vagy van őse, vagy nincs), és tetszőleges számú interface-t! Például: type TKacsa = class(TMadar,IUszniTud,IRepulniTud,IFutniTud) ... end; A fenti TKacsa a hagyományos módon származtatva van a TMadar osztályból, de implementál egyszerre három interface-t (a kacsa egy nagyon sokoldalú madár ☺ ). Lehet készíteni az adott interface típusú változót is: var Rep:ITudRepulni; begin Rep := TRepuloGep.Create; end; Nyílván az adott interface-ből közvetlenül példányosíthatunk, de példányosíthatunk egy olyan osztályból, amely megvalósítja az adott interface-t, mint a TRepuloGep osztály. A Delphi természetesen ellenőrzi, hogy ez tényleg így van-e, különben fordítási hibát jelez. Ami Delphi-ben teljessé teszi az interface-k deklarálását és használatát azok az alábbiak: Az interface-oknak a megfelelő működés érdekében rendelkeznie kell egy egyedi numerikus azonosítóval: type ITudRepulni = interface ['{10000000-000-000-000-000000000000}'] procedure Repulj(Magassag,Hossz:integer); end; A fenti azonosító egy egyedi GUID azonosító. Erre igazából akkor van szükség, ha ezeket az objektumokat exportálni is akarjuk. „Belső'' használat esetén tetszőleges egyedi sorszám megfelel. Delphi-ben ilyen teljesen egyedi sorszámot a Ctrl-Shift-G lenyomásáva generárltathatunk. Másrészről az interface-t implementáló osztálynak az TInterfacedObject osztályból kell származnia, vagy az interface típust újradeklaráljuk a származtatott osztályban, és az interface metódusait statikus metóduskohoz kötjük
Objektumok rokonsága – többszörös öröklődés A Turbo Pascal, a Delphi, a Java nyelveken nem megenegedett, hogy egy osztálynak közvetlenül több őse legyen. A C++ nyelv azonban ezt megengedi:
49
#include <stdio.h> class elso { public: int a,b; elso(void) {a=1;b=2;} int getb(void) {return b;} };
class masodik { public: int c; float b; masodik(void) {b=1.0;c=3;} float getb(void) {return b;} }; class harmadik:public elso,public masodik { public: int d; harmadik(void) {d=4;} }; A fenti példában a „harmadik” osztálynak egyszerre két őse van, az „elso” és a „masodik”. Egyszerre örökli azok összes adattatgját és metódusát. Ezért ezen osztály példányai egyszerre kompatibilisek mindkét osztállyal Ugyanakkor itt komoly kérdések merülnek fel: a fenti példában az „elso”-nek is volt „b” mezője, amely „int”, vagyis egész típusú volt, a „masodik”-ban is volt definiálva „b” mező, itt azonban az „float” volt, vagyis lebegőpontos. Mi lesz a helyzet a „harmadik”-ban? Elvileg a „harmadik”-ban egyszerre kellene lenni egy „b” nevű float és int típusú mezőnek. Ez pedig általában nem megengedett a programozási nyelvenekben. Ilyet szimplán nem készíthetek C++-ban sem: class proba { public: int d; float d; // szintaktikai hiba – d mező már van void Kiir(void); ... }; Ugyanakkor többszörös öröklődés során ez mégis előfordulhat. A C++ szerint lehet két dudás egy csárdában, ha külön sarokba húzódnak, sosem játszanak egyszerre, és amikor valaki zenét kér, mindig megmondja, melyik dudás játssza el ☺. Ez igen korrekt gondolkodás. A C++ értelmezve van egy fogalom, amelyet „namespace26”-nek hívnak. Ez a névterület definiálja a program szövegének azon „területét”, ahol az adott név (azonosító) használható. Az objektumosztályok mindegyike külön névterületet foglal magának, s az osztály tagjai (mezők, metódusok) csakis ezen a névterületen belül használhatók. 26
„névterület”
50
A névterület megnyitására, feloldására a C++-ban a „::” (dupla kettőspont) szolgál. Amennyiben egy osztály metódusait meg akarjuk írni, akkor is ezt a feloldó jelet kell használni. A feloldás után a metódus belseje már nyitott a névterületre, ezért ott már szabadon hivatkozhatunk a névterület, így az osztály adattagjaira és metódusaira. void proba::Kiir(void) { ... } A fenti esetben a „proba” osztály „Kiir” metódusát akartuk kifejteni. A fenti – többszörös öröklődés során felmerülő mezők duplikálódásával kapcsolatos – probléma feloldása is ezen névterületek kihasználásával történhet. Amennyiben a „harmadik” osztályba tartozó objektum „b” mezőjére akarnánk hivatkozni, a C++ fordító hibát jelezne, mely szerint nem egyértelmű, melyik „b”-re gondoltunk, ezt ő el nem döntheti helyettünk, ezért megtagadja a fordítás folytatását: int main() { harmadik a3; printf("int_b=%d”, a3.b); ... } A megoldás, hogy megmondjuk, melyik „b”-re gondoltunk: int main() { harmadik a3; printf("int_b=%d”, a3.elso::b); return 0; } Vagyis hogy az „a3” objektumon belül megnyitjuk az „elso” névterületet, és azon névterület belsejéből elérjük a „b” mezőt, amely így az „elso.b” lesz. Mostmár világos, miért nem készíthetünk egyetlen osztályon belül két egyforma mezőt - ugyanis mindkettő ugyanazon névterületen belül helyezkedne el! A másik megoldás a típuskényszerítés: int main() { harmadik a3; printf("int_b=%d”, (elso)a3.b); return 0; }27 A metódusokkal kapcsolatban kevesebb ilyen jellegű probléma van C++-ban, mert ott eleve része a nyevnek az „overloading28”, ami azt jelenti, hogy ugyanazon nevű metódus-ból többet is definiálhatok ugyanabban az osztályban – de más paraméterezéssel kell tennem. A fordító az aktuális paraméterlista alapján dönti el, melyiket is kell meghívni. Két osztályból egyszerre történő származtatás során – az osztályok tagjainak összefésülése során – természetesen előfordulhat azonos nevű metódus. Amennyiben ezeknek más a paraméterezése, úgy a probléma máris 27
C++-ban a típuskényszerítés szintaktikája szerint zárójelben le kell írnom a típus nevét azon kifejezés-rész elé, amelynek a típusát meg akarom változtatni. Így a (elso)a3.b pascal-ban valahogy így íródna: „elso(a3).b” 28 „Túlterhelés”
51
könnyedén feloldható29 a szokott módon. Amennyiben a két osztályban azonos nevű és paraméterezésű a két különböző helyről származó metódus, úgy a fenti módszerek mindegyike segíthet pontosan meghatározni, melyik metódust is akarjuk meghívni.
Property Property alapfokon Korábban már volt szó arról, hogy az egységbezárás, a mezők elrejtése problémákhoz vezet: publikus író és olvasó metódusokat kell írni, mely szintaktikai bonyodalmakhoz vezethet, növeli a kód hosszát, és lassítja a futást. Ugyanakkor természetesen előnyökkel is jár - a rejtett mezők változtatását az osztály metódusai „felügyelik'', a változtatások elvégzése csak rajtuk keresztül történhet, így az objektum példány belső állapota, és külvilágbeli állapota konzisztens maradhat. A property (jellemző) valójában egy virtuális metódus. Ennek segítségével fantasztikus dolgokat vihetünk végbe. Lássuk, hogyan: type TPont = class private fx:integer; ... procedure SetX(UjX:integer); public property X:integer read fx write SetX; ... end; A fentiekben egy X mezőt deklarálunk az osztályhoz, mely azonban valójában nincs, fizikailag nem tartozik hozzá memóriaterület. Ugyanakkor szintaktikailag úgy használható, mintha igazi mező lenne: var P:TPont; begin Writeln( P.X ); {c1} P.X := trunc(sin(30)*10+40); end.
{c1}
A fenti két sor között lényeges külöbség van! A „c1'' sorban ezen virtuális mező értékét kiolvassuk. Mivel ilyen mező valójában nincs, ezért hogyan történhet az érték kiolvasása? A property definíciójában megadtuk az olvasás (read) technikáját: amikor a virtuális mező értékét olvasni akarnánk, olvassuk ki helyette az fx ténylegesen létező mező értékét! Vagyis a „c1'' sort a fordító az alábbi tényleges sorra fogja lefordítani: Writeln( P.fx );. Ezt a sort mi ebben a formában nem írhatjuk, hiszen számunkra az fx mező nem létezik (private). De a fordító számára a fentiekkel engedélyezzük a hozzáférést. A „c2'' sor még érdekesebb. Itt az X mezőt írni akarjuk, értékét meg akarjuk változtatni. Ezt a property definíciójának értelmében a SetX metóduson keresztül tehetjük meg. Vagyis a „c2'' sort a fordító az alábbi módon fogja értelmezni: P.SetX(trunc(sin(30)*10+40)), amelyet mi szintén nem írhatunk ezen formában, hiszen a SetX is private. Mint látszik, a property segítségével eltüntethetjük szintaktikailag a rejtett mezőhöz való hozzáférést biztosító egyik eljárást (SetX), de ugyanakkor olvasásra egyenes módon biztosítjuk a lehetőséget közvetlenül az fx mezőből olvasunk. Így egyszerűsödik felhasználáshoz szükséges szintaktika, a sebességet visszanyerjük (legalábbis olvasásra), egyszóval a legtöbb problémánk megszűnik.
29
Ez azért nem annyira egyértelmű – a C++-ban van mód kevesebb paraméterrel meghívni egy függvényt vagy metódust – kihasználva a paraméterek alapértelmezett értékét…
52
Mire jó még ez a lehetőség? Ezzel a technikával megoldható csak olvasható, vagy csak írható mezők készítése. Ugyanis ha csak a read vagy write technikát definiáljuk, akkor csak azon írány fog működni az adott virtuális mező elérésekor: type TIRCKliens = class private fErkezettSor:string; procedure ElkuldEgySort(Uzenet:string); ... public property Erkezett:string read fErkezettSor; property Valasz:string write ElkuldEgySort; .... end; Természetesen az is megoldható, hogy a virtuális metódus mögött egyáltalán ne álljon tényleges mező: type TTcpIpCim = class private function KiolvasIpCim:string; procedure BeallitIpCim(ujTcpIpCim:string); public propery IP:string read KiolvasIpCim write BeallitIpCim; end; Ebben a példában az objektumon belül nincs szükség az IP cím tárolására - a gép IP címét az operációs rendszer fogja eltárolni helyettünk. Másik példa: type TVerem = class private procedurre Berak(X:integer); function Kiolvas:integer; public property Elem:integer read Berak write Kiolvas; end; var V:TVerem; Ebben a példában egy kellemes felhasználással találkozunk, a verem osztályt használó progamozó a
V.Elem:=10 szintaktika segítségével tud a verembe értéket elhelyezni, és a writeln(V.Elem)
szintaktikával tud kiolvasni értéket a veremből. Mivel mindkét esetben valójában metódus hajtódik végre, a verem teli és üres esetek természetesen figyelve vannak! Ilyen property-k használata sokszor akkor is hasznos, ha a read és a write is egyszerű, létező mezőn alapul: type TVerem = class private fHiba_VeremUres : TEsemenyErtesito; ... public property VeremUres:TEsemenyErtesito read fHiba_VeremUres write fHiba_VeremUres; ... end;
53
Ebben az esetben igazából csak a létező belső mezőt „kereszteltük'' át publikus nevén „VeremUres''re, hiszen ezen virtuális mező olvasását és írását is közvetlenül a valódi mező írására és olvasására fordítja le compiler, így sebességcsökkenés nem áll majd fenn. Ugyanakkor ha később az írást ki akarjuk cserélni egy metóduson keresztüli írásra, csak az osztály definícióját kell majd átírni, az osztályt használó programokban a szintakszis nem fog megváltozni!
Property középfokon Amennyiben a virtuális mező „mögött'' tényleges mező rejtőzik, úgy annak illik az osztály konstruktorában értéket adni. Ugyanakkor a vizuális fejlesztőeszközökben a tervezés közben a propertykkel dolgozunk. A fejlesztőrendszernek el kell tudni dönteni, hogy a tervezőfelületen beállított érték ugyanaz-e, mint amit a példány inicializálásakor a konstruktor úgyis be fog állítani (ez néha kifejezés is lehet, melynek értékét a fordító nem biztos, hogy meg tudja határozni). „Segíteni'' kell a fordítónak. Ez úgy történik, hogy a property mögött megadjuk, mi az alapértelmezett értéke az adott virtuális mezőnek: type TPont=class ... public property X:integer read fx write SetX default 0; ... end; var P:TPont; Ezzel jelezzük, hogy a TPont példányokban az X értékét a konstruktor 0-val fogja inicializálni. S amennyiben a tervezés során a fejlesztő programozó beállítja ennek értékét30, az csak akkor kell ténylegesen figyelembe is venni, ha az nem nulla. Ugyanakkor megjegyeznénk, hogy a propertyk használata némi gonddal is jár, hiszen a property mégsem létező mező, ezért nem lehet cím szerint átadni. Még akkor sem, ha mögötte tényleges mező áll, és mind az olvasása és írása közvetlenül ezen mezőn keresztül történik. Ilyen tipikus eljárás az inc, vagyis nem írható az |inc(P.X) utasítás ebben a formában, csak a P.X:=P.X+1!
Property felsőfokon Mi a helyzet akkor, ha a property típusa egy tömb? type TRealLista = class private function Get(Index: Integer): real; procedure Put(Index: Integer; Item: real); public property Items[Index: Integer]: real read Get write Put; end; var L:TList; Ez a deklaráció azt jelenti, hogy pl. az L.Items[10]:=24.32 a fordító a L.Put(10,24.32) formában fogja értelmezni. Ugyanakkor a X:=L.Items[I]/2 sort X:=L.Get(I)/2 formában hajtja végre. Amennyiben a fenti property-t definiáló sor végére a default kulcsszót is használjuk, úgy ez a property lesz az alapéertelmezett property, így az items mezőnevet nem is kell használni: a fenti sorokat L[10]:=24.32 és X:=L[I]/2 formában is írhatjuk. Valójában elég szerencsétlen a Delphi-től, hogy ezt a kulcsszót is ugyanúgy hívják, mint az alapértelmezett értéket jelölő esetben. Megkülönböztetésképpen ezen „default'' után nem áll érték, míg a mező induláskori értékét jelölő esetben áll. 30
Pl. Delphi-ben az Object Inspector segítségével
54
Ugyanilyen módon lehet több dimenziós tömböket is készíteni property-k segítségével: type TRealMatrix = class private function Get(Index1,Index2: Integer): real; procedure Put(Index1,Index2: Integer; Item: real); public property Items[Index1,Index2: Integer]: real read Get write Put; end;
Vizuális fejresztőrendszerek működése Rövid történeti kivonat A legrégebbi fejlesztőezközök több különálló alkalmazásból épültek fel. Nem is mindegyikhez mellékeltek editor-t31, ha igen, az sokszor igen primitív volt. Aztán adtak egy fordítót (compiler), melyet parancssorból kellett megfelelő paraméterezéssel elindítani a projekt moduljaira. A lefordított tárgykódú programokat a szerkesztő (linker) alakította át egyetlen futtatható állománnyá32. Az elkészített futtatható file tesztelését egy külön nyomkövető (debugger) tette lehetővé. Ezen programok mindegyikét külön kellett elindítani - jobb esetben batch fileban összefoglalva az utasítások sorozatát. A nyelv dokumentációja (kulcsszavak, beépített eljárások és függvények paraméterezése és működése) sokszor csak nyomtatott könyv formában volt meg, jobb esetben elektronikus formában egyetlen hosszú szöveges fileban mellékelték. Aztán kijöttek az integrált fejlesztői eszközök, melyek közül a legjobbak vitathatatlanul a Borland cég által készített nyelvekhez jelentek meg (Turbo Pascal, Borland Pascal, Borland C, stb...). Ezen integrált fejlesztőeszközök (IDE - Integrated Development Environment) a fent felsorolt funkciókat egyetlen programban valósította meg, nem kellett kilépni belőle az operációs rendszer szintjére. A funkciók aktivizálása menüpontokon, forró billentyűkön keresztül történt, vagy automatikus volt. A súgó (help) elérése is belső volt, sőt, megjelent a hiperhivatkozási rendszer. Ezen fejlesztői eszközökben a fejlesztés lényegesen kényelmesebb és egyszerűbb volt, ugyanakkor bizonyos feladatok elvégzése továbbra is kényelmetlen volt: ezek közül egyik legelső és legfontosabb az ún. „képernyő-tervezés''. Ennek során a program által végzett I/O műveletek eredményeinek elhelyezkedése a karakteres képernyőn, a színek beállítása programkódból történt, és a tesztelés során figyelni kellett, hogy a kód megfelelő módon generálja-e ezt a részt. A grafikus felüleletk megjelenésekor a probléma még összetettebb lett - nem 80x25 méretű táblázatban kellett a dolgokat elhelyezni, hanem finomabb rácsfelbontáshoz kellett tervezni. Ugyanakkor a színeken túl a betűtípusok használatát is meg kellett fontolni - és tesztelni. A másik fontos probléma a tervezésen túl a rengeteg kezdő értékadás kellett leírni, mely legtöbb esetben e tervezés részeként került be a kódba (képernyőkoordináták, stb). Ezekhez képernyőtervező segédprogramok készültek - külső eszközökként, melyek használata az IDE-n belül nem volt támogatva. Ezen eszközök a tervezés után generáltak egy adott programozási nyelvhez készült helyes szintaktikával ellátott programkód-részletet, melyet be kell szúrni a végső kódba. Ennek használata kissé kényelmetlen volt. A programkódban a tervezés eredménye során generált kód jelen volt, mely hosszabb projekt esetén kényelmetlenül növelte a programszöveg hosszát. A vizuális fejlesztő rendszerek gyakorlatilag integrált eszközök, kiegészítve képernyőtervező és kódgeneráló funkciókkal (és még sok mással is). 31 32
ezeket komoly hiba szövegszerkesztőknek hívni - azok egy kissé többet tudnak az editoroknál ☺ ez néha nem is egy darab állomány volt
55
A Delphi A Delphi is ilyen rendszer. A Delphi alapnyelve Pascal, melyet az OOP keretein belül jelentősen kibővítettek, és még kiegészítettek sok olyan dologgal, amely a Windows platform programozásához szükséges. A Delphi nem tisztán OOP nyelv. Ez azt jelenti, hogy lehetőség van hagyományos eljárások és fügvények készítésére, hagyományos globális változók definiálására. Ezen technikákat objektumokon belül is elérhetők (metódus belsejéből el lehet indítani hagyományos eljárásokat, el lehet érni a globális változókat). Éppen ezért a DELPHI program indulási pontja a hagyományos Pascal program felépítésében szereplő főprogram. A különbség csupán az, hogy a Delphi főprogramot tartalmazó modul kiterjesztése .DPR (Delphi PRoject file). A Windows platformra írt programok (alkalmazások) általában egy ablakot készítenek, azon keresztül kommunikálnak a felhasználókkal. Az ilyen ablakot a Delphi terminológiában FORM-nak nevezik. Egy Delphi program szinte mindíg tartalmaz legalább egy Form-t. Minden form egy külön modulban (unitban) található. A form-t kezelő kód alkotja a unit-ot (.PAS kiterjesztés), és a tervezés során beállított mezőértékek (leggyakrabban property-k értékeit) egy .DFM (Delphi ForM) kiterjesztésű fileban vannak letárolva. Ezen túl egy .RES (RESource) file is tartozik, amelyben (alap esetben) az alkalmazás ikonja van eltárolva. Egy egyszerű Delphi program tehát áll egy-egy DPR, PAS, DFM, RES fileból. A Delphi értékét a kifinomultan kidolgozott IDE-n túl a rendkívül bőséges objektum-gyűjtemény adja meg. A Turbo Pascal-hoz „gyárilag'' mindössze a SYSTEM,CRT,DOS,GRAPH,PRINTER unitok voltak mellékelve. A Delphi-hez mellékelt objektum-osztályok felsorolása poszter méretű lapot igényel. A Delphi programozók legnagyobb problémája kiválasztani a feladathoz legmegfelelőbb objektumot a gyűjteményből - vagy letölteni az internetről. Saját objektum fejlesztésére szinte soha nincs szükség. A Delphi objektumok két csoportba sorolhatók. A vizuális és nem vizuális objektumokra. A vizuális objektumok a tervezés során „egérkattintások'' révén kerülnek be a programba, őket a komponenspalettáról lehet kiválasztani, és a mezőik (property) értékét a tervezés során (Object Inspector) be lehet állítani. Valójában éppen ezeket az értékeket tárolja el a Delphi a DFM fileokban (természetesne kivéve a default értékeket). Ez a külső tárolás azért nagyon hasznos, mert ezen értékadások így nem kerülnek be a program kódjába, nem kell a programozónak kerülgetni (átlapozni). Ezen értékek, értékadások belefordítódnak a kész programba, és a program indulásakor kerül beállításra. Valahányszor új objektumot adunk a formhoz hozzá, a Delphi generál egy újabb változót a program kódjában. A form objektum definíciójának első része alapértelmezettben published láthatóságú, ami a public egy apró módosítása. Ezen részt a Delphi automatikusan generálja, és „nem szereti'', ha ezen a részen a programozó kézzel különböző beavatkozásokat végez. A felrakott objektumok nevét az object inspector-n keresztül „illik'' megváltoztatni (name property), ugyanis az objektum neve nem csak a program kódban (.pas) van eltárolva, hanem a dfm file-ban is. Az object inspector-ban elvégzett névmódosítás során mindkét helyen egyszerre kerül módosításra a név. A felrakott vizuális objektumokat a form konstruktora a dfm file alapján automatikusan létrehozza, és a form destruktora elpusztítja. Ezen kívül lehetőség van menet közben dinamikusan is létrehozni ilyen objektumokat, melyek a konstruktorban paraméterként megadott konténer jellegű33 objektum listájára34 felkerül, így a konténer desktruktorában fog megszünni:
33 34
olyan objektum, amely más objektumokat hordozhat, tipikus példa a TPanel, de ilyen a TForm is .Components lista
56
procedure TForm1.FormCreate(Sender: TObject); var B:TButton; begin B := TButton.Create(Self); {a nyomógomb tulajdonosa (owner) maga a form} B.Parent := Self; {a nyomógomb szülője is a form lesz} B.Caption := 'OK'; end; Ez a példa első olvasatra hibásnak tűnhet, hiszen a B lokális változó, melyhez hozzárendelünk egy új objektumot, de azt nem szabadítjuk fel, holott az eljárás lefutása után a B eltűnik a memóriából, de a mögöttes lefoglalt memóriaterület az objektum mezőinek marad. De ezen nyomógomb a form megszűnésekor, azzal együtt fog megszűnni a nyomógomb destruktorának meghívása révén. A nem vizuális objektumok létrehozása ugyanakkor csak ezzel a módszerrel történhet. Ezen objektumokat nekünk kell „kézzel'' létrehozni, és mivel ezeknek nincs tulajdonosuk, így a megszüntetésükről is nekünk kell gondoskodni. Ilyen objektum pl. a TList. Amennyiben ilyet szeretnénk használni, úgy nekünk kell felvenni egy ilyen típusú változót, és nevet adni neki. Mivel ezen változó tulajdonságainak beállítását nem végezhetjük az object inspector-n keresztül, nem is tárolódik róla semmilyen információ a dfm fileban. Ezért ezen változók nevét szabadon állíthatjuk be, és szabadon gazdálkodhatunk vele. Egy ilyen változót több helyen is deklarálhatunk: type TForm1=class(TForm) ... private L1:TList; {pl. itt} public L2:TList; {vagy itt} end; A kettő között az a különbség, hogy a private részben deklarálás esetén egy másik unitból, másik formból nem lenne hozzáférhető. A public-beli deklaráció esetén persze igen. Az ilyen helyeken deklarált változókat általában a form OnCreate eseményében hozzuk létre, és az OnDestroy eseményében szüntetjük meg: procedure TForm1.FormCreate(Sender: TObject); begin L1 := TList.Create; L2 := TList.Create; end; procedure TForm1.FormDestroy(Sender: TObject); begin L1.Free; L2.Free; end; Ezen túl lehetőségünk van egyszerű globális változóként is deklarálni: .... implementation var L3:TList; Az ilyen változókat szintén inicializálhatjuk a form OnCreate és OnDestroy eseményeiben, bár amennyiben a form-t menet közben dinamikusan hozzuk létre és szüntetjük meg (egy futás alatt akár többször is), ezen objektumok a formmal együtt jönnek létre és szünnek meg. Viszont ha a form-ból
57
egyszerre több példányt készítünk (nem túl gyakori jelenség), akkor elvileg többször is lefuthat a form OnCreate eseménye, miközben az OnDestroy egyszer sem, és ez eléggé hibás, mivel a globális változó végig ugyanaz marad. Ezért az ilyen változókat a unit inicializációs szakaszában szokták letrehozni, és hasonló helyen megszüntetni: unit Unit1; interface ... implementation ... initialization L3 := TList.Create; finalization L3.Free; end. Mint látszik, a unit formájának szintakszisát kibővítették Delphi-ben, a unit végi „főprogramot'' most nem a „begin'' jelzi, hanem az initialization, és felbukkan egy új dolog, a finalization szakasz, amelyben foglaltak a program kilépése előtt kerülnek végrehajtásra (ezen utóbbit Pascal-ban csak a trükkös ExitProc segítségével lehetett elérni). Amennyiben az ilyen globális változót a unit interface részében deklaráljuk, úgy külső modulokban is elérhető lesz - természetesen „simán'', mindenféle objektum mezőhivatkozás nélkül.
Delphi objektumhivatkozási modell Delphi-ben az objektum példányok deklarálása kevés memóriába kerül - minden példány valójában pointer típusú, s mint ilyen, 4 byte memóriaigényű. Az objektum inicializálásakor (a konstruktor meghívásakor) kerül maga az objektum méretéhez szükséges memória lefoglalásra, és a példány változó felveszi ezen memóriaterület kezdőcímét. A konstruktor hívása itt szintén meglepő formájú: az osztály nevével lehet a konstruktort meghívni. Pl: var L:TList esetén L:=TList.Create. Az ilyen jellegű konstruktor hívás tevékenységi köre kettős: memóriát allokál, és lefuttatja a megadott konstruktor-eljárást is. Ha a hagyományos módon hívjuk meg a konstruktort (L.Create), akkor csak a konstruktor fog (újra) lefutni, újabb memória allokálására nem kerül sor. Delphi-ben a konstruktor neve jellemzően Create. Az objektumok megszüntetését a „.Free” metódus végzi. A Free kódja egyszerű, meghívja a Destroy nevű destruktort. A destruktor virtuális. Persze lehet közvetlenül a destruktort is hívni (L.Destroy), de ez hibás lesz akkor, ha az L példány nem inicializált. Ekkor ugyanis nincs hozzá VMT tábla csatolva, és nem lesz feloldható a virtuális metódus, a destruktor hívása. A Free viszont „közönséges'' metódus, korai kötés, mindíg meghívható, nem inicializált példányokra is. És a Free ellenőrzi, hogy a példány inicializált-e (if assigned(self) then ...), s csak ezen esetben hívja meg a virtuális destruktort. Ezért a Free használata biztonságosabb, és javasolt. Ugyanakkor a példány pointer volta nem tűnik ki a szintakszisból. Bár az L egy pointer, de nem L^.Free formájában használjuk, hanem L.Free formában, mintha közönséges objektum lenne. De az L maga nem egy objektum, hanem hivatkozás egy objektumra.
CLASS kontra OBJECT Delphi-ben objektumok készítésére két kulcsszót használhatunk, a CLASS és az OBJECT kulcsszót. A kettő között mindössze annyi a különbség, hogy a CLASS a fent megjelölt objektumhivatkozási modellt használ, míg az OBJECT a hagyományos Pascal-beli objektum.
58
Vagyis: type TAkarmi=class(...) ... end; var L:TAkarmi; Az L itt egy pointer, használatához L:=TAkarmi.Create kell, használat végén L.Free, és használható az if L=NIL then ... és az |if assigned(L) then ... formák. Ezzel szemben: type TMasik = object(...) ... end; var X:TMasik; A X egy hagyományos Pascal-beli objektum, rögtön memóriafoglalásra kerül sor, inicializálni a X.Create-el lehet (konstruktor futtatása), felszabadítani lényegében nem lehet (a memóriaterület végig foglalva lesz), a destruktorát mindenesetre le lehet futtatni X.Free formában. type PMasik = ^TMasik; var P:PMasik; Ezzel a formával lehet belőle dinamikus objektumot készíteni, és a szokásos New(P,Create) formában lehet allokálni neki helyet, és a Dispose(P,Destroy) formában felszabadítani. Használat közben a P^.Metodushivas formát lehet használni. Ekkor lényegében megkapjuk a class esetét, bár sokkal kényelmetlenebben kezelhető formában.
Néhány típus-változás A Delphi-ben (a v2.0 változattól kezdve) apróbb eltérések vannak a Pascal-beli típusokhoz képest: Delphi
Pascal
Integer Cardinal Shortint Smallint Longint Int64 Byte Word Longword
-2147483648..2147483647 0..4294967295 -128..127 -32768..32767 -2147483648..2147483647 -2^63..2^63-1 0..255 0..65535 0..4294967295
ShortString AnsiString WideString PChar string
255 characters kb 2^31 characters, max 2GB, 8-bit (ANSI) characters kb 2^30 characters, max 2GB, Unicode characters (zero-terminated string, max 64Kb) alapértelmezett beállítások esetén ez az AnsiString
TDateTime TDate
signed 32-bit unsigned 32-bit signed 8-bit signed 16-bit signed 32-bit signed 64-bit unsigned 8-bit unsigned 16-bit unsigned 32-bit
longint shortint integer longint byte word string -
(valójában Double, dátum és idő tárolására) (valójában TDateTime, 1899.12.30 óta eltelt napok száma) -
Események, eseménykezelők Delphiben gyakran használt fogalom az eseménykezelő. Az eseménykezelő valójában egy metódus, mely valamely esemény bekövetkeztekor kerül meghívásra. Az eseménykezelés nem csak az absztakt metódusok kiváltására készült, hanem annak megakadályozására, hogy a programozónak folyton OOP-t kelljen programoznia, osztályokat módosítva, virtuális metódusokat módosítva. Így ha pl. egy nyomógomb objektumot nézünk, és szeretnénk valamit csinálni akkor, amikor a nyomógombra rákattintanak, „hagyományos'' OOP gondolkodásmód mellett készíteni kellene egy TButton
59
továbbfejlesztést (származtatást), ahol pl. egy Click virtuális metódust módosítanánk. Viszont egy ablakban sok nyomógomb is lehet, mindegyikhez külön továbbfejlesztést készíteni pedig sok és áttekinthetelen munka. A Delphi objektumaiban elterjedt technika a metódusmutatók használata. A TButton is úgy van megírva, hogy ilyen események bekövetkeztekor metódusmutatón keresztül hívjon meg egy eseményhez csatolt eljárást. Ezen metódusok az eseménykezelők. Az események informatikai terminológiában event-ek, így ezek az event handler metódusok. Az eseménykezelők is metódusok, általában a „nagy'' hordozó, a form valamely metódusa. Pl. egy nyomógomb metódusa lehet egy Button1OnClick form-beli metódus. Ennek következtében igazából mindig a Form osztályt fejlesztjük csak tovább – ha új metódust írunk, akkor mindig a Form osztályhoz írjuk. E közben valójában a vizuális objektumokat fejlesztjük tovább – azok absztrakt metódusait és virtuális metódusait helyettesitő metódusmutatóit állítjuk be sorba a Form megfelelő metódusaira, amely paraméterként megkapja az erdetei objektum Self mutatóját, e miatt szinte teljes értékű továbbfejlesztésnek minősül.35 Ez nagyon kényelmes eszközzé fejlődött ki a Delphi-ben, ugyanis ilyen eseménykezelő létrehozásához mindössze tervezés közben ki kell választani a szóban forgó objektumot (pl. Button1), majd az object inspector-ban kiválasztani ennek az OnClick eseményét, kettőt kattintva a Delphi elkészíti ezen eseménykezelő deklarációit, készít egy üres törzzsű megfejlécezett eljárást (pl. Button1OnClick névvel). A legtöbb ilyen eseménykezelő metódusnak mindössze egyetlen paramétere van, egy Sender, amely az eseményt kiváltó objektum példányt azonosítja, konkrétan a Sender a példány Self-je. A Sender segítségével lehet azonosítani, melyik konkrét példány váltotta ki az eseményt (amennyiben ugyanezen eseménykezelő több példányhoz is hozzá lenne rendelve). Az azonosítás során az „is” típusellenőrzés, „as” típuskonverzió lehet nagy hasznunkra, vagy a közvetlen azonosítás (if Sender=Button1 then ...). Ezen túl sokszor hasznos lehet a Tag mező is, amely minden vizuális komponensnek létező mezője, integer típusú, és amúgy semmire sem használja a Delphi. De mi akár minden egyes objektum-példányhoz saját egyedi sorszámot adhatunk a Tag révén, és az alábbi módon gyors esetszétválasztást tehet lehetővé: begin case (Sender as TComponent).Tag of ... end; end;
Eseményvezérelt programozás Windows alatt az alkalmazások élete események bekövetkeztekor zajlik. Windows alatt nem szokás az erőforrásokat (egér, billenytűzet, nyomtató, memória, stb.) kisajátítani, mert a Windows többfeladatos operációs rendszer, és „egyszerre'' több program is fut(hat), így ezen erőforrásokon osztozni illik. Ezért egyetlen program sem figyeli „végtelen'' ciklusban pl. az egér mozgását, hogy reagálhasson a változásokra, hanem várja, hogy a Windows „szóljon'' neki, ha az egér kapcsán valami változás történik. Ugyanis azt a Windows „dönti'' el, hogy melyik programnak szól, ha megmozdítják az egeret (vagy annak szól, akinek az ablaka fölött van éppen az egérkurzor, vagy csak az aktuális alkalmazásnak szól, vagy ...). A Windows folymatosan küldi az üzeneteket az alkalmazásoknak, akik gyorsan reagálnak az eseményre, és gyorsan visszaadják a vezérlést a Windows magjának (kernel), hogy az tovább figyelhesse az erőforrásokat, és küldhesse a következő üzeneteket. Amennyiben ezt nem tennék, úgy a rendszer „lefagyna'', nem kerülnének feldolgozásra az események (a gép nem reagál a leütött billentyűkre, stb). A Windows 3.1 még a cooperative technikát támogatta, melynek lényege, hogy az egyszerre elindított alkalmazások együttműködnek a multitaszk megvalósításához, minden alkalmazás igyekezett röviden 35
De nem érhetjük el az eredeti osztály private és protected részeit.
60
futni, és barátságosan átadni a vezérlést a következőenk36. Egy ilyen rendszerben, ha valamelyik alkalmazás „lefagyott'', végtelen ciklusba került, akkor az egész rendszer lefagyott, mert a vezérlés a végtelenségig a hibás alkalmazásnál maradt. A Windows '95-el megjelent a preemptive multitaszk, amelyben egy központi ütemező dönti el, melyik alkalmazásnak mennyi időszelet jut. Ha a rá szabott időszelet letelt, az ütemező átadta a vezérlést egy másik alkalmazásnak. Így elvileg, ha az egyik alkalmazás lefagy, a többi attól még fut tovább37. A Windows NT vonalon ezen elv megvalósítása valamelyest jobban sikerült. Mindenesetre egy alkalmazás élete mindíg erősen függ a Windows üzenetküldési lehetőségeitől. Minden Windows alkalmazás ezért lényegében nem más, mint egy nagy ciklus: CIKLUS HA Van_Uzenet_A_Varakozasi_Sorban X := Uzenet X feldolgozasa HVÉGE CVÉGE_HA PROGRAM_VEGE_JELLEGU_UZENET_JOTT Az X feldolgozása közben meg kell határozni, melyik az aktív form, azon belül melyik az aktív objektum (akinél a fókusz van), és elküldeni ezen objektumnak a megkapott üzenetet. Ezen üzenet nem mindíg érdekes az adott objektum számára. Pl. egy nyomógomb ritkán „érdeklődik'' az „egeret balra húzzák feletted'' jellegű üzenetek iránt, míg annál inkább foglalkozik a „kattintottak' fölötted' jellegű üzenetekkel. Ha egy ilyen üzenet érkezik, elképzelhető, hogy a program egy hosszabb feldolgozási lépésbe kezd, pl. egy hatalmas méretű tömböt kezd el rendezni. Ez egy sokáig futó (általában FOR) ciklus indítását jelenti. Amíg az alkalmazás ezen ciklusában dolgozik, nem fogadja az üzeneteket. Ezzel nincs is túl nagy baj, mert az üzenetekhez egy nem túl kicsi méretű várakozási sor tartozik. Igen ám, de ha a programban van egy „Megszakítás'' jellegű nyomógombot, melynek segítségével a felhasználó megszakíthatná a rendezést, és a felhasználó közben rákattinth erre az egérrel, az ezzel kapcsolatos esemény bár elküldésre kerül az alkalmazás felé, de az jelenleg nem fogadja az üzeneteket. Így nem venné észre a kattintást. Ezért az ilyen jellegű feldolgozások során illik többször visszatérni a a fenti ciklus magjába, hogy az eseménykezelés ne álljon le. Persze nem minden lépésben, mert akkor nagyon lelassulna a rendezés, hanem mondjuk minden belső ciklus lefutása után (ezt mindíg az adott algoritmus dönti el, túl sűrűn sem jó, túl ritkán sem jó). Ezen esemény-ellenőrzést az Application.ProcessMessages metódushívással lehet kiváltani.
36 37
ez sokban emlékeztet a token-ring hálózatok működési elvére az elv jó volt, a megvalósítás nem annyira ☺
61
OOP - Turbo Pascal 7.0 A Turbo Pascal v7.0 alábbi jellemzőkkel bír az OOP lehetőségei közül: •
Objektum-osztály deklarálása: type TVerem = object private vm : integer; T : array [1..10] of integer; public constructor Create; destructor Done; procedure Berak(x:integer); function Kivesz:integer; end;
•
Metódusok kifejtésekor meg kell adni az osztály nevét is: constructor TVerem.Create; begin ... end; destructor TVerem.Done; begin ... end; procedure TVerem.Berak(x:integer); begin ... end;
•
Objektum-példány deklarálása vagy statikus vagy dinamikus módon történhet: var IntVerem : TVerem; type PVerem = ^TVerem; var PIntVerem : PVerem; var PIntVerem : ^TVerem;
•
Dinamikus objektumok inicializálása és megszüntetése: new( PIntVerem, Create ) { konstruktor hívása opcionális } ... dispose( PIntVerem , Done ); { destruktor hívása opcionális }
•
Hivatkozás a példány tagjaira (mezők, metódusok): minősítő operátorral IntVerem.Berak( ... ); { statikus példány } PIntVerem^.Berak( ... ); { dinamikus példány }
•
Származtatás: csak egyszeres öröklődés megengedett, egy osztály több származtatott osztálynak is lehet őse, de egy gyermek-osztálynak csak egyetlen közvetlen őse lehet (az öröklődési hierarchia fa alakú).
•
Osztályok közötti rokoni kapcsolatok: semmilyen formában nem támogatott.
•
Osztálymetódusok készítése: semmilyen formában nem támogatott.
•
Osztályváltozók készítése: adott fordítási modulban deklarált „globális'' változókon keresztül támogatott.
•
Osztályhivatkozások kezelése: semmilyen formában nem támogatott.
62
•
Konstruktorok nem lehetnek virtuálisak. A konstruktoroknak tetszőleges név adható. Elterjedt az Init mint konstruktornév.
•
Destruktorok lehetnek virtuálisak. A destruktorok neve tetszőleges lehet. Elterjedt a Done mint destruktornév.
•
Késői kötés: mind a virtuális mind a dinamikus metódusak használata megengedett. A metódusindex egész típusú érték kell legyen. type ... = object ... procedure VirtualisMetodus; virtual; procedure DinamikusMetodus; virtual MetodusIndex; ... end;
•
Virtuális metódusok felüldefiniálása: o a virtualitás megtartásával: a származtatott osztályban a virtual kulcsszót kell használni. A paraméterezést nem lehet megváltoztatni. o a virtualitás megszüntetésével: nem megengedett, fordítási hibát kapunk. o visszatérés a virtualitáshoz: (előző pont értelmében nem vizsgálható)
•
Absztrakt metódusok készítése: lehetésges, de nem deklaráció szintjén. Közönséges metódusként kell deklarálni, s a törzsében az abstract; hagyományos Pascal eljárást kell meghívni, mely futási hibát generál, és leállítja a program futását.
•
Példányosítás absztrakt metódusokat tartalmazó osztályból: megengedett, figyelmeztető üzenetet sem kapunk.
•
Eseménykezelők készítése: o eljárásmutatókkal támogatott o metódusmutatók készítése nem támogatott.
•
Többszörös öröklődés: semmilyen formában nem támogatott.
•
Interface használata: semmilyen formában nem támogatott.
•
Property készítése: semmilyen formában nem támogatott.
•
Példány adatainak elérése a metódusokon belül: automatikus, egyébként a self kulcsszó segítségével.
•
Adatrejtés: o Public : az elérés a modulon kívül is garantált. o private: az elérés csak a modulon belül garantált.
•
Objektumok kompatibilitása: támogatott, a származtatott példányok bármely ősükkel kompatibilisek.
•
Futás közbeni típusellenőrzés: támogatott, a typeof kulcsszó segítségével.
•
Típuskonverzió: támogatott, a típusnév segítségével, pl: TVerem(Sender).Kivesz;
•
Vizuális fejlesztés: nem támogatott.
•
Közös ős az öröklődésben: nincs.
63
OOP - Delphi A Borland (Inprise) Delphi az alábbi jellemzőkkel bír az OOP lehetőségei közül: •
Objektum-osztály deklarálása: két módon type TVerem = class private vm : integer; T : array [1..10] of integer; public constructor Create; destructor Done; procedure Berak(x:integer); function Kivesz:integer; end; vagy a class helyett az object kulcsszó segítségével. A class használata esetén a példányok csak objektum-referenciák (mutató), az object esetén a példányok az adatterületet azonosítják.
•
Metódusok kifejtésekor meg kell adni az osztály nevét is: constructor TVerem.Create; begin ... end; destructor TVerem.Done; begin ... end; procedure TVerem.Berak(x:integer); begin ... end;
•
Objektum-példány deklarálása vagy statikus vagy dinamikus módon történhet: var IntVerem : TVerem; {TVerem egy object } type PVerem = ^TVerem; var PIntVerem : PVerem; var PIntVerem : ^TVerem; var Verem : TClassVerem;{ TClassVerem egy class, ez eleve dinamikus }
•
Dinamikus objektumok inicializálása és megszüntetése: new( PIntVerem, Create ) { konstruktor hívása opcionális } ... dispose( PIntVerem , Done ); { destruktor hívása opcionális } Verem := TClassVerem.Create; ... Verem.Free;
•
Hivatkozás a példány tagjaira (mezők, metódusok): minősítő operátorral IntVerem.Berak( ... ); { statikus példány } PIntVerem^.Berak( ... ); { dinamikus példány } Verem.Berak( ... ); { class típusú példány }
•
Származtatás: csak egyszeres öröklődés megengedett, egy osztály több származtatott osztálynak is lehet őse, de egy gyermek-osztálynak csak egyetlen közvetlen őse lehet (az öröklődési hierarchia fa alakú).
64
•
Osztályok közötti rokoni kapcsolatok: interface-n keresztül.
•
Osztálymetódusok készítése: támogatott, class of ... formában. type ...... = class ... class of function GetCounter:integer; end;
•
Osztályváltozók készítése: adott fordítási modulban deklarált „globális'' változókon keresztül támogatott.
•
Osztályhivatkozások kezelése: támogatott. Az osztályhivatkozások .......
•
Konstruktorok nem lehetnek virtuálisak. A konstruktoroknak tetszőleges név adható. Elterjedt az Create mint konstruktornév.
•
Destruktorok lehetnek virtuálisak. A destruktorok neve tetszőleges lehet. Elterjedt a Destroy mint destruktornév.
•
Késői kötés : mind a virtuális mind a dinamikus metódusak használata megengedett. A metódusindex egész típusú érték kell legyen. type ... = class ... procedure VirtualisMetodus; virtual; procedure DinamikusMetodus; dynamic; procedure DinamukusMetodus; dynamic; ... end; Az öröklődés következő szintjén már nem a fenti két kulcsszót kell használni, hanem az override kulcsszót mindkét esetben. Amennyiben az object stílust használjuk, ott a Pascal-ban leírtak igazak, továbbiakban is a virtual vagy dynamic kulcsszavakat kell használni.
•
Virtuális metódusok felüldefiniálása: o a virtualitás megtartásával: a származtatott osztályban az override kulcsszót kell használni. A paraméterezést nem lehet megváltoztatni. o a virtualitás megszüntetésével: megengedett, bár fordítási figyelmeztető üzenetet kapunk, melyet az újradeklarálás helyén szereplő reintroduce kulcsszóval tüntethetünk el. o visszatérés a virtualitáshoz: újra a virtual vagy dynamic kulcsszóval, de az eddig a szintig definiált metódusok nem látják újra a virtuális metódusokat. o
Absztrakt metódusok készítése: lehetésges deklaráció szintjén. A metódusfeje után az abstract kulcsszót kell használni.
•
Példányosítás absztrakt metódusokat tartalmazó osztályból: megengedett, figyelmeztető üzenetet kapunk.
•
Eseménykezelők készítése: támogatott. Az eseménykezelők definiálását az „... of object” kulcsszavakkal kell zárni, mert különben nem metódusmutatók lesznek, hanem
eljárásmutatók. •
Többszörös öröklődés: nem támogatott.
•
Interface használata: támogatott. Az interface-t implementáló osztálynak garantáltan minden metódust implementálni kell az interface-ből.
65
•
Property készítése: egyszerű és tömb típus is támogatott.
•
Példány adatainak elérése a metódusokon belül: automatikus, egyébként a self kulcsszó segítségével.
•
Adatrejtés: o published: a public speciális változata, az object inspector számára is elérhető public láthatóság. o public: az elérés a modulon kívül is garantált. o protected: a modulon belül, és a származtatott osztályokat tartalmazó modulokban is. o private: az elérés csak a modulon belül garantált.
•
Objektumok kompatibilitása: támogatott, a származtatott példányok bármely ősükkel kompatibilisek.
•
Futás közbeni típusellenőrzés: támogatott, a „typeof” és az „is” kulcsszavak segítségével.
•
Típuskonverzió: támogatott, a típusnév segítségével, pl: TVerem(Sender).Kivesz vagy az „as” kulcsszó segítségével.
•
Vizuális fejlesztés: támogatott. Az object inspector segítségével lehet a példányok induláskori értékét és az eseménykezelőket definiálni. A beállítások külön file-ban tárolódnak (dfm).
•
Közös ős az öröklődésben: van, minden osztály a TObject-ből származik.
66
OOP – C++ A C++ az alábbi jellemzőkkel bír az OOP lehetőségei közül: Osztályváltozók: léteznek: static int x; Osztálymetódusok léteznek: static int Count(void) { … }
67
Melléklet: {** ................................................................ {** DELPHI program a virtualitás megszüntetéséről és visszatéréséről {** ugyanezen program nem működik, ha nem CLASS hanem OBJECT típust {** használjuk. {** ................................................................ program Delphi_Program; type TElso=class x:integer; procedure A; procedure B;virtual; constructor Create; end; type TMasodik = class(TElso) procedure B;reintroduce; end; type THarmadik = class(TMasodik) procedure B;virtual; end; constructor TElso.Create; begin end; procedure TElso.A; begin B; end; procedure TElso.B; begin X:=1; end; procedure TMasodik.B; begin X:=2; end; procedure THarmadik.B; begin X:=3; end; var a1:TElso; a2:TMasodik; a3:THarmadik; begin a1:=TElso.Create; a1.A; {X=1} a2:=TMasodik.Create; a2.A; {X=1} a3:=THarmadik.Create; a3.A; {X=1} end.
68
**} **} **} **} **}