Podstawy Programowania – semestr drugi Wyk ad czternasty ł 1
Transkrypt
Podstawy Programowania – semestr drugi Wyk ad czternasty ł 1
Podstawy Programowania – semestr drugi 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 1 powiązanie VMT z obiektem . 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: 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. 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); 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 Podstawy Programowania – semestr drugi 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 Podstawy Programowania – semestr drugi 46 implementation 47 48 function CialoNiebieskie.podajMase:integer; 49 begin 50 51 podajMase:=masa; end; 52 53 function CialoNiebieskie.podajNazwe:string; 54 begin 55 56 podajNazwe:=nazwa; end; 57 58 function CialoNiebieskie.podajZ:real; 59 begin 60 61 podajZ:=z; end; 62 63 function CialoNiebieskie.podajY:real; 64 begin 65 66 podajY:=y; end; 67 68 function CialoNiebieskie.podajX:real; 69 begin 70 71 podajX:=x; end; 72 73 procedure CialoNiebieskie.ustawNazwe(const n:string); 74 begin 75 76 nazwa:=n; 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 88 Masa:=m; end; 89 90 constructor CialoNiebieskie.inicjuj; 3 Podstawy Programowania – semestr drugi 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 113 atmosfera:=a; end; 114 115 function Planeta.podajAtmosfere:boolean; 116 begin 117 118 podajAtmosfere:=atmosfera; 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 Podstawy Programowania – semestr drugi 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: 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”. 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; 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 Podstawy Programowania – semestr drugi 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