Pořád cítím, že bych měl něco napsat i pro začátečníky, kde bych osvětlil některé věci ohledně Object Pascalu, když už se na jejich znalost spoléhám.
A tak jsem se podíval do svého archívu a spojil několik útržků textů, které jsem kdysi napsal z jiných důvodů a výsledek trošku upravil. Nejedná se o učebnici, ani o kompletní přehled, ale spíše takové základy a postřehy. Pro pochopení předpokládám aspoň základní znalosti Pascalu.
V jedné knížce o Delphi jsem četl, že když se kdysi u Borlandů přemýšlelo o první verzi Delphi, tak někdo při brainstormingu napsal na tabuli
Button.Caption:='text'
a tím byl základ Delphi položen.
Když totiž například napíšeme uvedený kód, tak to není jen prosté uložení řetězce, ale zároveň to způsobí překreslení popisky tlačítka. Je to umožněno tím, že pro zápis je definována metoda (viz dále, ale pro neznalé procedura třídy), která toto chování implementuje. Kompilátor při kompilaci místo prostého přiřazení provede volání zápisové metody.
Ale při čtení titulku (Caption) už nic kreslit nemusíme, takže může probíhat např. přímo z interní proměnné třídy. Při čtení tedy není žádná časová ztráta. Kompilátor tedy provede např. jen přiřazení. Caption je property objektu Button (tlačítko).
Property tedy umožňuje provádět různé akce při čtení a zápisu dat, sice se k property ještě dostaneme, ale prozatím: Property je jedním z klíčových prvků při programování v Object Pascalu, i když si to třeba neuvědomujeme.
Programátoři často chybují v pojmech třída a objekt. Oblíbeným rčením je, že objekt je instance třídy.
Nejjednodušší je uvést analogii mezi typem proměnné a proměnnou. Třída je jakoby typ a objekt je jakoby proměnná. Ve své podstatě typ a třída existuje jen jako pomůcka pro programátora a kompilátor.
Velikost vlastního objektu je v 32bit kompilátoru 32bitů, tj. 4 byte a ve skutečnosti se jedná o ukazatel (pointer) ukazující na data objektu v paměti.
A zde máme druhý důsledek tehdejšího brainstormingu: jestliže je objekt vlastně pointer, tak by se logicky mělo psát:
Button^.Caption:='text'
Kompilátor ale při zjištění, že se odkazujeme na objekt dereferenci (^) odpustí.
Pokud tedy napíšeme
var objekt:TMujObject
, tak je sice v paměti vymezen prostor pro objekt (= pointer = u 32 bitového překladače 4 bajty), ale objekt ještě neexistuje. Teprve voláním konstruktoru (dle konvence Create, ale není to předpis, navíc může být konstruktorů více) je v paměti na haldě alokováno místo o potřebné velikosti.
Destruktor (dle konvence Destroy) objekt z paměti uvolní a paměť je vrácena. Pozn.: Místo Destroy volejte metodu Free, která interně destruktor zavolá. Objekt je sice uvolněn, ale proměnná pořád obsahuje odkaz na (nyní již naplatnou) paměť. Lepší je proto volat proceduru FreeAndNil, která má parametr typu TObject a po zavolání Free nastaví předaný parametr na nil, což značí prázdný objekt.
FreeAndNil(objekt);
FreeAndNil testuje zda není objekt nil, není třeba testovat předem manuálně.
Základní třídou je TObject. Třída kromě jiného zavádí constructor Create a destructor Destroy a několik dalších metod. Mezi ně patří i již uvedená metoda Free, která interně volá destruktor a uvolňuje vytvořený objekt. Konstruktor vytváří instanci třídy, tj. vytvoří objekt. Destruktor objekt uvolní.
Deklarace třídy může vypadat takto
interface
type
TMyClass=Class (TObject) // když nic, tak to přesto značí TObject
protected
fidata: Integer;
procedure mSetData(iValue:Integer);
public
constructor Create; // náš konstruktor
destructor Destroy; override; // náš destruktor
property piData:integer read fidata write mSetData;
end;
implementation
constructor TMyClass.Create;
begin
// zde vytvoříme, vše co potřebujeme
end;
procedure TMyClass.mSetData(iValue:Integer);
begin
fidata: iValue;
end;
destructor TMyClass.Destroy;
begin
// provedeme potřebné akce (uvolnění zdrojů, které jsme vytvořili)
inherited //zavoláme předchůdce (pokud je to třeba)
end;
Zde bych se rád trošku pozastavil u správy paměti Delphi. Jelikož jsou často alokovány malé bloky paměti, bylo by neefektivní, aby tím byl neustále obtěžován správce paměti v systému. Místo toho si Delphi alokuje větší bloky paměti, a z něho vlastním správcem paměti přiděluje malé kousky. Až paměť dojde je vyžádán další kus. Na konci běhu jsou všechny bloky paměti vráceny zpět systému. Při použití FastMM (nebo Delphi 2005+) je algoritmus přidělování komplikovanější a efektivnější a už jsem o něm zde psal.
Pokud zapomeneme zavolat konstruktor, tak objekt není vytvořen. Jelikož si ale kompilátor nemůže být jist, že to tak nechceme je zahlášen pouze warning. Případné volání nějaké metody skončí s chybou přístupu k paměti a je vyvolána výjimka EAccessViolation. Tato, stejně jako ostatní výjimky je následníkem třídy Exception. Pokud není výjimka odchycena programátorem (viz. dále) je odchycena globální obsluhou výjímek a je zobrazeno varování, ale program není ukončen. Často je tento globální ovladač nahrazen lepším (třeba z JCL), který zobrazuje třeba výpis zásobníku při chybě atd.
Od počátku programování je zjevná snaha o modularitu a případně skrytí implementačních detailů a zpřístupnění pouze rozhraní. Z hlediska historie pascalu byla jedním z prvním vývojových stupňů jednotka (unit) s dvěma stupni viditelnosti (interface x implementation). V Object Pascalu máme pro každou třídu možnost definovat několik stupňů viditelnosti.
TMyClass=Class(TObject)
private
protected
public
published
strict private // delphi 2006+
strict protected // delphi 2006+
end;
V každé z těchto sekcí může být uvedena deklarace metody, proměnné (někdo tomu říká i jinak) nebo property (má hlavně význam u public a published).
Sekce private (strict private) je nejpřísnější. Co je zde uvedeno, tak není vidět nikde kromě jednotky, kde je třída umístěna. Píši schválně jednotky a ne třídy, jelikož dvě třídy v jedné jednotce si mohou navzájem přistupovat k polím v části private (neplatí pro strict private – tam se opravdu jedná jen o třídu).
Sekce protected (strict protected) viditelnost lehce uvolňuje, jelikož pole zde definovaná lze vidět i v následnících mateřské třídy, popř. ve stejné jednotce. V ostatních případech pole vidět nejsou. Analogicky k strict private je u strict protected omezení na pouze následníci.
Pole v sekci public jsou vidět všude.
Pole v sekci published mají stejnou viditelnost jako public. Rozdíl je, že za běhu se vytvářejí RTTI (Run Time Type Information). Díky tomu mohou externí aplikace (jako debugger Delphi) získat za běhu o těchto polích doplňující informace. RTTI bylo v Delphi 2010 významně rozšířeno.
Metody poskytují rozhraní pro přístup k proměnným třídy, popř. pracují nad proměnnými třídy.
Jelikož někde používám výraz procedura, někde funkce a někde metoda tak na vysvětlení: Funkce se liší od procedury tím, že vrací hodnotu (hodnota je v těle funkce reprezentována proměnnou Result stejného typu jako je návratová hodnota). Metoda je "procedura" nebo "funkce" u třídy. U nových Delphi lze návratovou hodnotu specifikovat jako parametr Exit, např. Exit 20.
Pozn: Klasickým názorem je, že OOP je nutně pomalejší než klasické programování. Když se oprostíme od již uvedených výhod OOP, tak bych rád upozornil, že většina metod nemá v podstatě parametry (jejich data jsou z převážné většiny součástí definice třídy) a tudíž volání by mělo být teoreticky (i prakticky) rychlejší. Jen je třeba myslet na to, že se předá vždy jeden parametr navíc – odkaz na objekt, tj. Self.
V Delphi jsou standardně první tři parametry předávány v registrech procesoru. Pokud specifikujeme za deklarací metody klíčové slovo inline, Delphi zváží možnost začlenění volané metody jako přímý kód, tj. bez volání.
Speciálním typem metody (resp. to není úplně metoda) je class helper, kterému byl věnován speciální článek.
Metody statické jsou všechny metody, pokud není uvedeno jinak. Pokud kompilátor narazí na kód typu Třída.StatickáMetoda, tak prostě vygeneruje kód pro volání StatickáMetoda v objektu Třída nebo pro volání této metody u předchůdce, od kterého tuto metodu podědila.
Metody virtuální jsou označovány v deklaraci třídy jako virtual.
procedure Test5; virtual;
Na rozdíl od statických metod kompilátor negeneruje kód přímo pro volání konkrétní třídy, ale používá se mechanismu pozdního svázání. Metoda, která se bude volat, je určena až za běhu.
Virtuální metoda může být v následnících předefinována (override). Při předefinování musí být deklarace přesně zachována (mění se pouze implementace).
procedure Test5; override;
Pokud tedy kompilátor narazí na konstrukci Třída.VirtuálníMetoda tak je možné, že byla metoda předefinována. Vygenerovaný kód musí za běhu nalézt v tabulce virtuálních metod správnou metodu a zavolat ji.
Metody dynamické se používají ke stejnému účelu jako virtuální. Jsou označovány v deklaraci třídy jako dynamic. V praxi se používají minimálně.
procedure Test5; dynamic;
Rozdílem je, že volání dynamické procedury jsou pomalejší, ale zabírají méně prostoru. Používají se v případě, že základní třída má mnoho metod, které se sice mohou předefinovat, ale neděje se tak příliš často a navíc má třída mnoho následníků.
Metoda je definovaná, ale nemá implementaci. Instance této třídy způsobí výjimku jen v případě, že se na ní za běhu narazí (na rozdíl od Javy a C# jde přeložit, i když s warningem). Metoda musí být v následnících předefinována, a proto musí být virtuální.
procedure Test5; virtual; abstract;
Pokud chceme definovat několik metod (nebo např. konstruktorů) se stejným názvem, musíme tuto skutečnost kompilátoru sdělit. Tímto kouzelným slůvkem je overload. Každá z metod pak musí mít svoji implementaci.
procedure Test6 (aValue:String); overload;
procedure Test6 (aValue:Integer); overload;
Nejsem si jist, zda je to korektní pojmenování, ale jednak můžeme specifikovat, že parametr se nebude v metodě měnit (a tím a) napovíme kompilátoru, b) se pojistíme proti případné chybě), druhak můžeme říct, že procedura bude mít další parametr, ale pokud nebude při volání specifikován, bude použita uvedená hodnota.
procedure Test7 (const AValue:string; const AVal2:String = “”);
Předchůdce zavoláme pomocí klíčového slova inherited.
Destructor TMyClass.Destroy;
begin
inherited;
// nebo taky inherited Destroy;
end;
Třídu můžeme přetypovat použitím klíčového slova AS. Přetypování bychom měli použít jen tehdy, když si jsme jisti, že daný objekt obsahuje instanci (nebo následníka) třídy, na kterou ho chceme přetypovat. Pokud to není pravda je vyvolána výjimka EInvalidCast.
Explicitní přetypování je taky možno způsobem Třída(Objekt). V tomto případě ale není zaručeno, že nedojde k chybě přetypováním (není kontrolován typ objektu), ale někdy je to výhodné.
Operátor IS slouží k určení, zda je typ daného objektu roven danému typu třídy nebo některému následníku. Například if Button1 IS TCheckBox then ….
Pokud definujeme, že nějaká třída je následníkem jiné třídy, tak to kromě jiného znamená, že nová třída získá všechny metody a proměnné, které byly definovány pro předchůdce (s ohledem na viditelnost).
Vlastní operace je zapsána takto:
type
TNaslednik = class (TPredchudce)
// zde přidáme nové
end;
Předchůdce zavoláme pomocí inherited. Klíčové slovo Self je referencí na sebe sama (např. self.Print) a interně je kompilátorem předáváno jako první parametr při volání.
Polymorfismus je jedna ze základních vlastností OOP. Raději rovnou uvedu klasický příklad se zvířaty:
unit uObjects;
interface
type
TZvire=class
function Zvuk:String;virtual;
class function Jmeno:string; virtual;
end;
TPes=class (TZvire)
function Zvuk:String;override;
class function Jmeno:string; overide;
end;
implementation
function TZvire.Zvuk:String;
begin
// obecný zvuk roztomilého zvířátka
Result:='grrrrrrrr';
end;
class function TZvire.Jmeno:String;
begin
Result:='zvíře';
end;
class function TPes.Jmeno:String;
begin
Result:='pes';
end;
function TPes.Zvuk:String;
begin
Result:='hafinky'; //pes je taky zvíře
end;
end.
V uvedeném unitu jsou definovány dvě třídy: TZvire a TPes. Třída TPes je následníkem TZvire a je v ní předefinována jedna metoda.
Hlavní program (uložen v TestOOP.dpr):
program TestOOP;
uses
Classes,
uObjects in 'uObjects.pas';
var
zvire:TZvire;
pes:TPes;
procedure ProjevSe(const Kdo:String; AZvire:TZvire);
begin
writeln(Kdo + ' dělá ' + AZvire.Zvuk);
end;
begin
zvire:=TZvire.Create; // instance zvířete
pes:=TPes.Create; // instance psa
try
ProjevSe(TZvire.Jmeno, zvire);
ProjevSe(TPes.Jmeno, pes);
finally
pes.Free;
zvire.Free;
end;
end.
Jádro příkladu je druhý parametr procedury (nikoliv metody - viz výše).
Všimněte si, že je typu TZvire a přesto ji v hlavním programu volám i s objektem pes. Metoda Zvuk je virtuální a proto je za běhu zavolána ta metoda, která odpovídá objektu.
Navíc je zde ukázána class function, tj. funkce, která patří k třídě a ne objektu, a dá se tedy volat i nad třídou. Podrobněji to bylo již na našem serveru probíráno, včetně dalších možností.
Výsledkem běhu programu bude:
Zvíře dělá grrrrrrrr
Pes dělá hafinky
Komponenta je objekt s určitými vlastnostmi (jedná se o následníka TComponent - viz help). Pokud tedy vytvoříme následníka této třídy, tak se s ním bude dát manipulovat na formuláři a může být přidán do palety komponent. Dále může vlastnit jiné komponenty a v neposlední řadě bude umět zapsat a načíst svá data do a ze streamu (tj. proudu dat).
Obecný formulář (TForm) je nepřímým následníkem TComponent. Pokud vytvoříme vlastní formulář – je to následník právě TForm.
Možnost čtení dat z a do streamu je velmi důležitá. Celý popis formuláře je složen právě z dat komponent a informací o tom, kdo kterou komponentu vlastní. Pokud máme např. na formuláři tlačítko, ve skutečnosti to znamená, že formulář má mezi podřízenými komponentami jedno tlačítko.
Pokud tedy například pohneme tlačítkem doleva, tak se u dané komponenty změní property Left. Při ukládání komponenty (do souboru *.dfm) je hodnota Left zapsaná (tedy pokud není aktuální hodnota hodnotou defaultní). Během fáze kompilace (resp. linkování) jsou dfm soubory vloženy do výsledné binárky (tj. do EXE).
Soubor *.dfm tedy obsahuje textový popis komponent na formuláři a je generovaný IDE. Zobrazení popisu formuláře, tj. obsahu souboru dfm, lze provést v IDE při zobrazeném formuláři např. ALT+F12 (a zase zpět).
Při kompilaci se textový popis převede na binární a přilinkuje se k aplikaci. Při vytváření formuláře za běhu, je vytvořena instance formuláře, a pak postupně všech dalších komponent, které na formulář patří. Při jejich vytváření jsou ze spustitelného souboru nataženy uložené vlastnosti (každá komponenta svoje, tedy tlačítko i to svoje Left). No a jelikož je Left property dojde (skrze zápisovou metodu) k posunutí komponenty na správné místo určené při návrhu.
Ukážeme si popis formuláře s jednou komponentou TMemo. Formulář má jméno frmMain a komponenta Editor. Formulář je vlastníkem Editoru. Popis formuláře v souboru fMain.dfm:
object frmMain: TfrmMain
Left = 200
Top = 157
Width = 783
Height = 540
ActiveControl = Editor
Caption = 'Hlavní okno'
Color = clBackground
PixelsPerInch = 75
TextHeight = 16
TextWidth = 7
object Editor: TMemo
Left = 0
Top = 0
Width = 783
Height = 540
Align = alClient
TabOrder = 0
end
end
a třída formuláře v fMain.pas:
unit fMain;
interface
type
TfrmMain = class(TForm)
Editor: TMemo;
end;
var
frmMain:TfrmMain;
implementation
{$R *.dfm} // přilinkujeme výše uvedený dfm soubor
end.
Teď se určitě ptáte, kdo vytváří formuláře. Při pohledu do souboru dpr (menu Project/View Source) uvidíme následující:
program Test1;
uses
Forms,
fMain in 'fMain.pas' {frmMain};
begin
Application.Initialize; // inicializace objektu Application
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmMain, frmMain); // vytvoření formuláře
Application.Run; // jdeme na to
end.
V případě více formulářů se řádek s CreateForm opakuje (samozřejmě s jinými parametry).
Pokud nechceme při startu aplikace vytvářet všechny formuláře (a to určitě nechceme, protože to u složitějších formulářů chvilku trvá a zbytečně to zabírá paměť), můžeme tyto řádky smazat. Druhou možností je říct IDE, že si to nepřejeme (menu Project/Options/Forms). V tom případě musíme vytvářet formuláře sami v okamžiku potřeby. První vytvořený formulář se stane hlavním formulářem aplikace. Pokud se tedy po startu zobrazuje jiné okno než je Vaše ctěná libost - zkontrolujte pořadí vytváření formulářů.
Velmi vhodné je používat Visual Form Inheritance. Pod tímto tajuplným názvem se skrývá dědění formulářů (opakuji – formulář je také objekt, takže se dá dědit).
Představme si, že máme aplikaci, kde máme deset oken, které se liší pouze v detailech. Jestliže pro všechny okna nalezneme společné prvky (a metody) je výhodné použít dědění formulářů.
Společné prvky budou tvořit předchůdce. Těchto předchůdců může být i více a mohou být od sebe poděděny. Raději proto uvedu příklad: Mějme formulář (nazvěme ho třeba TIniForm) jehož jediným úkolem bude při svém vytvoření načíst informace o své pozici a velikosti z ini souboru a při rušení je tam zase uložit. Tento formulář je následníkem standardního TForm. Nyní mějme další formulář, který nazveme třeba TBasicForm. Tento formulář bude obsahovat třeba nějaká tlačítka (OK, Cancel …) a jejich obsluhu, ale jelikož je následníkem TIniForm tak bude umět číst a zapisovat svoji konfiguraci do ini. No a konečně mějme formulář TMainForm, který bude následníkem TBasicForm. Bude umět vše co předchůdce a navíc tam přidáme požadované prvky.
Výhod je při dobrém návrhu několik:
Jelikož je Object Pascal přísně typovým jazykem existuje také typ procedura (a v novějších verzích třeba i anonymní metody). Ve skutečnosti je to v podstatě ukazatel na proceduru nebo funkci.
Tím, že zadefinujeme tento typ, získáme možnost bezpečně přiřazovat nebo předávat odkaz a kompilátor nikdy neztratí kontrolu nad parametry, čímž minimalizujeme možnost předání špatného typu (nebo počtu) parametru. Použil jsem to např. v článku o DLL.
type
TMyProc = function (x,y:Integer):Integer;
TMyMethod = function (x,y:Integer):Integer of Object;
První deklarace definuje funkci s dvěma parametry a návratovým typem Integer. Druhá deklarace definuje metodu se stejnou syntaxí.
function Test(a,b:Integer):Integer;
begin
Result:=a+b
end;
var
mp:TMyProc;
begin
mp:=Test;
writeln(mp(1,2));
end;
Object Pascal (stejně jako třeba C#) neumí vícenásobnou dědičnost, ale jen vícenásobnou dědičnost rozhraní, anglicky Interface (u nás na Moravě někdy jako meziksicht). První předchůdce musí být následníkem TObject. Vhodným objektem je TInterfacedObject, který má implementované metody pro počítání referencí. Následně je uveden seznam rozhraní, které je nutno implementovat.
type
IRozhrani1=Interface
procedure Metoda;
end;
TMyObject=class(TInterfacedObject, IRozhrani1)
procedure Metoda;
end;
implementation
procedure TMyObject.Metoda;
begin
end;
Někdy je třeba umožnit kompilaci rozdílných verzí programu podle požadovaných podmínek. Např. vytváříme shareware, tak při startu necháme zobrazovat informaci o tom shareware, ale chceme mít i možnost pro platící vytvořit verzi bez informace. Rozlišení provedeme pomocí podmíněné kompilace. Parametry pro ni se dají specifikovat jak v IDE, tak třeba v kódu.
Níže třeba společný kód pro Kylix a Delphi.
uses
{$IFDEF LINUX}
QForm, QMenus, QGraphics …,
{$ELSE}
Form, Menus, Graphics …,
{$ENDIF}
Classes, SysUtils …
Pro více informací nahlédněte do nápovědy.
Kromě property považuji za další méně známý stavební prvek Delphi strukturovanou obsluhu výjimek. V podstatě se jedná o konstrukce try..except, try..finally a raise. První konstrukce odchytí výjimku, druhá zabezpečí provedení kódu jak v případě bez chyby tak s chybou a raise vyhodí vyjímku (případně poslední výjimku - viz příklad).
Mějte na paměti, že na rozdíl od klasického programování třeba v C, kdy programátor jako trubka by měl důsledně testovat návratové hodnoty jednotlivých volání funkcí (třeba otevření souboru, test alokace paměti atd), v Delphi prostě napíšeme kód tak, jak by měl v ideálním případě proběhnout a pouze tento kód obalíme obsluhou výjimek, které očekáváme a zbytek necháme na globálním ošetření, třeba takto (všimněte si zanoření finally a except):
procedure TForm1.btn1Click(Sender: TObject);
var
sl: TStringList;
const
csFileName = 'data.txt';
begin
try
sl := TStringList.Create;
try
sl.LoadFromFile(csFileName);
ShowMessage(sl.Text);
finally
FreeAndNil(sl);
end;
except
on E: EFOpenError do
begin
ShowMessage(Format('Soubor [%s] nenalezen!', [csFileName]))
end
else
raise
end;
end;
Ať se stane co se stane, tak sekcí finally to projde vždy. Toto je duležité, jelikož i něco jako:
try
blabla
if něco then exit;
finally
// se provede i při exit
end;
A poslední poznámka: ohledně umístění try…finally – try až za vytvořením objektu.
o := TStringList.Create;
try
finally
FreeAndNil(o);
end;
Mnoho programátorů nemá příkaz with rádo, a postupem časem se k nim čím dál více řadím. Přesto některé konstrukce jsou celkem populární:
with TMyInputDialog.Create(nil) do
begin
try
if ModalResult <> mrOK then Exit;
finally
Free;
end;
end;
nebo pokud pracujeme např. s jedním záznamem nebo objektem, tak si ušetříme dlouhé vypisování (a speciálně u složitých property i můžeme za použití with zrychlit kód, ale obecně se vyplatí spíše myslet na budoucnost a nahradit with lokální proménnou). Dokonce můžeme provést with objekt1, objekt2 do, ale to už musíme být velmi opatrní a raději bych to nedělal.
Někdy třeba používám tuto konstrukci
with ds.FieldByName(‘Name’) do
begin
if AsString = OldValue then
AsString := ‘xxxx’;
end;
kde FieldByName vrátí objekt, nad kterým se dále pracuje.
Častý argument (se kterým absolutně souhlasím) proti with je, že změna v rozhraní (přidání/odebrání/přejmenování vlastnosti) může způsobit že kód se zkompiluje, ale bude dělat něco jiného, resp. bude přistupovat k vlastnostem jiného objektu.
Datum: 2010-05-17 23:15:00 Tagy: delphi, začátečníci, Object Pascal, super