Objekty jsou rozšířením klasických proměnných. Stojí na nich celé moderní programování (ale i tak musíte vědět, co to jsou obyčejné proměnné, cykly, řízení programu, podmíněný překlad, debugging,...)
V dnešní lekci je pár doporučení, co se vyplatí dělat. Ne všechna jsou hned vysvětlena, ale i tak se jich držte, časem poznáte, proč jsou důležitá.
Správný objekt má několik velice důležitých vlastností:
My se na tyto vlastnosti prozatím vykašleme.
Definice objektu je stejná jako u záznamu, ale slovo record nahradíme slovem object.
Zavodnik = object Jmeno:string; StartCislo:Integer; end; var Vitez : Zavodnik;
Nyní trocha terminologického guláše. Pokud definujeme objekt pomocí type, říká se danému typu třída. Samotná proměnná dané třídy (=daného typu) se pak nazývá instance dané třídy. (Instancí dané třídy tedy může být mnoho). Aby toho nebylo málo, tak instance třídy se nazývá objekt. (Je nelogické, že se třída definuje pomocí slova object. Nikoho ani nenapadne psát object do sekce var!)
Nyní můžeme s objektem pracovat stejně tak, jako se záznamem (Pascal nám
to z neznámých důvodů umožňuje).
Zavodnik.Jmeno:='Martin';
Objekty toho umí mnohem víc než obyčejný záznam. Mohou obsahovat
vlastní procedury a funkce (tzv. metody).
Metod se nejčastěji používá k manipulaci s objektem a jeho proměnnými.
Metoda objektu může zacházet se všemi jeho proměnnými.
(Děsivě to jenom zní, podívejte se na příklad.)
U metod se uvádí pouze hlavička. Pak je musíme dodefinovat. (Při dodefinování nemusíte vypisovat celou hlavičku, ale je lepší ji uvést znovu.)
program Zaklad; type TPotravina = object Nazev : string; Kalorie: Integer; procedure Nastav(Co:string,Kolik:Integer); procedure Secti(A:TPotravina); {Přičte k instancím našeho objektu instance objektu A} procedure Vypis; end; procedure TPotravina.Nastav(Co:string,Kolik:Integer); begin Nazev:=Co; Kalorie:=Kolik; end; procedure TPotravina.Secti; begin Nazev:=Nazev+' a k tomu ' + A.Nazev; Kalorie:=Kalorie+A.Kalorie; end; procedure TPotravina.Vypis; begin Writeln('Dal jste si : ', Nazev); Writeln('Celkova kalorická hodnota vašeho pokrmu činí : ',Kalorie); end; var Vecere,Priloha:TPotravina; begin Vecere.Nastav('Zeli',100); Priloha.Nastav('Dezert',5000); Vecere.Secti(Priloha); Vecere.Vypis; Readln; end.
Snad jste to pochopili.
V objektovém programování bychom vždy měli dodržovat zásadu, že proměnné objektu lze měnit jenom pomocí metod daného objektu. Vyplatí se dokonce psát metody pro získání obsahu proměnných objektu. (Tedy nepsat BlaBla.Jmeno - a raději napsat funkci (metodu) BlaBla.ZjistiJmeno : string;)
Doufám, že zatím je vám vše jasné.
Objekt má jednu nevýhodu - nemůže být výsledným typem funkce (to ostatně nemůže být žádný uživatelem definovaný typ)...
Nadefinujeme-li si objekt, je zpočátku zcela prázdný. K jeho inicializaci se
používá tzv. konstruktor. Je to metoda, která provede inicializaci naší
proměnné. (Místo slova procedure napíšeme constructor a chováme se
k ní jako by se jednalo o proceduru)
Konstruktor se obvykle pojmenovává Init. V našem případě by bylo lepší
proceduru Nastav nadefinovat jako constructor.
constructor Init(Co:string, Kolik:Integer); a poté dále constructor
Init(...);
Přestaneme-li s objektem pracovat, měli bychom ho odstranit z paměti. K
tomu slouží destructor. (Má-li daný objekt otevřeno patnáct souborů
a zabírá-li půlku paměti, je lepší ten svinčík prostě uklidit). Pro destruktor
se nejčastěji používá název Done.
destructor Done;
My budeme dodržovat zásadu, že každý objekt má konstruktor i destruktor.
Všechny objekty v programu by měly být dynamické proměnné. (A v normálních programovacích jazycích to ani jinak nejde). Přistupovat k nim tedy budeme vždy pouze přes ukazatele. Máme-li tedy třídu TNejakyObjekt, současně s ní definujeme i typ PTenSamyObjekt = ^TNejakyObjekt. Po vytvoření instance dané třídy (pomocí New()) musíme zavolat konstruktor. Před Dispose voláme destruktor. (Všechny dynamické proměnné rušíme hned, jakmile nejsou zapotřebí, nejpozději před koncem programu).
type PObjekt = ^TObjekt; {Je lepsi definovat ukazatel driv, trida ho pak muze pouzivat, coz se obcas hodi.} TObjekt = object constructor Init; destructor Done; end; constructor TObjekt.Init;{I když konstruktor zdánlivě nic nedělá, vyplatí se ho psát} begin end; destructor TObjekt.Done; begin end; var A:PObjekt; begin New(A); A^.Init; {Tu se neco deje} A^.Done; Dispose(A); end.
Pascal má i zkrácený zápis:
New(A,Init); {Tu se neco deje} Dispose(A,Done);
Budeme se držet zásady, že New a Dispose pro objekty voláme vždy s konstruktorem a destruktorem. (Je lépe, když má objekt všechny proměnné (a ukazatele obzvlášť) inicializované, jinak jsou v nich opravdu zcela náhodné hodnoty. A pokud pouze zrušíme ukazatel na objekt, který si v paměti pomocí New zabral několik gigabytů, ... tak zůstanou zabrány až do konce programu a my s tím nebudeme moc nic udělat.)
Ještě se zmíním o polymorfismu. Máme-li definován nějakou třídu, můžeme definovat další třídu, která zdědí všechny vlastnosti původní třídy (rodiče) a přidá některé další (jeden rodič může mít i více potomků).
type Syn = object(Otec) Hracky : THracky; constructor Narozeni; destructor Smrt; end;
Stačí uvést jméno rodiče v závorce a třída zdědí veškeré vlastnosti rodiče (všechny jeho proměnné a metody).
Platí, že libovolnou proměnnou potomka můžeme vždy přiřadit do proměnné typu rodič (a stejně tak je
tomu s ukazateli).
Podívejme se na krátkou ukázku:
{----------------------------------TZvire----------------------------------------} type PZvire=^TZvire; {Tady s vyhodou vyuzivame toho, ze ukazatel muzeme definovat pred samotnou definici typu, na ktery ukazuje. Casto se totiz stane, ze trida vyuziva ukazatel sama na sebe.} TZvire=object jmeno:string; procedure VydejZvuk; constructor Init; {Kazda trida musi mit konstruktor.} destructor Done; {Kazda trida musi mit destruktor.} end; {Nyni musime dodefinovat jednotlive metody tridy} constructor TZvire.Init; {I kdyz konstruktor zdanlive nic nedela, pri jeho volani se objekt inicializuje, takze je dobre ho vzdy napsat.} begin end; procedure TZvire.VydejZvuk; begin writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.'); end; destructor TZvire.Done; {V tomto pripade destruktor skutecne nic nedela, ale casto je nezbytny. Proto ho piseme u kazde tridy.} begin end; {-------------------------------------end of TZvire---------------------------------------} {----------------------------------------TKocka-------------------------------------------} type PKocka=^TKocka; {Ten samy trik s ukazatelem} TKocka=object(TZvire) ChycenoMysi:integer; procedure ChytMys; {Kocka zdedila vsechny vlastnosti od TZvirete, pridala vsak novou metodu ChytMys; A udaj, kolik mysi jiz chytla.} end; procedure TKocka.ChytMys; begin ChycenoMysi:=ChycenoMysi+1; end; {-------------------------------------end of TKocka---------------------------------------} var Zvire:PZvire; Kocka:PKocka; begin Kocka:=New(PKocka,Init); {Vsechny objekty by mely byt dynamicke a volany s konstruktorem} Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne} Kocka^.VydejZvuk; {Tady vidime, ze i trida TKocka ma metodu VydejZvuk, kterou zdedila od sveho TZvirete.} Zvire^.VydejZvuk; Dispose(Kocka,Done); {Dynamicky alokovane promenne nikdy nezapomeneme odstranit, u objektu musime navic zavolat destruktor.} readln; end.
Zkuste si uvedený program spustit.
Dědičnost toho ale umožňuje mnohem víc. Jednou z jejích výhod je, že
potomek může předefinovat metody rodiče (ale nemůže je zrušit).
Zkusme nyní upravit náš program tak, aby měl větší smysl. Bylo by dobré, aby naše kočka mňoukala.
Navíc doděláme pár dalších legrácek.
Výsledek by mohl vypadat třeba nějak takto:
{----------------------------------TZvire----------------------------------------} type PZvire=^TZvire; TZvire=object jmeno:string; procedure VydejZvuk; constructor Init(truename:string); {Konstruktor priradi zvireti jmeno} destructor Done; function getname:string; end; {Nyni musime dodefinovat jednotlive metody tridy} constructor TZvire.Init(truename:string); begin jmeno:=truename; end; procedure TZvire.VydejZvuk; begin writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.'); end; function TZvire.getname:string; begin getname:=jmeno; end; destructor TZvire.Done; begin end; {-------------------------------------end of TZvire---------------------------------------} {----------------------------------------TKocka-------------------------------------------} type PKocka=^TKocka; TKocka=object(TZvire) ChycenoMysi:integer; procedure VydejZvuk;{Chceme-li proceduru predefinovat, musime ji znovu uvest v seznamu metod.} constructor Init(truename:string;Chyceno:integer); {I konstruktor lze predefinovat Kocka potrebuje navic nastavit pocet chycenych mysi.} procedure ChytMys; end; procedure TKocka.VydejZvuk; begin writeln('Mnau'); end; constructor TKocka.Init(truename:string;Chyceno:integer); begin jmeno:=truename; ChycenoMysi:=Chyceno; end; procedure TKocka.ChytMys; begin ChycenoMysi:=ChycenoMysi+1; end; {-------------------------------------end of TKocka---------------------------------------} var Zvire:PZvire; Kocka:PKocka; begin Kocka:=New(PKocka,Init('Micka',12)); Kocka^.VydejZvuk; {Tady pouzivame predefinovanou metodu.} Zvire:=Kocka; Zvire^.VydejZvuk; Dispose(Kocka,Done); readln; end.
Zkuste si uvedený program spustit.
Předefinovaná metoda může využít původní metody rodiče.
Toho se často využívá například u konstruktorů.
Chceme-li využít původní metodu rodiče (kterou náš objekt předefinoval), napíšeme před volání funkce klíčové slovo
inherited.
Raději si vše ukažme na našem příkladu (konstruktor TKocka.Init využívá dvě původní metody třídy TZvire) - raději zkopíruji celý text, snadněji si ho zkopírujete do Pascalu:
{----------------------------------TZvire----------------------------------------} type PZvire=^TZvire; TZvire=object jmeno:string; procedure VydejZvuk; constructor Init(truename:string); destructor Done; function getname:string; end; constructor TZvire.Init(truename:string); begin jmeno:=truename; end; procedure TZvire.VydejZvuk; begin writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.'); end; function TZvire.getname:string; begin getname:=jmeno; end; destructor TZvire.Done; begin end; {-------------------------------------end of TZvire---------------------------------------} {----------------------------------------TKocka-------------------------------------------} type PKocka=^TKocka; {Ten samy trik s ukazatelem} TKocka=object(TZvire) ChycenoMysi:integer; procedure VydejZvuk; constructor Init(truename:string;Chyceno:integer); procedure ChytMys; end; procedure TKocka.VydejZvuk; begin writeln('Mnau'); end; constructor TKocka.Init(truename:string;Chyceno:integer); begin inherited Init(truename); {Volame puvodni konstruktor predka} inherited VydejZvuk; {Volame puvodni metodu predka} ChycenoMysi:=Chyceno; writeln('A ted se ze mne stala kocka a budu mnoukat!'); end; procedure TKocka.ChytMys; begin ChycenoMysi:=ChycenoMysi+1; end; {-------------------------------------end of TKocka---------------------------------------} var Zvire:PZvire; Kocka:PKocka; begin Kocka:=New(PKocka,Init('Micka',12)); writeln('Konec konstruktoru'); writeln; Kocka^.VydejZvuk; Zvire:=Kocka; Zvire^.VydejZvuk; Dispose(Kocka,Done); readln; end.
Zkuste se zamyslet nad tím, co přesně náš program teď bude dělat a pak ho spusťte. Trefili jste se?
Další zázračná věc, kterou objekty umožňují, je dynamická vazba. Té docílíme tak, že za deklaraci procedury ve třídě napíšeme klíčové slovo virtual. A kdykoli voláme nějakou virtuální funkci, podívá se překladač, s jakým konstruktorem byl daný objekt vytvořen, a podle toho vybere příslušnou funkci. Definujeme-li metodu jako virtuální, musí taková být ve všech potomcích. Také není možné aby metoda u rodiče virtuální byla a u potomka ne.
Raději si to osvětlíme na našem příkladu. Předěláme ho nyní tak, aby kočka Micka mňoukala pořád.
Je to jednoduché, stačí připsat dvakrát slovo virtual:
{----------------------------------TZvire----------------------------------------} type PZvire=^TZvire; TZvire=object jmeno:string; procedure VydejZvuk;virtual; {Klicova zmena, nyni se bude metoda vybirat podle toho, jaky objekt je tam ve skutecnosti.} constructor Init(truename:string); destructor Done; function getname:string; end; {Nyni musime dodefinovat jednotlive metody tridy} constructor TZvire.Init(truename:string); begin jmeno:=truename; end; procedure TZvire.VydejZvuk; begin writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.'); end; function TZvire.getname:string; begin getname:=jmeno; end; destructor TZvire.Done; begin end; {-------------------------------------end of TZvire---------------------------------------} {----------------------------------------TKocka-------------------------------------------} type PKocka=^TKocka; {Ten samy trik s ukazatelem} TKocka=object(TZvire) ChycenoMysi:integer; procedure VydejZvuk;virtual; {I zde musi zustat metoda virtualni} constructor Init(truename:string;Chyceno:integer); procedure ChytMys; end; procedure TKocka.VydejZvuk; begin writeln('Mnau'); end; constructor TKocka.Init(truename:string;Chyceno:integer); begin inherited Init(truename); ChycenoMysi:=Chyceno; end; procedure TKocka.ChytMys; begin ChycenoMysi:=ChycenoMysi+1; end; {-------------------------------------end of TKocka---------------------------------------} var Zvire:PZvire; Kocka:PKocka; begin Kocka:=New(PKocka,Init('Micka',12)); writeln('Konec konstruktoru'); writeln; Kocka^.VydejZvuk; Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne} Zvire^.VydejZvuk; Dispose(Kocka,Done); readln; end.
Spusťte si nyní daný program a uvidíte ten rozdíl!
Jelikož virtuální metody jdou volat až po zavolání konstruktoru, nemůže být konstruktor nikdy virtuální. Navíc je z toho vidět, proč je důležité, aby každá třída měla konstruktor. Schválně si zkuste v uvedeném programu napsat pouhé Kocka:=New(PKocka); místo Kocka:=New(PKocka,Init(...)). Virtuální metody nebudou fungovat, v horším případě vám spadne počítač, v lepším se nestane vůbec nic, jako byste žádnou metodu nevolali. Pascal to takhle umožňuje, ale my se neinicializovaným objektům (tj. objektům, na které nebyl použit konstruktor) budeme vyhýbat.
Představme si následující situaci. Máme dva objekty Rodiče(TZvire) a Potomka(TKocka) a rodič destruktor nemá (nepotřebuje ho), ale potomek volání destruktoru vyžaduje. Přiřadíme-li nyní potomka do proměnné typu rodič a odstraníme tuhle dynamickou proměnnou pomocí Dispose, žádný destruktor se nezavolá. To může mít katastrofální následky. Proto každý objekt musí mít destruktor a tento destruktor musí být virtuální. Usnadníme si tak práci, pokud někdy v budoucnu budeme potřebovat dodefinovat potomka, který destruktor vyžaduje. Nebudeme tak muset v našem programu hledat všechny výskyty Dispose a kontrolovat, jestli náhodou nerušíme proměnnou tohoto nového typu... (Což je náročný oříšek, jelikož potomci jdou přiřazovat do proměnných typu rodič a rušíme-li proměnnou typu rodič, odkud máme jistotu, že v ní není onen destrukci vyžadující potomek?) Napíšeme nyní náš příklad v duchu všech uvedených zásad:
{----------------------------------TZvire----------------------------------------} type PZvire=^TZvire; TZvire=object jmeno:string; procedure VydejZvuk;virtual; {Klicova zmena, nyni se bude metoda vybirat podle toho, jaky objekt je tam ve skutecnosti.} constructor Init(truename:string); destructor Done;virtual; function getname:string; end; {Nyni musime dodefinovat jednotlive metody tridy} constructor TZvire.Init(truename:string); begin jmeno:=truename; end; procedure TZvire.VydejZvuk; begin writeln('Ja jsem anonymni zvire, nevim, jaky vydavam zvuk.'); end; function TZvire.getname:string; begin getname:=jmeno; end; destructor TZvire.Done; begin writeln('Zviratko ', jmeno, 'umrelo.'); end; {-------------------------------------end of TZvire---------------------------------------} {----------------------------------------TKocka-------------------------------------------} type PKocka=^TKocka; {Ten samy trik s ukazatelem} TKocka=object(TZvire) ChycenoMysi:integer; procedure VydejZvuk;virtual; {I zde musi zustat metoda virtualni} constructor Init(truename:string;Chyceno:integer); procedure ChytMys; destructor Done;virtual; end; procedure TKocka.VydejZvuk; begin writeln('Mnau'); end; constructor TKocka.Init(truename:string;Chyceno:integer); begin inherited Init(truename); ChycenoMysi:=Chyceno; end; procedure TKocka.ChytMys; begin ChycenoMysi:=ChycenoMysi+1; end; destructor TKocka.Done; begin writeln('Kocka ',jmeno, ' snedla za svuj zivot ', ChycenoMysi, ' mysi. Budiz ji zeme lehka.') end; {-------------------------------------end of TKocka---------------------------------------} var Zvire:PZvire; Kocka:PKocka; begin Kocka:=New(PKocka,Init('Micka',12)); writeln('Konec konstruktoru'); writeln; Kocka^.VydejZvuk; Zvire:=Kocka; {Jelikoz je TKocka potomkem TZvirete, je toto prirazeni spravne} Zvire^.VydejZvuk; Dispose(Zvire,Done); {Bez virtualniho destruktoru by se zavolal spatny destruktor.} readln; end.
Nejčastěji se pak využívají seznamy objektů (že nechcete vědět, co to je). Nebojte se, je to úplně stejné, jako u obyčejných seznamů. U seznamů hrají konstruktory a destruktory výraznou úlohu.
Hlavní využití tříd spočívá v tom, že spoustu tříd již někdo definoval. Potřebujeme-li tedy něco naprogramovat, podíváme se, která třída náš problém řeší. Pokud žádnou takovou nenajdeme, podíváme se, jestli není snadnější napsat řešení jako potomka nějaké již existující třídy. Pokud se to povede, ušetříme si spoustu práce.
Objekty se také používají, pokud vyvíjíme rozsáhlejší program. Představme si, že na vývoji programu pracuje několik programátorů a každý vyvíjí jinou část programu. Velice snadno se pak může stát, že dva z nich pojmenují nějakou funkci či proměnnou stejně. Říká se tomu kolize jmen. Pokud ovšem každý z nich vyvíjí jinou třídu, kolize jmen nehrozí (TMojeTrida.Vypis je jiný název než TTvojeTrida.Vypis). Programátoři tak nemusí vymýšlet šílené názvy svých funkcí a mohou je pojmenovávat zcela přirozeně. To vede k lepší čitelnosti programu (a tím i k rychlejšímu dokončení vývoje).
V sekci s příklady bude ukázka, která bude demonstrovat další výhody objektů. Bude se jedna o takovou demoverzi skutečného programování (občas bude zapotřebí něco přidat, občas něco upravit, finální verze se bude lišit od původní...)
To by byl pouze takový nezbytný základ objektově orientovaného programování. Dnes nedostanete žádný úkol, neboť je nezbytné, abyste si sami prošli a pochopili všechny příklady. Objekty se musíte prokousat každý po svém. Zkoušejte, pište vlastní programy, opravujte chyby v mojí lekci, ale hlavně - využívejte co možná nejvíce objektů (klidně i nesmysluplně, hlavně, ať zjistíte, jak fungují). Snažte se vymyslet, kdy a jak využít jejich vlastností.