17. Polymorfizmus


Posledná zmena: 1.11.2002

Banner Text 19.11.2002

    čo sme sa doteraz naučili

    • objektové programovanie pre nás znamená, že môžeme definovať nové triedy a tiež vytvárať inštancie týchto tried
    • triedy majú dve vlastnosti: zapuzdrenie (v jednom celku sú stavové premenné a metódy) a dedičnosť

    čo sa budeme dnes učiť

    • bez ďalšej vlastnosti - polymorfizmus - by bola práca s objektmi veľmi obmedzená
    • polymorfizmus je silný mechanizmus zdieľania metód
    • vďaka kompatibilite inštancií môžeme pracovať s polymorfnými objektmi aj polymorfnými parametrami

trieda TKruh a TStvorec

Na 13. prednáške - úvod do objektového programovania - sme robili projekt, v ktorom sme zadefinovali triedu TKruh:

už známa definícia triedy TKruh:

var
  g:TCanvas;

type
  TKruh = class
  private
    x,y,r:integer;
    f:TColor;
    vid:boolean;
  public
    constructor Create(xx,yy,rr:integer);
    procedure ukaz;
    procedure skry;
    procedure ZmenFarbu(ff:TColor);
    procedure posun(dx,dy:integer);
  end;

constructor TKruh.Create(xx,yy,rr:integer);
begin
  x:=xx; y:=yy; r:=rr; vid:=false; f:=clBlack;
end;

procedure TKruh.ukaz;
begin
  g.Pen.Color:=f;
  g.Brush.Style:=bsClear;
  g.Ellipse(x-r,y-r,x+r,y+r);
  vid:=true;
end;

procedure TKruh.skry;
begin
  g.Pen.Color:=clWhite;
  g.Brush.Style:=bsClear;
  g.Ellipse(x-r,y-r,x+r,y+r);
  vid:=false;
end;

procedure TKruh.ZmenFarbu(ff:TColor);
begin
  f:=ff;
  if vid then ukaz;
end;

procedure TKruh.posun(dx,dy:integer);
var
  b:boolean;
begin
  b:=vid; if vid then skry;
  inc(x,dx); inc(y,dy);
  if b then ukaz;
end;
  • na tri tlačidlá sme priradili vytvorenie troch inštancií tejto triedy - štvrté tlačidlo hýbalo s týmito kruhmi, piate rušilo všetky inštancie:

a túto triedu sme používali napr. takto:

var
  a,b,c:TKruh;
  poc:integer;

procedure TForm1.FormCreate(Sender: TObject);
begin
  g:=Image1.Canvas;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  a:=TKruh.Create(100,100,50);
  with a do begin ZmenFarbu(clGreen); ukaz; end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  b:=TKruh.Create(300,200,100); b.ZmenFarbu(clRed); b.ukaz;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  c:=TKruh.Create(200,250,70); c.ukaz;
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  poc:=100;
  Timer1.Enabled:=true;  
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if a<>nil then a.posun(3,2);
  if b<>nil then b.posun(-5,-1);
  if c<>nil then c.posun(2,-3);
  dec(poc);
  if poc<=0 then Timer1.Enabled:=false;
end;

procedure TForm1.Button5Click(Sender: TObject);
begin
  if a<>nil then
    with a do begin
      if vid then skry;
      Free; a:=nil;
    end;
  if b<>nil then
    with b do begin
      if vid then skry;
      Free; b:=nil;
    end;
  if c<>nil then
    with c do begin
      if vid then skry;
      Free; c:=nil;
    end;
end;

dodefinujeme novú triedu TStvorec - podtriedu TKruh:

type
  TStvorec = class(TKruh)
  public
    procedure ukaz;
    procedure skry;
    procedure ZmenFarbu(ff:TColor);
    procedure posun(dx,dy:integer);
  end;

procedure TStvorec.ukaz;
begin
  g.Pen.Color:=f;
  g.Brush.Style:=bsClear;
  g.Rectangle(x-r,y-r,x+r,y+r);
  vid:=true;
end;

procedure TStvorec.skry;
begin
  g.Pen.Color:=clWhite;
  g.Brush.Style:=bsClear;
  g.Rectangle(x-r,y-r,x+r,y+r);
  vid:=false;
end;

procedure TStvorec.ZmenFarbu(ff:TColor);
begin
  f:=ff;
  if vid then ukaz;
end;

procedure TStvorec.posun(dx,dy:integer);
var
  b:boolean;
