Rozdział 3.
Klasy i programowanie zorientowane obiektowo
Klasy stanowią główny temat tego rozdziału. Na nich właśnie opiera się Object Pascal oraz VCL. Stanowią one fundament wielu nowoczesnych języków programowania. Dowiesz się, co to są klasy i jak ich używać. Nieznane dotąd terminy, takie jak: obiekty, dziedziczenie i hermetyzacja przestaną być dla Ciebie tajemnicą. Na początek jednak poznasz jeszcze kilka elementów Object Pascala, o których nie wspomniałem w poprzednim rozdziale.
Zbiory
Zbiory są bardzo często wykorzystywane w Delphi, musisz więc wiedzieć, czym są i jak się ich używa.
|
Zbiór (typ mnogościowy) jest zestawem wartości jednego typu. |
Nie jest ta definicja zbyt obrazowa. Najlepiej więc będzie, jeśli jak zwykle przytoczę jakiś przykład. Niech będzie nim właściwość Style obiektu VCL o nazwie Font. Może ona przyjmować następujące wartości:
fsBold
fsItalic
fsUnderline
fsStrikeout
Czcionka (Font) może posiadać jeden lub więcej z powyższych stylów, może też nie mieć żadnego z nich albo mieć je wszystkie. Jest to właśnie zbiór stylów.
Jak używa się zbiorów? Niech za przykład posłuży ponownie właściwość Style. Zwykle wybierasz pewne wartości ze zbioru stylów w czasie projektowania aplikacji. Czasem jednak zachodzi potrzeba zmiany tego zestawu w czasie wykonywania programu. Powiedzmy, że chcesz nadać czcionce dodatkowe atrybuty Bold i Italic. Jednym ze sposobów jest zadeklarowanie zmiennej typu TFontStyles i dodanie do zbioru stylów fsBold i fsItalic:
var
Styles : TFontStyles;
begin
Styles := Styles + [fsBold, fsItalic];
end;
Powyższy kod dodaje elementy fsBold i fsItalic do zbioru Styles. Nawiasy kwadratowe oznaczają, że chodzi o elementy zbioru. Użyte w ten sposób nazywane są konstruktorem zbioru. Kod ten nie zmienia stylu czcionki, na razie do zbioru dodane są tylko odpowiednie elementy. Aby rzeczywiście zmienić styl czcionki, należy nowo stworzony zbiór przypisać właściwości Font.Style odpowiedniego komponentu:
Memo.Font.Style := Styles;
Teraz chcemy na przykład, żeby czcionka ta nie miała dłużej atrybutu Italic. Trzeba więc usunąć ten styl ze zbioru:
Styles := Styles - [fsItalic];
Często trzeba sprawdzić, czy określona wartość jest już w zbiorze. Powiedzmy, że chcesz wiedzieć, czy czcionka posiada już atrybut Bold. Sprawdzenie, czy element fsBold jest w zbiorze Styles, wygląda następująco:
if fsBold in Styles then
JakasProcedura;
Czasami też zachodzi potrzeba upewnienia się, że zbiór przed przypisaniem nie posiada wcześniej żadnych wartości. Można to zrobić przypisując mu „zbiór pusty”:
{opróżnienie zbioru}
Styles := [];
{teraz dodanie atrybutów Bold i Italic}
Styles := Styles + [fsBold, fsItalic];
W powyższym przykładzie kasowana jest cała zawartość zbioru, następnie dodawane są do niego dwa elementy. To samo można osiągnąć przypisując zbiorowi wprost te wartości:
Styles := [fsBold, fsItalic];
Żeby zmienić styl czcionki jakiegoś komponentu, nie potrzeba właściwie deklarować w tym celu osobnej zmiennej, można się do odpowiednich właściwości odwoływać bezpośrednio:
Memo.Font.Style := [];
Memo.Font.Style := Memo.Font.Style + [fsBold, fsItalic];
Zbiory są deklarowane przy pomocy słowa kluczowego set. Właściwość FontStyles jest zadeklarowana w pliku źródłowym (GRAPHICS.PAS) biblioteki VCL następująco:
TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeout);
TFontStyles = set of TFontStyle;
W pierwszej linii zadeklarowany jest typ wyliczeniowy TFontStyle (typ wyliczeniowy jest listą możliwych wartości), w drugiej natomiast deklarowany jest zbiór TFontStyles.
Rzutowanie typów
|
Dzięki rzutowaniu typów kompilator może traktować typy danych tak, jakby były one innymi typami danych. |
Oto przykład traktowania typu Char tak, jak typu Integer:
var
Litera : Char;
Liczba : Integer;
begin
Litera := 'A';
Liczba := Integer(Litera);
Label1.Caption := IntToStr(Liczba);
Wyrażenie Integer(Litera) instruuje kompilator, żeby przekonwertował wartość typu Char na wartość typu Integer. Rzutowanie to jest niezbędne, ponieważ przypisywanie wartości typu Char zmiennej typu Integer nie jest dozwolone. Jeżeli spróbowałbyś dokonać tego przypisania bez rzutowania, kompilator wygenerowałby komunikat błędu Incompatible types: 'Integer' and 'Char'.
Po wykonaniu się powyższego przykładu na etykiecie pojawiłby się napis 65 (litera A ma w kodzie ASCII numer 65).
Rzutowanie typów nie zawsze jest jednak możliwe do wykonania. Popatrz na poniższy przykład:
procedure TForm1.Button1Click(Sender: TObject);
var
Pi : Double;
Liczba : Integer;
begin
Pi := 3.14;
Liczba := Integer(Pi);
Label1.Caption := IntToStr(Liczba);
end;
W tym przypadku próbuję przekonwertować liczbę Double na liczbę Integer. Nie jest to dozwolone, więc kompilator wypisze komunikat Invalid typecast. Do konwersji liczb zmiennoprzecinkowych na całkowite służą funkcje Trunc, Floor albo Ceil (obcięcie części ułamkowej, zaokrąglenie do dołu lub do góry).
Wskaźniki mogą być konwertowane z jednego typu na inny z użyciem operatora as (szczegóły o wskaźnikach oraz o operatorach is i as - w następnych rozdziałach).
Wskaźniki
Wskaźniki należą do tych elementów Object Pascala, których opanowanie sprawia najwięcej kłopotów. Są to zmienne, które przechowują adresy innych zmiennych w pamięci komputera; mówi się o nich, że „wskazują” na te zmienne.
|
Wskaźnik jest to zmienna, która zawiera adres innej zmiennej. |
Powiedzmy, że stworzyłeś jakiś rekord, oraz że chcesz przekazać adres tego rekordu do procedury wymagającej wskaźnika jako swego parametru. Adres tej zmiennej rekordowej można uzyskać za pomocą operatora @:
var
Karta001 : TKartaAdresowa;
Wsk : Pointer;
begin
{wpisanie wartości do pól rekordu}
...
Wsk := @Karta001;
FunkcjaWymagajacaWskaznika(Wsk);
end;
Zmienna Wsk (typu Pointer) użyta jest do przechowywania adresu rekordu Karta001. Ten typ wskaźnika nazywany jest wskaźnikiem amorficznym (ang. untyped pointer), ponieważ nieokreślony jest typ zmiennej, na którą wskazuje. Jest to po prostu adres w pamięci. Innym rodzajem wskaźnika jest wskaźnik wskazujący na zmienną typu określonego przy deklarowaniu tego wskaźnika. Stworzyć można na przykład wskaźnik do rekordu TKartaAdresowa. Deklaracja mogłaby mieć postać:
type
PKartaAdresowa = ^TKartaAdresowa;
TKartaAdresowa = record
Imie : string;
Nazwisko : string;
Ulica : string;
Miasto : string;
KodPocztowy : Integer;
end;
Typ PKartaAdresowa jest tu zadeklarowany jako wskaźnik na rekord TKartaAdresowa. Często w praktyce będziesz spotykał rekordy i wskaźniki deklarowane w ten właśnie sposób. Ale, tak na dobrą sprawę, do czego te wskaźniki tak naprawdę są potrzebne? Odpowiedź na to pytanie znajdziesz w następnych akapitach książki.
|
Długie łańcuchy (long strings) są bardzo rzadko używane w rekordach, ponieważ mają one nieokreśloną długość. Stała wielkość rekordu jest ważna przy zapisywaniu go na dysk. Używam więc raczej w rekordach łańcuchów shortstring lub tablic znaków. |
Zmienne statyczne kontra zmienne dynamiczne
W poprzednim rozdziale, przy omawianiu rekordów, przytaczałem kilka przykładów. We wszystkich tych przykładach używałem zmiennych statycznych. Określenie to oznacza, że pamięć dla tych zmiennych przydzielana była w obszarze stosu programu.
|
Alokacja statyczna oznacza przydzielanie pamięci dla zmiennych w obszarze stosu programu. |
|
Stos jest obszarem pamięci operacyjnej rezerwowanej dla programu w czasie jego uruchamiania. |
Cała pamięć potrzebna na zmienne lokalne, wywołania funkcji itd. pochodzi ze stosu programu. Pamięć ta jest alokowana zwykle w momencie startu programu lub w momencie wejścia do podprogramu. Pamięć potrzebna na zmienne lokalne procedury lub funkcji przydzielana jest w momencie wejścia do tej procedury lub funkcji, zwalniana jest natomiast w momencie wyjścia z niej. Analogicznie, w momencie zakończenia programu zwalniana jest cała przydzielona uprzednio dla niego pamięć. Wszystko to odbywa się w pełni automatycznie.
Alokacja statyczna ma swoje wady i zalety. Zaletą jej jest duża szybkość. Minusem zaś jest to, że rozmiar stosu programu nie może być w żaden sposób zmieniony w czasie jego działania. Jeżeli program przekroczy obszar stosu, może zdarzyć się praktycznie wszystko. Program może się zawiesić, może się zacząć dziwnie zachowywać, może działać z pozoru normalnie i zawiesić się w momencie zakończenia. Problem ten ma mniejsze znaczenie w środowisku 32-bitowym niż w 16-bitowym, jednak ciągle istnieje.
Dla zmiennych o typach wbudowanych i dla małych tablic jest to całkowicie wystarczające rozwiązanie. Gdy jednak planujesz używać np. dużych tablic rekordów, dynamiczna alokacja pamięci ze sterty (ang. heap) może okazać się jedynym rozwiązaniem. Sterta oznacza całą wolną pamięć fizyczną komputera plus całą wolną przestrzeń na dysku twardym. W typowym systemie Windows jest to zwykle około 100 MB. Można więc przyjąć, że dostępna pamięć wirtualna jest praktycznie nieograniczona. Okupione jest to jednak pewnym narzutem czasowym na dynamiczny przydział pamięci, zapis i odczyt z dysku itd. W większości przypadków jednak ten narzut jest niezauważalny. Kolejną niedogodnością jest fakt, że przydział dynamiczny pamięci nie odbywa się automatycznie - programista musi o to zadbać sam.
|
Alokacja dynamiczna oznacza przydzielanie pamięci dla zmiennych w obszarze sterty. |
|
Sterta jest to cała dostępna pamięć wirtualna komputera. |
Alokacja dynamiczna i wskaźniki
W programie napisanym w Object Pascalu, dynamiczny przydział pamięci może odbywać się na kilka sposobów. Najlepszym, wydaje się, z nich jest użycie funkcji AllocMem. Funkcja ta przydziela pamięć i wypełnia ją zerami (inne metody to użycie procedury GetMem i funkcji New). Wróćmy jeszcze raz do przykładu z rekordami zawierającymi dane adresowe. Poprzednio, pamięć przydzielana była dla każdego z rekordów statycznie:
var
Karta001 : TKartaAdresowa;
begin
Karta001.Imie := 'Jan';
Karta001.Nazwisko := 'Kowalski';
...
end;
Zróbmy to teraz inaczej - używając dynamicznej alokacji pamięci:
var
Wsk : PKartaAdresowa;
begin
Wsk := AllocMem(SizeOf(TKartaAdresowa));
Wsk^.Imie := 'Jan';
Wsk^.Nazwisko := 'Kowalski';
...
FreeMem(Wsk);
end;
W tej drugiej wersji zamiast deklarować rekord, deklaruję wskaźnik na ten rekord. Przydział pamięci dla tego rekordu odbywa się poprzez wywołanie funkcji AllocMem. Parametrem tej funkcji jest wielkość obszaru (w bajtach) potrzebnej pamięci (do określenia wielkości tego obszaru używam funkcji SizeOf). Po przydzieleniu pamięci dla rekordu można używać wskaźnika na ten rekord tak, jak normalnej zmiennej. Zwróć uwagę, że na końcu zwalniam funkcją FreeMem wcześniej przydzieloną pamięć. Gdybym tego nie zrobił, pamięć ta nie zostałaby zwolniona aż do zamknięcia Windows (kolejne uruchomienia tego programu powodowałyby ciągłe zmniejszanie się dostępnej pamięci).
|
Klasy, w przeciwieństwie do rekordów i tablic, nie posiadają w Object Pascalu formy statycznej - egzemplarz klasy (obiekt) może być utworzony wyłącznie w drodze dynamicznego przydziału pamięci. |
|
Słowo kluczowe nil oznacza „wskazanie puste”. Jeżeli wartością wskaźnika jest nil, znaczy to, że nie wskazuje on na żadne dane: Wskaznik := nil; Można użyć wartości nil do sprawdzenia, czy jakiś wskaźnik wskazuje już na jakieś dane: if Wskaznik = nil then Wskaznik := AllocMem(Rozmiar); |
Odwoływanie się do danych dynamicznych
|
Odwoływanie się do danych dynamicznych jest to odwoływanie się do danych, na które wskazują wskaźniki. |
Powiedzmy, że utworzyłeś, stosując alokację dynamiczną, rekord zawierający dane adresowe:
var
Wsk : PKartaAdresowa;
Rek : TKartaAdresowa;
begin
Wsk := AllocMem(SizeOf(TKartaAdresowa));
Chcesz teraz dane z tego rekordu przepisać do rekordu statycznego, czyli chcesz skopiować dane, na które wskazuje Wsk do zmiennej statycznej Rek (zmienna Wsk jest wskaźnikiem na rekord TKartaAdresowa, a zmienna Rek jest rekordem TKartaAdresowa). Mógłbyś spróbować następującego przypisania:
Rek := Wsk; {źle}
Nie jest to jednak dozwolone, ponieważ Wsk zawiera adres pamięci, a nie rekord TKartaAdresowa. Żeby odwołać się do rekordu, na który wskazuje wskaźnik Wsk, należy użyć operatora ^:
Rek := Wsk^; {dobrze}
Działanie tego operatora można wyjaśnić następująco: „zwróć obiekt wskazywany przez ten wskaźnik, a nie wartość tego wskaźnika”.
Klasy
Klasa jest obiektem składającym się z pól i metod (funkcji i procedur) współdziałających w celu wykonania określonego zadania lub funkcji. W klasach wyróżnić można następujące elementy:
Kategorie widoczności elementów klasy
Konstruktory
Destruktory
Pola
Metody (procedury i funkcje)
Ukryty, specjalny wskaźnik o nazwie Self
Weźmy na początek jakiś prosty przykład klasy - niech będzie nim kontrolka Windows: pole wyboru (check box). Klasa reprezentująca check box musi posiadać następujące pola: opis (wyświetlany na ekranie) oraz stan (zaznaczony albo nie zaznaczony). Klasa ta musi posiadać także metody umożliwiające sprawdzenie oraz przypisanie wartości powyższym polom. Nazwijmy te metody odpowiednio: JakiOpis, CzyZaznaczone, UstawOpis, UstawStan. Stworzenie takiej klasy to jednak nie wszystko - trzeba jeszcze zadeklarować obiekty tej klasy (podobnie jak z rekordami, klasa jest stworzonym przez programistę nowym typem danych. Aby z niego korzystać, trzeba utworzyć zmienne tego typu). W rzeczywistości check box jest trochę bardziej skomplikowany, ale w końcu jest to tylko przykład. Użyjmy teraz tej klasy w programie:
var
Pole1 : TMojCheckBox;
Pole1 : TMojCheckBox;
Pole1 : TMojCheckBox;
begin
Pole1 := TMojCheckBox.Create(ID_POLE1);
Pole2 := TMojCheckBox.Create(ID_POLE2);
Pole3 := TMojCheckBox.Create(ID_POLE3);
Pole1.UstawOpis('Dodatkowy majonez');
Pole1.UstawStan(True);
Pole2.UstawOpis('Dodatkowy ketchup');
Pole2.UstawStan(False);
Pole3.UstawOpis('Dodatkowa musztarda');
Pole3.UstawStan(True);
if Pole1.CzyZaznaczone then IloscMajonezu := IloscMajonezu * 2;
if Pole2.CzyZaznaczone then IloscKetchupu := IloscKetchupu * 2;
...
end;
W tym przykładzie każda zmienna typu TMojCheckBox jest odrębnym obiektem. Każdy obiekt posiada własne pola i działa niezależnie od innych. Są one wszystkie tego samego typu, ale każdy z nich ma własne miejsce w pamięci komputera.
|
Powyższy przykład byłby bardziej czytelny, gdybym zamiast metod JakiOpis, CzyZaznaczone, UstawOpis, UstawStan użył właściwości. Właściwości jednak szczegółowo omówię dopiero w rozdziale 5. |
Anatomia klasy
Klasy, podobnie jak rekordy, posiadają swoje deklaracje. Deklaracje klas umieszcza się zawsze w sekcji type.
Poziomy dostępu do składników klasy
Klasy posiadają 4 poziomy dostępu do swoich składników:
Private
Public
Protected
Published
Poszczególne poziomy dostępu określają, w jakim stopniu i do których składników klasy można się w programie odwoływać. Wiąże się to z ogólną cechą programowania obiektowo zorientowanego: hermetyzacją danych.
|
Hermetyzacja jest to ukrywanie szczegółów implementacji klasy przed światem zewnętrznym (użytkownikiem klasy). |
Ma to swoje analogie z innymi dziedzinami życia. Weźmy na przykład samochód. Dla kierowcy (użytkownika klasy „samochód”) udostępnionych jest tylko kilka elementów sterowniczych: kierownica, pedały, dźwignia zmiany biegów i tablica wskaźników. Elementy regulacyjne silnika są przed nim ukryte po to, żeby nie mógł nic popsuć. Poza tym szczegółowa wiedza o wszystkich detalach i o działaniu wszystkich elementów nie jest mu potrzebna. Aby z powodzeniem korzystać z samochodu, musi wiedzieć jedynie, jak używać kierownicy, pedałów i dźwigni.
Część private klasy zawiera właśnie te wszystkie szczegóły implementacyjne znane tylko twórcy klasy (programiście, który ją tworzył). Pola i metody „prywatne” nie są widziane na zewnątrz klasy (nie można się do nich odwoływać z funkcji i procedur nie będących składnikami klasy) - z jednym wyjątkiem, o którym za chwilę.
Część „publiczna” (public) klasy zawiera natomiast cały „interfejs”, za pomocą którego świat zewnętrzny komunikuje się z klasą. Do pól i metod publicznych można odwoływać się swobodnie z każdego miejsca programu (z innych modułów także, jeżeli tylko umieściło się nazwę modułu zawierającego daną klasę na ich liście uses).
Poziom dostępu protected (elementy definicji na tym poziomie nazywane są elementami chronionymi) wymaga trochę bardziej obszernego wyjaśnienia. Powiedzmy, że chcesz klasę „samochód” odpowiednio zmodyfikować i uzyskać na jej bazie klasę „limuzyna”. Wymaga to wydłużenia (dosłownie) samochodu. Aby go wydłużyć, potrzebna jest wiedza na temat budowy ramy i wału napędowego samochodu. Szczegóły na temat ramy i wału napędowego znalazłyby się właśnie w części protected. Pola i metody chronione dostępne są dla klas będących potomkami klasy podstawowej (w tym przypadku limuzyna jest potomkiem klasy samochód - wchodzę tu w zagadnienie zwane dziedziczeniem, które omówię później). Szczegóły implementacyjne na temat silnika pozostałyby nadal w części prywatnej - przy wydłużaniu samochodu nie muszę ich znać.
Poziom dostępu published używany jest przy pisaniu komponentów (klas będących nowymi komponentami VCL). Elementy klasy umieszczone w tej sekcji znajdą się w Inspektorze Obiektów w czasie tworzenia programów korzystających ze stworzonych w ten sposób komponentów. Szerzej objaśnię to zagadnienie w rozdziale 20.
Object Pascal posiada 4 słowa kluczowe, które odnoszą się do wymienionych czterech poziomów dostępu. Są to słowa private, public, protected i published. Częściom składowym klasy przyporządkowuje się odpowiednie poziomy dostępu w momencie jej deklarowania. Klasę deklaruje się używając słowa kluczowego class:
TPojazd = class
private
BiezacyBieg : Integer;
Uruchomiony : Boolean;
Predkosc : Integer;
procedure UruchomienieSystemuElektrycznego;
procedure UruchomienieSilnika;
protected
procedure SekwencjaStartowa;
public
JestKluczyk : Boolean;
Start : Boolean;
procedure WlaczBieg(Bieg : Integer);
procedure Przyspiesz(Przyspieszenie : Integer);
procedure Hamuj(Wspolczynnik : Integer);
procedure Skrec(Kierunek : Integer);
procedure WylaczSilnik;
end;
Zwróć uwagę na sposób podziału klasy na poszczególne poziomy dostępu. Zastosowanie wszystkich poziomów dostępu nie jest konieczne, nie użyłem tu na przykład słowa kluczowego published. Formalnie nie jest wymagane używanie żadnego z poziomów dostępu, zwykle jednak stosuje się co najmniej dwa z nich: public i private.
Konstruktory
|
Konstruktor jest specjalną metodą używaną przy tworzeniu obiektu danej klasy. |
Konstruktor używany jest do inicjalizacji zmiennych składowych obiektu, do przydzielania pamięci potrzebnej do działania obiektu, lub innych czynności, które muszą być wykonane przy tworzeniu nowego obiektu danej klasy. Klasa TPojazd z ostatniego przykładu nie ma własnego konstruktora. Jeżeli jakaś klasa nie ma własnego konstruktora, można przy jej tworzeniu użyć konstruktora klasy bazowej. Każda klasa w Object Pascalu, jeżeli nie podano inaczej w jej deklaracji, jest potomkiem klasy TObject. Klasa TObject posiada konstruktor o nazwie Create, zostanie on więc automatycznie wywołany, jeżeli klasa - potomek nie posiada własnego konstruktora. Co prawda używanie konstruktora klasy bazowej jest dopuszczalne, jednak dobrym zwyczajem jest wyposażanie każdej nowo stworzonej klasy we własny konstruktor. Nazwa konstruktora może być dowolna, jednak musi on być zadeklarowany za pomocą słowa kluczowego constructor. To słowo właśnie odróżnia deklarację konstruktora od deklaracji pozostałych metod. Spróbujmy więc dodać taką deklarację do klasy TPojazd:
TPojazd = class
private
BiezacyBieg : Integer;
Uruchomiony : Boolean;
Predkosc : Integer;
procedure UruchomienieSystemuElektrycznego;
procedure UruchomienieSilnika;
protected
procedure SekwencjaStartowa;
public
JestKluczyk : Boolean;
Start : Boolean;
procedure WlaczBieg(Bieg : Integer);
procedure Przyspiesz(Przyspieszenie : Integer);
procedure Hamuj(Wspolczynnik : Integer);
procedure Skrec(Kierunek : Integer);
procedure WylaczSilnik;
constructor Create; {konstruktor}
end;
Konstruktor jest specjalnym rodzajem metody. Nie zwraca on żadnych wyników (tak jak funkcja), więc nie posiada w deklaracji żadnego typu wyniku. Próba deklaracji konstruktora zwracającego jakiś wynik skończy się komunikatem błędu wygenerowanym przez kompilator.
W klasie można zadeklarować więcej niż jeden konstruktor. Można to zrobić na dwa sposoby. Pierwszym jest nadanie poszczególnym konstruktorom różnych nazw, na przykład:
TPojazd = class
...
constructor Create;
constructor CreateModel(Model : string);
end;
Innym sposobem na zadeklarowanie wielu konstruktorów w klasie jest użycie przeciążania (które omawiałem w poprzednim rozdziale). Poniżej przedstawiony jest przykład deklaracji dwóch konstruktorów o tych samych nazwach, lecz o różnych parametrach:
TPojazd = class
...
constructor Create; overload;
constructor Create(Wlasciciel : TObject); overload;
end;
Ponieważ przeciążanie metod jest nowością w Delphi 4, prawdopodobnie w programach pisanych w Delphi będziesz częściej spotykał się z pierwszą metodą (konstruktory o różnych nazwach). Tendencja ta pewnie utrzyma się jeszcze jakiś czas mimo, że oba powyższe sposoby deklarowania są teraz równoprawne.
|
Jeżeli planujesz sprzedawać stworzone przez siebie komponenty, używaj deklarowania konstruktorów z użyciem przeciążania. Umożliwi to współpracę Twoich komponentów z C++ Builderem, który nie dopuszcza konstruktorów o różnych nazwach. Warto stosować ten sposób nawet, jeżeli teraz nie przewidujesz takiej ewentualności. |
Jaki jest cel deklarowania wielu konstruktorów? Otóż umożliwia to tworzenie obiektów danej klasy na wiele różnych sposobów. Na przykład, klasa może posiadać jeden konstruktor, który nie wymaga żadnych parametrów i drugi konstruktor, któremu podaje się jeden lub wiele parametrów i który inicjalizuje poszczególne pola określonymi wartościami. Powiedzmy, że stworzyłeś klasę TMojProstokat, która reprezentuje prostokąt na ekranie (bardzo często występujący element przy programowaniu). Klasa ta powinna posiadać kilka konstruktorów. Jednym z nich powinien być konstruktor domyślny, który inicjalizuje wszystkie pola wartością 0. Inny konstruktor powinien umożliwiać ustawianie żądanych początkowych wartości tych pól. Deklaracja takiej klasy mogłaby wyglądać następująco:
TMojProstokat = class
private
Lewy : Integer;
Gorny : Integer;
Prawy : Integer;
Dolny : Integer;
public
function PodajSzerokosc : Integer;
function PodajWysokosc : Integer;
procedure UstawProstokat(L, G, P, D : Integer);
constructor Create;
constructor CreateAndSet(L, G, P, D : Integer);
end;
Definicje zaś poszczególnych konstruktorów mogłyby być następujące:
constructor TMojProstokat.Create;
begin
inherited Create;
Lewy := 0;
Gorny := 0;
Prawy := 0;
Dolny := 0;
end;
constructor TMojProstokat.CreateAndSet(L, G, P, D : Integer);
begin
inherited Create;
Lewy := L;
Gorny := G;
Prawy := P;
Dolny := D;
end;
Pierwszy konstruktor po prostu wstawia do wszystkich pól wartość 0. Drugi natomiast przypisuje odpowiednim polom obiektu wartości swoich parametrów. Nazwy parametrów są lokalne dla tego konstruktora, więc muszą się różnić od nazw pól.
|
Inicjalizowanie pól wartością 0, tak jak to robi konstruktor Create w powyższym przykładzie, nie jest konieczne. Kiedy obiekt (egzemplarz) danej klasy jest tworzony, wszystkie jego pola są zerowane automatycznie. |
Oto przykład użycia zdefiniowanych wyżej konstruktorów :
var
Prostokat1 : TMojProstokat;
Prostokat2 : TMojProstokat;
begin
Prostokat1 := TMojProstokat.Create;
Prostokat2 := TMojProstokat.CreateAndSet(0, 0, 100, 100);
end;
W klasie można deklarować dowolną liczbę konstruktorów, jeżeli tylko stosuje się przy tym różne ich nazwy lub zachowuje się zasady przeciążania metod.
Jeżeli chodzi o powyższy przykład, jest jeszcze jedna rzecz, o której powinieneś wiedzieć. Mianowicie oba obiekty (egzemplarze) klasy TMojProstokat zostały utworzone dynamicznie. Pisałem wcześniej, że zmienne dynamiczne tworzy się w pamięci komputera poprzez wywołanie funkcji GetMem. Klasy są jednak wyjątkiem - są one zawsze dynamiczne. W powyższym przykładzie zatem przy każdorazowym wywołaniu konstruktora przydzielana jest dla obiektu pamięć ze sterty. Ponieważ nie umieściłem tam instrukcji zwalniających przydzieloną pamięć (pamiętasz - pamięć przydzielana dynamicznie nie jest zwalniana automatycznie) oznacza to, że pamięć ta nigdy nie jest zwalniana. Zaradzimy temu w następnym akapicie. Ponieważ wszystkie klasy w Object Pascalu są tworzone dynamicznie, wszystkie obiekty (egzemplarze danej klasy) są wskaźnikami. Zmienne Prostokat1 i Prostokat2 są zatem wskaźnikami na odpowiednie obiekty klasy TMojProstokat.
Destruktory
|
Destruktor jest specjalną metodą wywoływaną automatycznie tuż przed zakończeniem życia obiektu. |
Destruktor można uważać za przeciwieństwo konstruktora. Używany jest on głównie do zwalniania zaalokowanej pamięci, a także wykonuje inne czynności niezbędne przy niszczeniu obiektu. Tak jak w przypadku konstruktora, klasa nie musi posiadać własnego destruktora. Zamiast niego można wywołać destruktor klasy bazowej. Destruktor nie zwraca także żadnej wartości.
Pomimo, że klasa może posiadać więcej niż jeden destruktor, nie jest to jednak praktyka szeroko stosowana. Gdy w danej klasie jest tylko jeden destruktor, powinieneś nazwać go Destroy. Jest to coś więcej niż tradycja. Aby usunąć obiekt z pamięci, wywołuje się metodę Free. Metoda Free wywołuje automatycznie metodę Destroy tuż przed usunięciem obiektu z pamięci. Jest to typowa metoda zwalniania zasobów zajmowanych przez obiekty. Poniżej przedstawiony jest ilustrujący to przykład:
Prostokat1 := TMojProstokat.Create;
...
Prostokat1.Free;
Poniższy kod jest uzupełnioną wersją przykładu z podrozdziału o konstruktorach. Teraz cała przydzielona pamięć zostanie na końcu zwolniona i udostępniona dla systemu (nieistotne części kodu zostały pominięte):
TMojProstokat = class
private
Lewy : Integer;
Gorny : Integer;
Prawy : Integer;
Dolny : Integer;
Tekst : PChar; {nowe pole}
public
function PodajSzerokosc : Integer;
function PodajWysokosc : Integer;
procedure UstawProstokat(L, G, P, D : Integer);
constructor Create;
constructor CreateAndSet(L, G, P, D : Integer);
destructor Destroy; override;
end;
constructor TMojProstokat.Create;
begin
inherited Create;
{przydzielenie pamięci dla łańcucha z zerowym ogranicznikiem}
Tekst := AllocMem(1024);
end;
destructor TMojProstokat.Destroy;
begin
{zwolnienie zajmowanej pamięci}
FreeMem(Tekst);
inherited Destroy;
end;
W zmodyfikowanej wersji klasy TMojProstokat pamięć dla łańcucha z zerowym ogranicznikiem przydzielana jest w konstruktorze, zwalniana jest natomiast w destruktorze.
Przyjrzyjmy się dokładniej deklaracji destruktora klasy TMojProstokat:
destructor Destroy; override;
Słowo override na końcu tej deklaracji mówi kompilatorowi, że jest to destruktor używany zamiast destruktora klasy bazowej.
|
Słowa inherited będziesz zwykle używać w pierwszej instrukcji konstruktora i w ostatniej instrukcji destruktora. |
Pola
Pola są to po prostu zmienne umieszczone w klasie przy jej deklarowaniu. Są one widziane w klasie tak, jak jej zmienne lokalne (tak jak pola w rekordach). Można się do nich odwoływać z poziomu wszystkich metod tej klasy. Na zewnątrz klasy jednak to, czy są one widzialne czy nie, zależy od tego, czy umieszczono je w sekcji private, public czy protected. Pola prywatne i chronione nie są widziane na zewnątrz, pola publiczne są (odwoływać się można do nich stosując notację kwalifikowaną - z podaniem obiektu, do którego pół się odwołujemy). Weźmy na przykład klasę TMojProstokat. Nie posiada ona pól publicznych. Próba kompilacji następującego kodu:
Prostokat1 := TMojProstokat.CreateAndSet(0, 0, 100, 100);
Prostokat1.Lewy := 20;
spowoduje błąd kompilatora Undeclared Identifier: 'Lewy'. Kompilator mówi w ten sposób, że próbujesz odwołać się do pola prywatnego klasy. Gdyby pole Lewy było umieszczone w sekcji public, kod ten skompilowałby się bez problemów.
|
To, co było do tej pory powiedziane o dostępie do prywatnych pól klasy jest prawdą, gdy klasa ta jest zadeklarowana w oddzielnym module. Klasy zadeklarowane w jednym module mają wzajemny swobodny dostęp do swoich pól prywatnych (to jest właśnie ów wyjątek, o którym nieco wcześniej wspominałem). |
Object Pascal kontroluje dostęp do pól prywatnych klasy za pomocą właściwości. Właściwości mogą być tylko odczytywalne, tylko zapisywalne (rzadko) albo równocześnie zapisywalne i odczytywalne. Z daną właściwością mogą być powiązane metody, które wywoływane są w momencie jej odczytywania lub modyfikacji. Metody te - zwane metodami dostępowymi - nie są jednak konieczne, ponieważ właściwości mają bezpośredni dostęp do wszystkich pól prywatnych klasy. Metody te są wywoływane automatycznie przy używaniu odpowiednich właściwości. Szczególnie ważna jest metoda wywoływana przy zapisie do właściwości. Może ona być użyta np. do sprawdzenia poprawności danych wejściowych. Dzięki właściwościom nie potrzeba odwoływać się bezpośrednio do prywatnych pól klasy (choć można to robić).
Każdy utworzony obiekt danej klasy posiada swoje własne dane. Możesz przypisać jakąś wartość polu Lewy obiektu Prostokat1, inną zaś wartość polu Left obiektu Prostokat2:
Prostokat1 := TMojProstokat.CreateAndSet(100, 100, 500, 500);
Prostokat2 := TMojProstokat.CreateAndSet(0, 0, 100, 100);
|
Niektórzy ekstremiści OOP twierdzą, że klasy nie powinny posiadać żadnych pól publicznych - wszelki dostęp do pól obiektu powinien odbywać się za pośrednictwem metod dostępowych. Druga skrajna grupa twierdzi, że wszystkie pola klasy powinny być publiczne. Myślę, że prawda jak zwykle leży gdzieś pośrodku. Niektóre nie wymagające szczególnej ochrony pola mogą pozostać publiczne (łatwiej i szybciej można się do nich dzięki temu dostać). Inne pola, ważne z punktu widzenia funkcjonowania klasy, muszą pozostać prywatne. Jeżeli nie jesteś pewny, czy dane pole uczynić publicznym czy prywatnym , lepiej jednak na wszelki wypadek zadeklarować ją w sekcji private. |
Utworzono w ten sposób dwa obiekty klasy TMojProstokat. Mimo, że ich struktura jest identyczna, istnieją w pamięci komputera jako dwa zupełnie niezależne i oddzielne obiekty. Pole Lewy obiektu Prostokat1 ma wartość 100, natomiast pole Lewy obiektu Prostokat2 ma wartość 0. To jest tak jak z seryjnie produkowanymi samochodami: wszystkie zeszły z tej samej taśmy, jednak jeden od drugiego różni się kolorem, wyposażeniem itd.
Metody
Metody są to funkcje i procedury będące składnikami klasy. Są one lokalne w danej klasie i nie istnieją poza nią. Mogą być one wywoływane z wewnątrz klasy lub poprzez obiekt danej klasy. Metody mają dostęp do wszystkich pól: prywatnych, publicznych i chronionych (protected). Tak jak pola, metody mogą być także prywatne, publiczne i chronione. Przy projektowaniu klasy należy się w tym względzie kierować następującymi zasadami:
Metody publiczne, podobnie jak właściwości, tworzą interfejs użytkownika. To właśnie poprzez nie następuje interakcja klasy z całym światem zewnętrznym. Powiedzmy na przykład, że tworzysz klasę zapisującą i odtwarzającą pliki dźwiękowe. Do metod publicznych należałoby w tym przypadku zakwalifikować takie metody jak: Otworz, Nagrywaj, Zapisz, Przewin itd.
Metody prywatne są to metody, które wykonują całą „czarną robotę”. Bezpośrednie ich wywoływanie przez użytkownika nie jest konieczne, a czasem wręcz niepożądane. Zwykle przy tworzeniu obiektu trzeba wykonać zespół jakichś czynności. Może to być czasem wiele linii kodu. Żeby nie zagracać konstruktora klasy, można te wszystkie czynności zebrać w jedną prywatną metodę Init i umieścić jej wywołanie właśnie w konstruktorze. Tej metody użytkownik klasy nie będzie nigdy wywoływał bezpośrednio. Nawet gdyby to zrobił, mógłby przy tym narobić dużych szkód.
Metody chronione nie mogą być wywoływane z zewnątrz, a tylko z klas - potomków danej klasy. Zagadnienie to omówię szczegółowo później w podrozdziale zatytułowanym „Dziedziczenie”.
Metoda danej klasy może być wywoływana w kontekście konkretnego jej egzemplarza (przykładem może być metoda PodajSzerokosc klasy TMojProstokat)lub w kontekście klasy jako całości. Metody tej drugiej grupy - zwane „metodami klasowymi” (ang. class methods) - deklarowane są jako class function lub class procedure; nie mają one dostępu do pól i „zwykłych” metod danej klasy (później wyjaśnię, skąd bierze się to ograniczenie). Metody-klasy są rzadko używane, nie będę ich więc szczegółowo omawiał.
Wskaźnik Self
|
Wszystkie klasy (a tym samym - wszystkie obiekty) posiadają ukryte pole o nazwie Self. Jest to wskaźnik do tego właśnie obiektu (czyli - obiekt zawiera wskaźnik na samego siebie). |
Definicja ta wymaga szerszego wyjaśnienia. Po pierwsze, spójrz, jak deklaracja klasy TMojProstokat wyglądałaby, gdyby pole Self nie było ukryte:
TMojProstokat = class
private
Self : TMojProstokat;
Lewy : Integer;
Gorny : Integer;
Prawy : Integer;
Dolny : Integer;
Tekst : PChar; {nowe pole}
public
function PodajSzerokosc : Integer;
function PodajWysokosc : Integer;
procedure UstawProstokat(L, G, P, D : Integer);
constructor Create;
constructor CreateAndSet(L, G, P, D : Integer);
destructor Destroy; override;
end;
Tak deklarację klasy TMojProstokat widzi kompilator. Kiedy obiekt tej klasy jest tworzony, wskaźnik Self jest inicjalizowany automatycznie adresem tego obiektu w pamięci:
Prostokat1 := TMojProstokat.CreateAndSet(0, 0, 100, 100);
{teraz 'Prostokat1' i 'Prostokat1.Self' mają te same wartości}
{ponieważ obie zmienne wskazują na ten sam obiekt w pamięci}
Możesz zapytać teraz: „Jakie właściwie znaczenie ma wskaźnik Self?” Pamiętaj o tym, że każdy obiekt danej klasy otrzymuje swoją własną kopię pól. Ale wszystkie obiekty tej klasy dzielą ten sam zestaw metod (nie ma potrzeby powielania tego samego kodu dla wszystkich obiektów). Skąd jednak kompilator ma wiedzieć, do którego konkretnie obiektu odnosi się wywołanie danej metody? Przy każdym wywołaniu jakiejś metody otrzymuje ona informację (właśnie wskaźnik Self), na którym obiekcie ma być wykonana dana akcja. Aby zilustrować ten problem, zdefiniujmy metodę PodajSzerokosc klasy TMojProstokat:
function TMojProstokat.PodajSzerokosc : Integer;
begin
Result := Prawa - Lewa;
end;
Tak definicja ta wygląda dla mnie i dla Ciebie. Dla kompilatora jednak wygląda ona następująco:
function TMojProstokat.PodajSzerokosc : Integer;
begin
Result := Self.Prawa - Self.Lewa;
end;
Nie jest to co prawda w 100% zgodne z prawdą z technicznego punktu widzenia, jednak na nasze potrzeby można przyjąć, że tak właśnie jest. Widać tutaj wskaźnik Self w akcji. Nie musisz dokładnie wiedzieć, jak to się dzieje, jednak musisz być świadom, że coś takiego istnieje.
|
Nigdy nie modyfikuj „ręcznie” wskaźnika Self. Możesz używać go do przekazywania adresu Twojego obiektu do innych metod, lub jako parametr przy tworzeniu nowych obiektów, ale nigdy nie zmieniaj jego wartości. Traktuj wskaźnik Self jak stałą. |
Pomimo że wskaźnik Self działa „w tle”, pozostaje ciągle zmienną, do której można się odwoływać w obrębie klasy. Weźmy na przykład bibliotekę VCL. Z reguły, komponenty tworzy się w VCL umieszczając je przy pomocy myszy na formularzu. Delphi automatycznie tworzy wtedy wskaźnik do danego komponentu i zajmuje się innymi technicznymi szczegółami, dzięki czemu nie musisz się nimi przejmować. Czasami jednak trzeba utworzyć komponent w czasie działania programu. VCL wymaga wówczas (jak powinno robić każde dobre środowisko) informacji na temat, który istniejący już obiekt ma być rodzicem tego nowo tworzonego. Na przykład, chcesz w czasie działania programu, po naciśnięciu przycisku na formularzu utworzyć nowy przycisk. Musisz powiadomić VCL, który komponent ma być rodzicem tego nowego przycisku:
procedure TForm1.Button1Click(Sender: TObject);
var
Przycisk : TButton;
begin
Przycisk := TButton.Create(Self);
Przycisk.Parent := Self;
Przycisk.Left := 20;
Przycisk.Top := 20;
Przycisk.Caption := 'Naciśnij mnie';
end;
W powyższym przykładzie, wskaźnik Self użyty jest przy wywołaniu konstruktora (ustawia to właściwość Owner tego przycisku - więcej szczegółów na ten temat w rozdziale 7. „Komponenty VCL”), oraz w przypisaniu do właściwości Parent nowo utworzonego przycisku. Najczęściej wskaźnik Self wykorzystuje się w aplikacjach Delphi w ten właśnie sposób.
|
Wspominałem wcześniej o tym, że metody-klasy nie mają dostępu do metod danej klasy. Powodem jest to, że metody-klasy nie posiadają ukrytego wskaźnika Self, w odróżnieniu od zwykłych metod. Bez tego wskaźnika metoda nie może odwoływać się do pól klasy. |
Przykład klasy
Myślę, że dobrym pomysłem będzie zaprezentowanie na tym etapie jakiegoś dość rozbudowanego przykładu klasy. Listing 3.1 pokazuje kod modułu zawierającego klasę o nazwie Samolot. Klasa ta mogłaby być użyta przez kontrolera lotów na lotnisku. Umożliwia ona kontrolę nad samolotem poprzez wydawanie mu komend. Mogą to być komendy startu, lądowania, zmiany kursu, wysokości lub prędkości. Przejrzyj teraz ten listing, a potem objaśnię dokładnie jego zawartość.
Listing 3.1. SAMOLOTU.PAS
unit SamolotU;
interface
uses
SysUtils;
const
{Typy samolotów}
Odrzutowy = 0;
Turbosmiglowy = 1;
Awionetka = 2;
{Stany, w jakich może się znajdować samolot}
StStart = 0;
StPrzelot = 1;
StLadowanie = 2;
StNaPasie = 3;
{Typy komunikatów}
PolZmien = 0;
PolStartuj = 1;
PolLaduj = 2;
PolRaportuj = 3;
type
TSamolot = class
private
Nazwa : string;
Predkosc : Integer;
Wysokosc : Integer;
Kierunek : Integer;
Stan : Integer;
Rodzaj : Integer;
Pulap : Integer;
protected
procedure Startuj(Kier : Integer); virtual;
procedure Laduj; virtual;
public
constructor Create(Naz : string; Rodz : Integer = Odrzutowy);
function WyslijPolecenie(Pol : Integer; var Odpowiedz : string;
Pred : Integer; Kier : Integer; Wys : Integer) : Boolean;
function JakiStan(var StanString : string) : Integer; overload;
virtual;
function JakiStan : Integer; overload;
function JakaPredkosc : Integer;
function JakiKierunek : Integer;
function JakaWysokosc : Integer;
function JakaNazwa : string;
end;
implementation
constructor TSamolot.Create(Naz : string; Rodz : Integer);
begin
inherited Create;
Nazwa := Naz;
Rodzaj := Rodz;
Stan := StNaPasie;
case Rodzaj of
Odrzutowy : Pulap := 35000;
Turbosmiglowy : Pulap := 25000;
Awionetka : Pulap := 8000;
end;
end;
procedure TSamolot.Startuj(Kier : Integer);
begin
Kierunek := Kier;
Stan := StStart;
end;
procedure TSamolot.Laduj;
begin
Predkosc := 0;
Kierunek := 0;
Wysokosc := 0;
Stan := StNaPasie;
end;
function TSamolot.WyslijPolecenie(Pol : Integer; var Odpowiedz : string;
Pred : Integer; Kier : Integer; Wys : Integer) : Boolean;
begin
Result := True;
{Podjęcie odpowiedniej akcji w zależności od otrzymanego polecenia}
case Pol of
PolStartuj :
{Nie można wystartować będąc już w powietrzu}
if Stan <> StNaPasie then begin
Odpowiedz := Nazwa + ': Jestem już w powietrzu!';
Result := False;
end else
Startuj(Kier);
PolZmien :
begin
{Sprawdzenie, czy polecenie jest poprawne}
if Pred > 500 then
Odpowiedz := 'Błędne polecenie: Prędkość nie może przekraczać
* 500.';
if Kier > 360 then
Odpowiedz := 'Błędne polecenie: Kierunek nie może przekraczać
* 360.';
if Wys < 100 then
Odpowiedz := Nazwa + ': Rozbiłbym się!';
if Wys > Pulap then
Odpowiedz := Nazwa + ': Nie mogę lecieć tak wysoko.';
if (Pred = 0) and (Kier = 0) and (Wys = 0) then
Odpowiedz := Nazwa + ': Hę?';
if Odpowiedz <> '' then begin
Result := False;
Exit;
end;
{Nie można zmienić stanu będąc na ziemi}
if Stan = StNaPasie then begin
Odpowiedz := Nazwa + ': Jestem na pasie startowym.';
Result := False;
end else begin
Predkosc := Pred;
Kierunek := Kier;
Wysokosc := Wys;
Stan := StPrzelot;
end;
end;
PolLaduj :
{Nie można wylądować będąc na ziemi}
if Stan = StNaPasie then begin
Odpowiedz := Nazwa + ': Jestem na pasie startowym.';
Result := False;
end else
Laduj;
PolRaportuj :
begin
JakiStan(Odpowiedz);
Exit;
end;
end;
{Jeżeli wszystko poszło dobrze}
if Result then
Odpowiedz := Nazwa + ': Roger.';
end;
function TSamolot.JakiStan(var StanString : string) : Integer;
begin
StanString := Format('%s, Wysokosc: %d, Kierunek: %d, ' +
'Predkosc: %d', [Nazwa, Wysokosc, Kierunek, Predkosc]);
Result := Stan;
end;
function TSamolot.JakiStan : Integer;
begin
Result := Stan;
end;
function TSamolot.JakaPredkosc : Integer;
begin
Result := Predkosc;
end;
function TSamolot.JakiKierunek : Integer;
begin
Result := Kierunek;
end;
function TSamolot.JakaWysokosc : Integer;
begin
Result := Wysokosc;
end;
function TSamolot.JakaNazwa : string;
begin
Result := Nazwa;
end;
end.
Spójrz na deklarację klasy TSamolot w sekcji interface. Klasa ta posiada przeciążoną funkcję o nazwie JakiStan. Gdy ta funkcja jest wywoływana z parametrem typu string, zwraca tekst opisujący stan samolotu oraz wartość liczbową oznaczającą ten stan. Gdy jest wywoływana bez żadnych parametrów, zwraca jedynie wartość liczbową. Zauważ także, że jedyną metodą odwołania się po pól prywatnych klasy jest użycie jej publicznych metod. Na przykład, chcąc zmienić prędkość, wysokość lub kierunek samolotu należy wysłać mu odpowiednie polecenie. Tak jak w świecie rzeczywistym: kontroler lotów nie może sam zmienić kursu samolotu, może jedynie wysłać takie polecenie pilotowi.
|
W klasie tej aż prosi się o zastosowanie właściwości. Ponieważ jednak nie znasz jeszcze tego zagadnienia, zostawmy definicję klasy taką, jaka jest. |
Skupmy się teraz na definicji klasy TSamolot w sekcji implementation. Widać tu, że konstruktor przypisuje początkowe wartości poszczególnym cechom samolotu. Funkcją, która jest najczęściej wykorzystywana i wykonuje najwięcej czynności jest WyslijPolecenie. W zależności od otrzymanego polecenia podejmowana jest tu odpowiednia akcja (instrukcja case). Funkcje Startuj i Laduj są zadeklarowane w sekcji protected, więc nie mogą być wywoływane bezpośrednio, są wywoływane poprzez funkcję WyslijPolecenie. Kontroler lotu nie może przecież fizycznie wylądować i wystartować samolotem, może jedynie wysłać mu takie polecenie.
Jest w deklaracji klasy TSamolot coś, czego dotychczas nie omawiałem. Chodzi o słowo virtual. Oznacza ono, że dana funkcja lub procedura jest metodą wirtualną.
|
Metoda wirtualna jest to metoda, która jest automatycznie wywoływana w klasach dziedziczących, jeżeli klasy te posiadają metodę o tej nazwie. |
Sprawa ta stanie się bardziej jasna po lekturze następnego podrozdziału.
Na dyskietce dołączonej do tej książki znajduje się przykładowy program o nazwie Airport, wykorzystujący klasę TSamolot. W programie tym najpierw zakładana jest w pamięci tablica obiektów klasy TSamolot, a następnie tworzone są 3 obiekty tej klasy. Po uruchomieniu programu można wydawać komendy każdemu z tych samolotów ustawiając najpierw parametry polecenia, i klikając na przycisku Wykonaj. Przyciśnięcie przycisku spowoduje wywołanie funkcji WyslijPolecenie. Samolot, do którego wysłano polecenie, odpowiada na nie. Odpowiedź ta jest wyświetlana w komponencie Memo. Na rysunku 3.1 widoczne jest okno główne programu.
Rysunek 3.1. Działający program Airport |
|
Dziedziczenie
Jedną z najważniejszych cech klas w Object Pascalu jest to, że mogą one być rozbudowywane poprzez mechanizm dziedziczenia.
|
Dziedziczenie jest to budowanie nowych klas na bazie już istniejących. |
|
Klasa, z której dziedziczą nowe klasy, nazywa się klasą bazową albo przodkiem, natomiast klasa, która dziedziczy cechy klasy bazowej, nazywa się klasą pochodną. |
Jako przykład rozważmy klasę TSamolot. Jak wiesz, różnice między samolotami cywilnymi i wojskowymi są dość znaczne. Można przyjąć, że samolot wojskowy posiada wszystkie cechy samolotu cywilnego plus kilka dodatkowych, związanych np. z przenoszoną bronią, wykonywanymi misjami itp. Można więc bazując na klasie TSamolot utworzyć (poprzez dziedziczenie) nową klasę TSamolotWojskowy:
TSamolotWojskowy = class (TSamolot)
private
Misja : TMisja;
constructor Create(NazwaP : string; TypP : Integer);
function JakiStatus(var StanString : string) : Integer; override;
protected
procedure Startuj(KierunekP : Integer); override;
procedure Laduj; override;
procedure Atakuj; virtual;
procedure NowaMisja; virtual;
end;
Klasa TSamolotWojskowy posiada wszystkie elementy klasy TSamolot, z dodatkiem kilku nowych pól i metod. Zwróć uwagę na pierwszą linię deklaracji klasy. W nawiasach po słowie class umieszcza się nazwę przodka, z którego dziedziczy dana klasa (w tym przypadku przodkiem klasy TSamolotWojskowy jest klasa TSamolot).
|
Kiedy klasa dziedziczy od swojego przodka, przejmuje wszystkie jego pola i metody. Zwykle deklaruje się wówczas także nowe elementy klasy (zgodnie z ideą dziedziczenia). |
Zauważyłeś zapewne, że w sekcji private klasy TSamolotWojskowy występuje obiekt klasy TMisja. Zawiera on w sobie wszystkie elementy misji do wykonania: cel, punkty nawigacyjne, kursy, itp. Jest to przykład zawierania w jednej klasie obiektu innej klasy (jest to często spotykane w Delphi).
Zastępowanie metod
Chcę na chwilę jeszcze powrócić do metod wirtualnych. Procedura Startuj (patrz listing 3.1) jest wirtualną metodą klasy TSamolot. Procedura ta jest wywoływana w metodzie WyslijPolecenie w odpowiedzi na komendę PolStartuj. Gdyby w klasie TSamolotWojskowy nie zdefiniowano jej własnej metody Startuj, wywoływana byłaby metoda Startuj klasy TSamolot. Ponieważ jednak klasa TSamolotWojskowy posiada własną metodę Startuj, to właśnie ta metoda będzie wywoływana.
|
Definiowanie metod klasy bazowej w klasie pochodnej nazywa się zastępowaniem (ang. overriding) metod. |
Żeby mechanizm ten zadziałał, deklaracje metod klasy bazowej i pochodnej muszą być dokładnie takie same. Nazwy metod, typy wyników i parametry muszą dokładnie sobie odpowiadać. Dodatkowo, deklaracja takiej metody w klasie pochodnej musi być uzupełniona słowem override.
Metody klasy bazowej zastępuje się w celu ich zmiany lub rozszerzenia. Weźmy na przykład metodę Startuj. Gdybyś chciał całkowicie zastąpić starą metodę nową, w definicji nowej metody wpisałbyś po prostu odpowiedni kod:
|
Object Pascal posiada także tzw. metody dynamiczne. Można je w zasadzie utożsamiać z metodami wirtualnymi. Różnica między nimi polega na sposobie przechowywania wskaźników na te metody w tablicach VMT (Virtual Method Table) klasy. Nie musisz się w tej chwili tym przejmować. Chciałem Cię jedynie powiadomić o ich istnieniu, gdybyś natknął się na nie w którymś z licznych programów przykładowych VCL. |
procedure TSamolotWojskowy.Startuj(Kier : Integer);
begin
{nowy kod}
...
end;
Gdyby jednak Twoją intencją było jedynie rozszerzenie funkcjonalności starej metody, w definicji nowej metody oprócz nowego kodu umieściłbyś wywołanie starej („odziedziczonej”) metody. Robi się to stosując słowo inherited, na przykład:
procedure TSamolotWojskowy.Startuj(Kier : Integer);
begin
{najpierw wywołanie metody Startuj klasy TSamolot}
inherited Startuj(Kier);
{nowy kod}
...
end;
Starą metodę można wywoływać z dowolnego miejsca nowej. Zwróć uwagę, że metoda Startuj klasy TSamolot jest zadeklarowana w sekcji protected. Gdyby była w sekcji private, skompilowanie powyższego przykładu nie byłoby możliwe. Nawet obiekty klasy pochodnej nie mają dostępu do metod prywatnych klasy bazowej. Przypominam, że deklarując daną metodę w sekcji protected umożliwiamy dostęp do niej z klas pochodnych, podczas gdy dla świata zewnętrznego nadal pozostaje ona niewidoczna.
|
Istnieje jeden wyjątek od tej zasady. Jeżeli klasa bazowa i pochodna są zadeklarowane w jednym module, także pola i metody prywatne klasy bazowej są widoczne w klasie pochodnej. |
Aby zapewnić poprawną inicjalizację obiektu danej klasy, wywołując konstruktor tej klasy należy także wywołać konstruktor jej klasy bazowej (jeżeli taka istnieje). Robi się to także z użyciem słowa kluczowego inherited:
constructor TSamolot.Create(Naz : string; Rodz : Integer);
begin
inherited Create;
Nazwa := Naz;
Rodzaj := Rodz;
Stan := StNaPasie;
case Rodzaj of
Odrzutowy : Pulap := 35000;
Turbosmiglowy : Pulap := 25000;
Awionetka : Pulap := 8000;
end;
end;
„Chwileczkę!” - możesz powiedzieć - „przecież klasa TSamolot nie posiada żadnej klasy bazowej!”. W rzeczywistości nie jest to prawda - jeżeli w deklaracji danej klasy nie podano żadnej klasy bazowej, jej klasą bazową staje się automatycznie klasa TObject. Pamiętaj, żeby zawsze w konstruktorze danej klasy wywoływać konstruktor jej klasy bazowej.
Rysunek 3.2 przedstawia przykład hierarchii klas.
Rysunek 3.2. Przykład dziedziczenia klas |
|
Widać na tym rysunku, że samolot typu F16 jest potomkiem klasy Myśliwiec. W ostatecznym rozrachunku F16 dziedziczy z klasy Samolot, ponieważ jest ona klasą bazową wszystkich rodzajów samolotów.
Słowa kluczowe: is i as
W Object Pascalu istnieją dwa operatory odnoszące się wyłącznie do klas. Operatora is używa się do sprawdzenia, czy dany obiekt należy do określonej klasy (lub jej klasy pochodnej). Powiedzmy, że istnieje obiekt (samolot) o nazwie F117A. Może to być obiekt klasy TSamolot, klasy TSamolotWojskowy lub jeszcze innej. Aby sprawdzić, czy obiekt należy do danej klasy, należy użyć właśnie operatora is:
if PZL57 is TSamolotWojskowy then
Atakuj;
Operator ten zwraca wartość typu Boolean - True, jeżeli dany obiekt należy do określonej klasy (lub jej klasy pochodnej) i False w przeciwnym przypadku. Na przykład, ponieważ TSamolotWojskowy jest klasą pochodną klasy TSamolot, następujące wyrażenie jest prawdziwe:
if F117A is TSamolot then
JakasProcedura;
Częściej od is używanym operatorem jest operator as. Jest on używany do konwersji (rzutowania typu) wskaźnika na jakąś klasę do wskaźnika na określoną klasę, np.:
|
Ponieważ wszystkie klasy są potomkami klasy TObject, następujące wyrażenie będzie zawsze prawdziwe: if JakakolwiekKlasa is TObject then JakasProcedura; |
with Samolot as TSamolotWojskowy do
Atakuj;
Operator as używany jest zwykle w połączeniu z operatorem with. W powyższym przykładzie obiekt Samolot może być obiektem klasy TSamolot, TSamolotWojskowy, albo żadnej z nich. Operator as mówi kompilatorowi, aby potraktował on obiekt Samolot tak, jakby był on obiektem klasy TSamolotWojskowy. Jeżeli jednak obiekt Samolot nie jest klasy TSamolotWojskowy lub którejś z jej przodków, konwersja ta się nie powiedzie (wystąpi wyjątek - przyp. red.) i procedura Atakuj nie zostanie wywołana. Jeżeli Samolot jest wskaźnikiem na obiekt klasy TSamolotWojskowy, rzutowanie typu się powiedzie i procedura Atakuj zostanie wywołana.
Podsumowanie
W rozdziale tym zapoznałeś się z klasami w Object Pascalu. Dobrze zaprojektowane klasy są łatwe w użyciu i potrafią zaoszczędzić wielu godzin pracy. Mogą one sprawić (zwłaszcza te własne), że programowanie staje się niezłą zabawą.
Treść trzech pierwszych rozdziałów tej książki jest bardzo ważna. Daje ona bowiem wiedzę teoretyczną niezbędną do opanowania reszty materiału. Jeżeli jednak czytając te rozdziały nie zrozumiałeś wszystkiego, nie przejmuj się. Podczas dalszej lektury poznasz różne zastosowania dotychczas zaprezentowanych zagadnień, które pomogą Ci w ich zrozumieniu. Nie spiesz się przy tym. Próbuj modyfikować przykłady i wymyślać nowe. Jak już wspominałem wcześniej, praktyka jest najlepszym nauczycielem.
Warsztat
Warsztat składa się z pytań kontrolnych oraz ćwiczeń utrwalających i pogłębiających zdobytą wiedzę. W razie trudności lub wątpliwości, odpowiedzi do tych pytań zamieszczone są w Dodatku A „Quiz - odpowiedzi”.
Pytania i odpowiedzi
Jak mogę uczynić metodę niewidoczną dla świata zewnętrznego z wyjątkiem klasy dziedziczącej?
Umieść ją w sekcji protected. Metoda ta będzie wtedy niewidoczna dla użytkowników klasy, jednak będzie można się do niej odwoływać z klas dziedziczących.
Co oznacza określenie „hermetyzacja”?
Hermetyzacja jest to ukrywanie szczegółów implementacji klasy przed użytkownikami. Klasa może posiadać dziesiątki pól i metod prywatnych, o których użytkownik wcale nie musi wiedzieć - i tylko kilka publicznych, do których może się odwoływać.
Co to jest obiekt?
Obiektem może być właściwie dowolny blok kodu, który można uznać za oddzielną jednostkę w programie. W Object Pascalu oznacza to przede wszystkim klasę. W Delphi obiektami są także komponenty VCL oraz kontrolki ActiveX.
Czy klasa może posiadać więcej niż jeden konstruktor?
Tak. Może mieć ich tyle, ile potrzebuje.
Czy wszystkie obiekty VCL są wskaźnikami?
Tak. Ponieważ pamięć dla wszystkich klas w Object Pascalu (a tym samym obiektów VCL) jest dynamicznie przydzielana ze sterty, wszystkie one są wskaźnikami.
Mam problemy ze zrozumieniem wskaźników. Czy tylko ja je mam?
Nie! Wskaźniki należą do najbardziej zaawansowanych i skomplikowanych aspektów Object Pascala. Gdy nabierzesz trochę praktyki w programowaniu, wskaźniki nie będą miały przed Tobą żadnych tajemnic.
Quiz
Jak usunąć ze zbioru wszystkie wartości?
W jakim celu deklaruje się pola i metody jako prywatne?
Jak umożliwić użytkownikom czytanie i zapisywanie do pól prywatnych klasy (pozostawiając je jako prywatne)?
Kiedy wywoływany jest destruktor klasy?
Co oznacza zastępowanie metody klasy bazowej?
Jak można zastąpić metodę klasy bazowej i jednocześnie skorzystać z niej w nowej metodzie?
Jaki operator jest używany do odwołania się do danej, na którą wskazuje wskaźnik?
Czy klasa może zawierać jako swoje pola obiekty innych klas?
Jakie słowo używane jest do nadania wskaźnikowi wartości nieokreślonej?
W jakim celu używa się operatora as?
Ćwiczenia
Stwórz klasę, która pobiera wzrost człowieka w centymetrach i przelicza go na cale (wskazówka: 1 cal = 25,4 mm).
Stwórz nową klasę poprzez dziedziczenie z klasy z ćwiczenia 1, która zwraca wzrost także w metrach i milimetrach.
Wynika to stąd, iż obiekty w Object Pascalu reprezentowane są przez wskaźniki - innymi słowy, wewnętrzną reprezentacją zmiennej obiektowej jest wskaźnik na odnośny obiekt, nie zaś sama zmienna, jak to miało miejsce chociażby w Turbo Pascalu (przyp. red.)
W wersji 5.5 Turbo Pascala - pierwszej wersji, w której pojawiły się elementy OOP - była to jedyna możliwość. Wersja 6.0 wprowadziła kwalifikatory public i private. Właściwości i metody dostępowe są natomiast nowością Object Pascala - klasy Turbo Pascala posiadały jedynie pola i metody. (przyp. red.)
Dokładniej - w ramach metody klasowej również istnieje identyfikator Self, ma on jednak zupełnie inne znaczenie niż w „zwykłej” metodzie (przyp. red.)
128 Część I
Rozdzia³ 3. ♦ Klasy i programowanie zorientowane obiektowo 127
128 C:\Dokumenty\Roboczy\Delphi 4 dla kazdego\03.doc
C:\Dokumenty\Roboczy\Delphi 4 dla kazdego\03.doc 127