17. fejezet: Objektumok

Ebben a fejezetben megismerkedünk az objektumok és osztályok fogalmával. Ugyan írhatsz programot úgy, hogy csak a Lazarus beépített objektumait használod, a továbblépéshez mindenképpen szükséges saját objektumok létrehozása.

Kitérő: TYPE és CONST

Általunk létrehozott típusokat a deklarációs részben elláthatunk névvel. Így

VAR a:array[1..100] of integer;
    b:array [1..100] of integer;


helyett

TYPE nagytomb=array[1..100] of integer;
VAR a,b:nagytomb;


is használható. Ha a fontosabb típusoknak nevet adunk, a programkódunk olvashatóbb lesz, összetettebb típusok (osztályok) használatakor pedig a type nélkülözhetetlen.

A konstansokat olyan változókként használhatjuk, melyeknek értékét nem lehet módosítani, és már fordítási időben értéket kapnak. Gondoljunk egy programra, amely szabadesést számol: a programban sok helyen felbukkan a 9.81 érték. Ha a deklarációs részbe beírjuk:

CONST g=9.81;
...
x:=g/2*sqr(t);

a programban mindenhol g szerepelhet a 9.81 helyett. Így a kód érthetőbb lesz, mert nem puszta számokat látunk, hanem neveket, amelyek a szám funkcióját is mutatják. Könnyebb dolgunk lesz akkor is, ha a programot a Föld helyett a Holdra kell alkalmazni: egyetlen helyen, a konstansdeklarációnál kell átírni. A fordító egyébként a kód elkészítésekor g-t mindenhol 9.81-gyel fogja helyettesíteni.

Az előző példában a tömb mérete 100, de mi van, ha ez változik? Át kell írnunk a 100-akat, nem csak a deklarációban, hanem a program tömbön végigmenő ciklusainál. Ráadásul nem használhatjuk a szerkesztő Csere funkcióját, mert máshol is szerepelhet 100 a programban, aminek a tömbmérethez nincs köze. Így viszont:

CONST meret=100;
VAR t:array[1..meret] of integer;
...
for i:=1 to meret do writeln(t[i]);

elég csak egy helyen módosítani.

A Pascal unitjai nem csak eljárásokat, hanem típusok és konstansdeklarációkat is tartalmaznak. Ilyen  pl. a színbeállításoknál használt clRed konstans.

Az eredeti objektumtípus, amit már ritkán használunk

Bár az objektumok központi szerepet játszanak napjaink programozástechnikájában, eredeti formájukban ritkán használják őket. A következő példaprogram konzolalkalmazás!
Létrehozunk egy szakasz objektumtípust, és ennek alapján több szakasz típusú objektumot. Egy szakasznak van kezdőpontja (x1,y1) koordinátákkal, és végpontja (x2,y2). Ezek az objektumban tárolt változók, más néven tulajdonságok vagy mezők. Az objektum tartalmaz továbbá egy hossz függvényt, amely kiszámítja a szakasz hosszát (Pitagorasz tétele alapján), és egy beallit eljárást, amely beállítja a koordinátákat. Az objektum függvényeinek és eljárásainak neve metódus.
TYPE Szakasz=object
  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
End;

procedure Szakasz.beallit(p,q,r,s:real);
begin
  x1:=p; y1:=q;
  x2:=r; y2:=s;
end;

function Szakasz.hossz:real;
begin
  hossz:=sqrt(sqr(x1-x2)+sqr(y1-y2));
end;

VAR a,b:Szakasz;

BEGIN
  a.beallit(10,10,20,30);
  b:=a;
  b.x2:=50;
  writeln(a.hossz);
  writeln(b.hossz);
  readln;
END. 