begin
  b:=vid; if vid then skry;
  inc(x,dx); inc(y,dy);
  if b then ukaz;
end;

jedna z premenných - inštancií kruhu bude teraz štvorec:

var
  a:TKruh;
  b:TStvorec;
  c:TKruh;
...

procedure TForm1.Button2Click(Sender: TObject);
begin
  b:=TStvorec.Create(300,200,60);
  b.ZmenFarbu(clRed);
  b.ukaz;
end;
  • takto vylepšený projekt funguje správne - druhé tlačidlo vytvorí štvorec a štvrté tlačidlo rozhýbe všetky útvary.

  • ak porovnáme metódy ZmenFarbu a posun v oboch triedach, zistíme, že sú úplne identické, takže sa ich pokúsime v triede TStvorec vyhodiť: kompilátoru to nevadí - pochopí, že sa majú zdediť, ale projekt už nefunguje správne ...

Statické metódy

  • ak zavoláme TStvorec.posun, zavolá sa TKruh.skry a nie TStvorec.skry
    • TKruh.posun  ---->  TKruh.skry
           ^
           |
      TStvorec.posun      TStvorec.skry

Virtuálne metódy

  • potrebovali by sme, aby TKruh.posun "pochopil", že bol zavolaný z potomka a teda nebude volať svoj TKruh.skry ale potomkovu metódu skry:
    • TKruh.posun     --    TKruh.skry         // skry je virtuálna
            ^             \
            |                  \
      TStvorec.posun --> TStvorec.skry     // skry je virtuálna

  • zrejme sa to nedá urobiť počas kompilácie, lebo TKruh.posun nemôže "tušiť", že niekedy v budúcnosti si niekto niekde inde naprogramuje veľa potomkov, ktoré budú volať TKruh.posun

Polymorfizmus

  • metóda sa zdieľa v rôznych stupňoch objektovej hierarchie ->
    • napr. vtedy, ak rôzne triedy zdedia "spoločnú" metódu (napr. metódu posun), ale detaily pre rôzne objekty zodpovedajú ich zvláštnostiam (napr. metóda skry)

Včasná a neskorá väzba

  • kompilátor - okamžité rozhodnutie pri statických metódach
  • odložené rozhodnutie pri virtuálnych metódach
    • zatiaľ nevieš, ktorú metódu (ktorého objektu), keď príde čas volania, opýtaj sa inštancie (premennej typu objekt)
  • pri statických metódach kompilátor hľadá zodpovedajúce meno v hierarchii smerom k predkom od danej úrovne (kde je definovaná metóda)
  • pri virtuálnych metódach nie kompilátor, ale počas behu sa hľadá zodpovedajúce meno v hierarchii smerom k predkom, ale od úrovne inštancie
  • včasná väzba = early binding - pri statických
  • neskorá väzba = late binding - pri virtuálnych

Virtuálne metódy

  • v deklarácii objektu virtual
  • ak je v nejakej triede virtuálna metóda => všetci potomkovia, ak majú metódu tohoto mena, mali by mať override (prekryje virtuálnu metódu) a s rovnakou hlavičkou - bez override sa táto definícia bude chápať ako "predefinovanie" virtuálnej na statickú ...
  • každý objekt obsahuje VMT (Virtual Method Table):
    • konštruktor objektu inicializuje napojenie na túto tabuľku
    • každá inštancia obsahuje len referenciu na túto (jedinú pre danú triedu) tabuľku - je to prvý položka v zázname
    • hovoríme, že konštruktor môže byť použitý aj na inicializovanie stavových premenných - jeho "hlavná" funkcia je vyhradiť pamäť pre objekt a inicializovať VMT !!!

Konštruktory a deštruktory

      constructor Create;   // ľubovoľné meno - môže mať aj parametre
      destructor Destroy;
      override;   // v TObject je Destroy virtual

Kompatibilita tried (objektových typov)

kompatibilita je jednosmerná => odvodený typ dedí kompatibilitu so svojimi predkami (kompatibilita s predkami nie potomkami)

  • medzi inštanciami
  • medzi formálnymi a skutočnými parametrami

Príklady:

máme deklarácie:

var
  a,c:TKruh;
  b:TStvorec;

potom je povolené:

     a:=TKruh.Create(...);
     b:=TStvorec.Create(...);
     c:=b;      // c referencuje tú istú inštanciu ako b
     c:=TStvorec.Create(...);

