Wykład czternasty 1. Polimorfizm Ostatni wykład zakończyliśmy stwierdzeniem, że możemy obiektowi dowolnej klasy przypisa ć obiekt klasy dziedziczącej po tej klasie. Przypisanie takie obejmuje jednak jedynie wartości pól wspólnych dla klas obu obiektów. Może by ć ono dokonywane zarówno poprzez jawne użycie instrukcji przypisania, jak i za pośrednictwem przekazania przez warto ść. Technika obiektowa pozwala na bardziej wyrafinowane korzystanie z obiektów, ale wymaga użycia wskaźników i zapoznania si ę z dwoma nowymi pojęciami: metodami wirtualnymi i konstruktorami. Własno ść instrukcji przypisania, która dotyczy obiektów klas należących do jednej hierarchii dziedziczenia jest zachowana równie ż w przypadku wskaźników, co oznacza, że wskaźnik klasy bazowej może przechowywa ć adres obiektu dowolnej klasy pochodnej. Ta własność okazuje si ę by ć bardzo przydatna w połączeniu z późnym wią zaniem adresów metod. Dotychczas adres metody, która ma by ć wywołana by ł ustalany na etapie kompilacji. Język Object Pascal pozwala na odroczenie określenia tego adresu do czasu wykonania programu, co nazywane jest późnym lub dynamicznym wiązaniem i ma ścisły związek z tzw. polimorficznym zachowaniem metod. Aby adres metody by ł ustalany na etapie wykonania programu za jej deklaracj ą należy umieści ć słowo kluczowe virtual. Tak zadeklarowana metoda jest nazywana metod ą wirtualn ą lub metod ą polimorficzn ą. Polimorficzne zachowanie metody polega na tym, że kiedy na obiekt klasy pochodnej wskazuje wskaźnik klasy bazowej i przy pomocy tego wskaźnika wywoływana jest metoda, to zostanie wywołana metoda właściwa dla klasy obiektu, a nie dla klasy wskaźnika. By ten mechanizm zadziała ł konieczne jest, aby ta metoda była zadeklarowana w klasie bazowej i to jako metoda wirtualna. Jeśli metoda została zadeklarowana w klasie bazowej jako wirtualna, to w klasach pochodnych nie można jej deklaracji zmieni ć z powrotem na statyczn ą (pomin ąć słowo kluczowe virtual), natomiast odwrotna sytuacja jest możliwa. Nie jest bezpośrednio możliwe wywołanie, przy pomocy wskaźnika klasy bazowej metod, które w tej klasie nie zostały zadeklarowane, a s ą dodane do klas pochodnych. Ten problem można jednak rozwiąza ć i rozwiązanie to zostanie przedstawione w dalszej części wykładu. Adresy metod wirtualnych s ą ustalane na podstawie tablicy metod wirtualnych ( ang. VMT = Virtual Method Table). Jest to specjalna tablica umieszczana w segmencie danych po uruchomieniu programu. Warunkiem stworzenia tej tablicy jest zadeklarowanie w klasie specjalnej metody, która nazywana jest konstruktorem. Deklaracja tej metody określona jest następującym schematem: constructor nazwa; Konstruktor w języku Object Pascal może si ę nazywa ć dowolnie, zazwyczaj jednak nazywa si ę go init lub inicjuj. Może on równie ż by ć metod ą pust ą, ale zazwyczaj s ą w nim umieszczane instrukcje związane z inicjalizacj ą pól obiektu. Konstruktor jest jedyn ą metod ą, która nie może by ć wirtualna, gdy ż jest on odpowiedzialny za powiązanie VMT z obiektem 1. Wirtualna tablica metod jest tworzona dla każdej klasy, która zawiera konstruktor. W przypadku, kiedy taka klasa nie posiada metod wirtualnych, to rozmiar tej tablicy wynosi 8 bajtów i przechowuje ona między innymi rozmiar obiektu klasy oraz zanegowany rozmiar obiektu klasy. Każda metoda wirtualna powoduje zwiększenie rozmiaru tej tablicy o 4 bajty, w których jest zapisywany adres tej metody. Kiedy tworzony jest obiekt konstruktor dokonuje inicjalizacji dwubajtowego pola tego obiektu, które przechowuje adres (dokładniej: tylko offset) VMT. Pole to jest tworzone niejawnie przez kompilator. Możemy ustalić jakiej faktycznie klasy jest wskazywany przez wskaźnik obiekt posługując si ę funkcj ą TypeOf, która zwraca adres wirtualnej tablicy metod właściwej klasy obiektu. Dzięki temu możemy obiekt rzutowa ć na wskaźnik odpowiedniej klasy i wywoła ć metody, które s ą obecne w tej klasie, ale nie s ą obecne w klasie bazowej. Do tego zagadnienia wrócimy na przyszłych wykładach. Oto przykład ilustrujący działanie mechanizmu polimorfizmu: 1 program Uniwersum; 2 uses cn; 3 type Wszechswiat=object 4 procedure drukuj(ob:pcialoniebieskie); 5 end; 6 var 7 ws:wszechswiat; 8 gw:gwiazda; 9 pl:planeta; 10 11 procedure Wszechswiat.drukuj(ob:PCialoNiebieskie); 12 begin 13 ob^.drukuj; 14 end; 15 16 begin 17 pl.inicjuj; 18 gw.inicjuj; 19 ws.drukuj(@gw); 20 ws.drukuj(@pl); W programie zdefiniowano cztery klasy: CialoNiebieskie (CiałoNiebieskie), Planeta, Gwiazda i Wszechswiat (Wszechświat). Przyjrzyjmy si ę najpierw klasom w module. Tworz ą one drzewo dziedziczenia, którego korzeniem jest klasa CialoNiebieskie - dwie pozostałe dziedzicz ą po niej. Każda z tych klas posiada konstruktor. W klasie CialoNiebieskie jest on metod ą pust ą, w pozostałych wywołuje inne metody odpowiedzialne za nadawanie wartości poszczególnym polom odpowiednich klas. Każda klasa pochodna oprócz pól i metod dziedziczonych po klasie bazowej posiada równie ż własne pola oraz metody je obsługujące. Jedn ą z metod wspólnych dla wszystkich klas jest metoda drukuj. To co j ą odróżnia od pozostałych jest to, że jest metod ą wirtualn ą. Ta metoda jest nadpisywana w kolejnych klasach, tak aby obsługiwała pola charakterystyczne dla tych klas. Jeśli jej nie nadpisalibyśmy, to będzie si ę zachowywała tak jak metoda z klasy bazowej, gdy ż te ż podlega dziedziczeniu. W pliku zawierającym blok główny programu stworzono jeszcze jedn ą klas ę. Ta klasa zawiera tylko jedną metod ę, która przez parametr pobiera wskaźnik do obiektu klasy CialoNiebieskie. W bloku głównym programu tworzony jest obiekt tej klasy i wywoływana jest metoda drukuj tego obiektu. W wierszu 18 jest jej przekazany przez parametr adres obiektu klasy Gwiazda, a w wierszu 19 adres obiektu klasy Planeta. Okazuje si ę, że w metodzie drukuj obiektu klasy Wszechswiat zostały wywołane metody drukuj właściwe dla klas obiektów, których adresy zostały przekazane tej metodzie. Jeśli zmienilibyśmy t ę metod ę w ten sposób, że będzie korzystała nie ze wskaźnika, ale z parametru klasy CialoNiebieskie przekazującego przez warto ść, to program będzie zachowywa ł si ę podobnie do programu z poprzedniego wykładu, który nie korzysta ł z mechanizmu polimorfizmu. 21 end. 1 Robi to w sposób niejawny, czyli od strony programisty nie wymaga to dodatkowych zabiegów, poza stworzeniem takiej metody, która, jak to ju ż wcześniej napisano, może by ć metod ą pust ą. 1
1 unit cn; 2 interface 3 uses crt; 4 type 5 6 PCialoNiebieskie = ^CialoNiebieskie; 7 8 CialoNiebieskie = object 9 private 10 nazwa:string; 11 masa:integer; 12 x,y,z:real; 13 function podajmase:integer; 14 function podajnazwe:string; 15 function podajx:real; 16 function podajy:real; 17 function podajz:real; 18 public 19 constructor inicjuj; 20 procedure ustawnazwe(const n:string); 21 procedure ustawwspolrzedne(x1,y1,z1:real); 22 procedure ustawmase(m:integer); 23 procedure drukuj; virtual; 24 end; 25 26 Planeta = object(cialoniebieskie) 27 private 28 atmosfera:boolean; 29 public 30 constructor inicjuj; 31 function podajatmosfere:boolean; 32 procedure ustawatmosfere(a:boolean); 33 procedure drukuj; virtual; 34 end; 35 36 Gwiazda = object(cialoniebieskie) 37 private 38 temperaturapowierzchni:integer; 39 public 40 constructor inicjuj; 41 function podajtemperaturepowierzchni:integer; 42 procedure ustawtemperaturepowierzchni(t:integer); 43 procedure drukuj; virtual; 44 end; 45 2
46 implementation 47 48 function CialoNiebieskie.podajMase:integer; 49 begin 50 podajmase:=masa; 51 end; 52 53 function CialoNiebieskie.podajNazwe:string; 54 begin 55 podajnazwe:=nazwa; 56 end; 57 58 function CialoNiebieskie.podajZ:real; 59 begin 60 podajz:=z; 61 end; 62 63 function CialoNiebieskie.podajY:real; 64 begin 65 podajy:=y; 66 end; 67 68 function CialoNiebieskie.podajX:real; 69 begin 70 podajx:=x; 71 end; 72 73 procedure CialoNiebieskie.ustawNazwe(const n:string); 74 begin 75 nazwa:=n; 76 end; 77 78 procedure CialoNiebieskie.ustawWspolrzedne(x1,y1,z1:real); 79 begin 80 x:=x1; 81 y:=y1; 82 z:=z1; 83 end; 84 85 procedure CialoNiebieskie.ustawMase(m:integer); 86 begin 87 Masa:=m; 88 end; 89 90 constructor CialoNiebieskie.inicjuj; 3
91 begin 92 end; 93 94 procedure CialoNiebieskie.drukuj; 95 begin 96 clrscr; 97 writeln('nazwa: ',podajnazwe); 98 writeln('masa: ',podajmase); 99 writeln('współrzędne: ',podajx, ',', podajy, ',', podajz); 100 end; 101 102 constructor Planeta.inicjuj; 103 begin 104 ustawmase(2000); 105 ustawwspolrzedne(1,0,0); 106 ustawnazwe('ziemia'); 107 ustawatmosfere(true); 108 end; 109 110 procedure Planeta.ustawAtmosfere(a:boolean); 111 begin 112 atmosfera:=a; 113 end; 114 115 function Planeta.podajAtmosfere:boolean; 116 begin 117 podajatmosfere:=atmosfera; 118 end; 119 120 procedure Planeta.drukuj; 121 begin 122 inherited drukuj; 123 if podajatmosfere=true then writeln('atmosfera: tak') else 124 writeln('atmosfera: nie'); 125 readln; 126 end; 127 128 constructor Gwiazda.inicjuj; 129 begin 130 ustawmase(20000); 131 ustawwspolrzedne(0,0,0); 132 ustawnazwe('słońce'); 133 ustawtemperaturepowierzchni(5000); 134 end; 135 4
136 procedure Gwiazda.ustawTemperaturePowierzchni(t:integer); 137 begin 138 temperaturapowierzchni:=t; 139 end; 140 141 function Gwiazda.podajTemperaturePowierzchni:integer; 142 begin 143 podajtemperaturepowierzchni:=temperaturapowierzchni; 144 end; 145 146 procedure Gwiazda.drukuj; 147 begin 148 clrscr; 149 inherited drukuj; 150 writeln('temperatura powierzchni: ', podajtemperaturepowierzchni); 151 readln; 152 end; 153 end. Modu ł cn jest równie ż wykorzystywany w krótkim programie, który wypisuje cz ęść zawartości tablicy metod wirtualnych na ekran: 1 program tablica_vmt; 2 uses crt,cn; 3 type Rek = record 4 SizePoz,SizeNeg,Pom1,Pom2:Word; 5 Adr_Metody_1:Pointer; 6 end; 7 var 8 pl:planeta; 9 gw:gwiazda; W programie tym stworzony zosta ł typ rekordowy opisujący elementy tablicy metod wirtualnych. Typ ten jest używany przez procedurę display_vmt, która dokonuje rzutowania danych wskazywanych przez wskaźnik bez określonego typu na ten typ rekordowy i wyświetlenia na ekran zawartości pól tego rekordu (chodzi tu o nadanie określonej formy danym, które zostały pobrane wprost z pamięci operacyjnej). Adresy VMT poszczególnych klas s ą uzyskiwane dzięki wcześniej wspomnianej funkcji TypeOf. Program napisano na podstawie skryptu Zofii Kruczkiewicz pt.: Metody programowania obiektowego. 10 11 wsk:pointer; 12 13 procedure display_vmt(adr:pointer); 14 begin 15 with rek(adr^) do 16 begin 17 writeln('rozmiar obiektu: ',SizePoz); 18 writeln('rozmiar zanegowany: ',SizeNeg); 19 writeln('pom1: ',Pom1, ' Pom2: ',Pom2); 20 writeln('adres drukuj:',seg(adr_metody_1^),':',ofs(adr_metody_1^)); 21 end; 22 end; 23 24 begin 5
25 clrscr; 26 pl.inicjuj; 27 gw.inicjuj; 28 wsk:=typeof(pl); 29 display_vmt(wsk); 30 wsk:=typeof(gw); 31 display_vmt(wsk); 32 readln; 33 end. 2. Podsumowanie Polimorfizm jest bardzo pożytecznym mechanizmem w programowaniu obiektowym. Pełni ę jego możliwości będziemy mogli wykorzysta ć po zapoznaniu si ę z obiektami tworzonymi dynamicznie. Czasem, aby ułatwi ć wywoływanie metod z klas pochodnych, do klas bazowych dodaje si ę wszystkie metody jakie mog ą wystąpi ć w ich klasach pochodnych i umieszcza si ę w nich instrukcj ę abstract, która powoduje błąd czasu wykonania, jeśli metoda j ą zawierająca zostanie wywołana. Takie metody nazywa si ę metodami abstrakcyjnymi, a klasy które je zawieraj ą klasami abstrakcyjnymi. Dosy ć często używa si ę takich klas w innym celu aby uczyni ć model obiektowy bardziej odpowiadającym rzeczywistemu. Klasami abstrakcyjnymi mog ą by ć klasy reprezentujące takie pojęcia jak: figura geometryczna, czy ssak. Problem metod i klas abstrakcyjnych zostanie szerzej omówiony na następnym wykładzie. W wyżej zaprezentowanym programie należy zwróci ć uwag ę na użycie operatora @. Potencjalnie ten operator jest niebezpieczny, poniewa ż kompilator nie sprawdza typu wskaźnika, jaki on zwraca, co oznacza, że za jego pomoc ą do metody drukuj klasy Wszechś wiat możemy przekaza ć wskaźnik na dowoln ą zmienn ą, nawet tak ą, która nie jest obiektem (np.: wskaźnik na zmienn ą typu integer). Aby pozby ć si ę tego operatora możemy zastąpi ć przekazanie wskaźnika do metody przekazaniem przez zmienn ą, tzn. przekształci ć nagłówek metody drukuj na procedure drukuj(var ob:cialoniebieskie); i zmieni ć w ciele tej metody ob^.drukuj na ob.drukuj. Przekazanie przez zmienn ą jest przekazaniem wskaźnika, ale w sposób bezpieczny (z kontrol ą typów). Przekazanie przez zmienn ą nazywane jest równie ż przekazaniem przez referencj ę. Rol ę referencji, czyli bezpiecznego wskaźnika pełni parametr poprzedzony słowem kluczowym var. 6