Először létrehoztuk az objektum típusát (Szakasz), melynek segítségével több Szakasz típusú objektumot deklarálhatunk (a és b). Az objektumok is változók. A típusdeklarációban megadtuk a tulajdonságok nevét és típusát, valamint a metódusok fejlécét. A metódusokat a típusdeklaráció után fejtettük ki.
A beallit metódus nélkül is tudnánk értéket adni a koordinátáknak (ahogy a b objektumnál történik), de így kényelmesebb.
A mezők a és b objektumban is létrejönnek, a metódusokból azonban csak egy példány létezik, és mindkét szakasz azokat használja - ilyen értelemben az objektum csak a mezőit tartalmazza, a metódosok a típushoz tartoznak.

Leszármazás és öröklődés

Hozzuk létre a teglalap objektumtípust is! Ha az oldalak párhuzamosak a koordinátatengelyekkel, a téglalap ugyanazokkal a koordinátákkal adható meg, mint a szakasz (szemközti csúcsok). Ezért a meglévő kódból felhasználjuk, amit lehet.
A teglalap típus a szakasz típusból származik, ugyanakkor ki is bővül egy új függvénnyel (terulet).
TYPE

Szakasz=object

  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
End;

Teglalap=object(szakasz)
  function terulet:real;
End;

A téglalap örökli a szülő objektum minden tulajdonságát és metódusát, ugyanakkor újakkal bővíthető. (Sőt, az örökölt tulajdonságok és metódusok módosíthatóak a gyerek objektumban. Ehhez újra meg kell őket adni.) Nézzük az új metódust:
function Teglalap.terulet:real;
begin
  terulet:=abs(x1-x2)*abs(y1-y2);
end;

Osztályok és mutatók

Változókat (így objektumokat is) eddig statikusan hoztunk létre, ami azt jelenti, hogy ezek a változók már fordítási időben előkerülnek, és a program indulásakor fix memóriaterület lesz lefoglalva számukra. Ez nem praktikus akkor, ha előre nem látható mennyiségű adattal lesz dolga a programnak (ilyen esetben eddig a lehető legnagyobb méretű tömböt kellett deklarálni). Lehetőség van változók dinamikus létrehozására futási időben, az ezek által használt memóriaterület mérete a program futása közben rugalmasan változik.
A dinamikus memóriakezelés kulcsa a pointer, azaz mutató, amely egy memóriacímet tartalmazó változó. Az általa mutatott memóriaterületen tetszőleges típusú adat elhelyezkedhet. A pointert deklaráljuk, de az általa mutatott adatnak már futási időben foglalunk helyet.
Az osztály (class) objektumra mutató pointer típus. Nézzük az első programot objektum helyett osztállyal:
TYPE Szakasz=class 
  x1,y1,x2,y2:real;
  procedure beallit(p,q,r,s:real);
  function hossz:real;
END;

procedure Szakasz.beallit(p,q,r,s:real);
begin
  x1:=p; y1:=q;
  x2:=r; y2:=s;
end;

function Szakasz.hossz:real;
begin
  hossz:=sqrt(sqr(x1-x2)+sqr(y1-y2));
end;

VAR a,b:Szakasz;

BEGIN
  a:=Szakasz.Create;
  a.beallit(10,10,20,30);
  b:=a;
  b.x2:=50;
  writeln(a.hossz);
  writeln(b.hossz);
  readln;
END.