procedure zmen(k:TKruh);
begin
  k.ZmenFarbu(clBlue);
  k.posun(1,1);
end;

a tiež môže byť volané:

  zmen(b);   // 
      ===> formálny parameter k je polymorfný objekt (rôznorodý)
      ===> potomok môže kdekoľvek nahradiť predka (predok môže zastupovať potomka)

Polymorfný objekt

  • keď do procedúry príde ako parameter nejaký objekt, môže to byť vlastne ľubovoľný potomok tohoto objektu (zatiaľ nevieme zistiť ktorý)
  • umožňuje spracovať objekty, ktorých typ nie je známy
  • umožňuje využiť metódy predkov so špecifickými zmenami - úpravami

Spôsob (metóda) návrhu hierarchie objektov

  • vytypujeme spoločné vlastnosti a metódy => bázová trieda (typ)
  • rozhodneme, ktoré metódy budú virtual
    • pre budúceho používateľa umožníme rozširovanie metód
    • môžeme vyrobiť aj "prázdne" virtuálne metódy - pre perspektívne rozširovanie
  • rozširovanie (rozšíriteľnosť = extensibility)
    • umožňuje meniť niektoré časti bez zdrojových textov
    • vďaka virtuálnym metódam možno meniť činnosť niektorých procedúr

príklad s polymorfným štvorcom by mal vyzerať takto:

type
  TKruh = class
  ...
    procedure ukaz; virtual;
    procedure skry; virtual;
  ...
  end;

  TStvorec = class(TKruh)
  public
    procedure ukaz; override;
    procedure skry; override;
  end;

použijeme:

var
  a:TKruh;
  b:TKruh;
  c:TKruh;

...

procedure TForm1.Button2Click(Sender: TObject);
begin
  b:=TStvorec.Create(300,200,60); b.ZmenFarbu(clRed); b.ukaz;
end;

príklad na polymorfné pole

zadefinujeme novú triedu TTroj - potomok TKruh:

{ Trojuholnik }
type
  TTroj = class(TKruh)
  public
    procedure ukaz; override;
    procedure skry; override;
  end;

procedure TTroj.ukaz;
begin
  g.Pen.Color:=f;
  g.Brush.Style:=bsClear;
  g.PolyLine([Point(x-r,y),Point(x+r,y),Point(x,y-r),Point(x-r,y)]);
  vid:=true;
end;

procedure TTroj.skry;
begin
  g.Pen.Color:=clWhite;
  g.Brush.Style:=bsClear;
  g.PolyLine([Point(x-r,y),Point(x+r,y),Point(x,y-r),Point(x-r,y)]);
  vid:=false;
end;

pole p bude polymorfné pole:

const
  n=20;

var
  p:array[1..n] of TKruh;

procedure TForm1.FormCreate(Sender: TObject);
var
  i:integer;
begin
  g:=Image1.Canvas;
  for i:=1 to n do p[i]:=nil;
  randomize;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i,x,y,r:integer;
begin
  for i:=1 to n do begin
    x:=random(Image1.Width);
    y:=random(Image1.Height);
    r:=random(80)+10;
    case random(3) of
      0: p[i]:=TKruh.Create(x,y,r);
      1: p[i]:=TStvorec.Create(x,y,r);
      2: p[i]:=TTroj.Create(x,y,r);
    end;
  end;
  for i:=1 to n do p[i].ukaz;
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  poc:=100;
  Timer1.Enabled:=true;
end;

procedure TForm1.Button5Click(Sender: TObject);
var
  i:integer;
begin
  for i:=1 to n do
    if p[i]<>nil then
      with p[i] do begin
        if vid then skry;
        Free;
        p[i]:=nil;
      end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var
  i:integer;
begin
  Timer1.Enabled:=false;
  for i:=1 to n do
    if p[i]<>nil then p[i].posun(i mod 5-2,i mod 3-1);
  dec(poc);
  if poc>0 then Timer1.Enabled:=true;
end;

procedure TForm1.FormDestroy(Sender: TObject);
var
  i:integer;
begin
  for i:=1 to n do p[i].Free;
end;

Poznámky:

  • zatiaľ o prvkoch polymorfného poľa p nevieme zistiť, akého sú naozaj typu
  • vieme, že buď sú to inštancie triedy kruh alebo jeho potomkovia
  • každý prvok "sám seba vie" správne ukázať, skryť a zafarbiť, posunúť


© 2002 AB, KVI
blaho@fmph.uniba.sk