Mi a különbség? Először is, a Szakasz most nem objektumtípus, hanem objektumra mutató pointer típus, vagyis osztály (object helyett class).  Ezért a és b sem objektum, hanem szakasz objektumra mutató pointer. Ebből következik, hogy a program indulásakor nem léteznek objektumok, csak két pointerünk van.
A szakasz objektumunkat létre kell hozni, a memóriában le kell foglalni helyet a koordináták tárolására. Az objektumot a Szakasz osztály konstruktora hozza létre (minden osztálynak alapból van konstruktora), ennek neve Create. Figyeld meg, hogy a Create nem objektum metódusaként kerül meghívásra (akkor a.Create lenne), hanem az osztályhoz tartozik. Ez így logikus, hiszen a konstruktor meghívásakor maga az objektum még nem létezik.
A Create egyrészt létrehozza az objektum egy példányát (instance), másrészt, mint függvény, eredményként visszaadja az objektum memóriacímét. Ezt a címet tároljuk el a-ban. (Ha nem tárolnánk el, nem tudnánk hivatkozni az objektumra.)
A továbbiakban a fordító gondoskodik arról, hogy ha class típusú változóval találkozik, akkor a műveleteket a megfelelő objektummal (az osztály egy példányával) végezze el. A b.x2:=50 utasítás ugyanazt csinálja, akár objektum, akár osztály a b. Ugyanakkor nem szabad elfelejtenünk, hogy míg a b.x2 egy objektum adott tulajdonságára vonatkozik (ebben nincs különbség az objektumos és osztályos programváltozat között), az osztály típusú változók mégis csak mutatók!
A második példaprogramban egyetlen objektum jön létre, mert egyszer hívtuk meg a konstruktort. A b:=a értékadás a b mutatónak értékül adja az a-ban tárolt memóriacímet. Így két mutatónk van, és mind a kettő ugyanarra az objektumpéldányra mutat. Ezért a.hossz és b.hossz ugyanazt az eredményt adja.
Az objektumnak destruktora is van, mely felszabadítja a lefoglalt memóriaterületet. Ennek neve Destroy. Pl. a.Destroy. Ezt ritkábban használjuk a konstruktornál, mert egy objektumra gyakran hivatkoznak más objektumok, pl. egy form törlése előtt a benne lévő gomb objektumokat is törölni kell. Jó gyakorlat a Destroy helyett a FreeAndNil metódus, amely egyúttal a mutató értkét nil-re állítja (ez egy speciális mutató-érték, amely jelzi, hogy a mutatónk nem mutat érvényes objektumra).

Hol szerepel mindez a programban?

A fentiek fényében nézzünk át ismét egy grafikus alkalmazást. Legyen csak egy formunk, amelyen egy gomb van, és beállítottuk a gomb OnClick eseménykezelőjét.

TYPE

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

Ezt a kódot a Lazarus állította elő. Figyeljük meg, hogy minden osztály neve T-vel kezdődik, az azonosítóknak hosszú nevük van, és felváltva tartalmaznak kis- és nagybetűket (camel case). Ezek a szabályok a programkód olvashatóságát és érthetőségét javítják.
A Lazarus minden osztálya a TObject osztály leszármazottja. Itt létrejön egy új TForm1 osztály, mely kibővíti a szülő TForm osztályt egy mezővel (Button1) és egy metódussal (Button1Click). A Button1Click-nek van egy paramétere: a Sender paraméter az eseményt kiváltó objektum - mivel ez most csak a Button1 lehet, ebben az esetben nincs jelentősége. Fontos lesz majd, ha ugyanazt az eseménykezelőt több objektum is meghívhatja. Figyeljük meg, hogy a Sender típusa nem TButton, hanem általános TObject, ez később fontos lesz.
Az eseménykezelők mind a TForm1 metódusai.

VAR
  Form1: TForm1;

Itt létrejön a Form1 class típusú változó, a TForm1 típus alapján, amely tehát az objektumra mutató pointer, mellyel majd az objektumunkra (a program főablakára) hivatkozunk. Azonban joggal hiányolhatjuk a Form1:=TForm1.Create utasítást, az objektumot létrehozó konstruktort, amely sehol nem szerepel a kódban. Akkor hogyan keletkezik a form? A megoldás a .lpr fájlban szerepel, az Application.CreateForm(TForm1,Form1) utasítás hívja meg a konstruktort, de még sok mást is csinál, létrehozza a formon lévő többi objektumot is (hiszen a Button1 konstruktorát is meg kell hívni).

A következő fejezetben mindezeket egy konkrét példán mutatom be.



Comments