Professional Documents
Culture Documents
Notatka
Ilekroć wspominamy w niniejszym rozdziale o języku C, mamy na myśli elementy wspólne dla C i C++. Gdy
mowa jest o elementach specyficznych dla C++ zaznaczamy to wyraźnie.
Komentarze
Komentarz jest najprostszym elementem języka programowania — stanowi swobodny tekst mający znaczenie
jedynie dla czytelności programu; przez kompilator jest on całkowicie ignorowany. Object Pascal dopuszcza trzy
rodzaje ograniczników komentarza:
• nawiasy klamrowe { .. }, znane z Turbo Pascala,
• ograniczniki typu „nawias-gwiazdka” (* ... *), również występujące w Turbo Pascalu,
• podwójny ukośnik // (ang. double slash), zapożyczony z języka C++.
Oto przykłady poprawnych komentarzy w Object Pascalu:
{
to jest komentarz języka Object Pascal,
podstawy Delphi 6
}
Za koniec komentarza rozpoczynającego się od podwójnego ukośnika przyjmuje się koniec linii.
Ostrzeżenie
Komentarze tej samej postaci nie mogą być zagnieżdżane, gdyż jest to sprzeczne z regułami składniowymi
języka. Na przykład w poniższym przykładzie
początkiem komentarza jest oczywiście pierwszy z nawiasów otwierających, lecz końcem — pierwszy, nie
drugi nawias zamykający. Nie ma jednak żadnych przeszkód, aby zagnieżdżać komentarze różnych typów, na
przykład:
Nie jest to może żadna rewelacja językowa, lecz z pewnością drobne ułatwienie życia programistom, którzy
oprócz Object Pascala używają również języków wcześniej wymienionych, w których użycie wspomnianych
nawiasów jest obowiązkowe.
Przeciążanie
W Delphi 4 wprowadzono mechanizm przeciążania (overloading) procedur i funkcji, umożliwiający
zdefiniowanie całej rodziny procedur (funkcji), posiadających tę samą nazwę, lecz różniących się postacią listy
parametrów wywołania, na przykład:
function Divide(X, Y: Real): Real; overload;
begin
Result := X/Y;
end;
Informacją o przeciążeniu procedury funkcji jest klauzula overload (jak w powyższym przykładzie).
Kompilator, analizując postać wywołania przeciążonej procedury (funkcji), automatycznie wybierze właściwy
jej egzemplarz (zwany często jej aspektem), aby jednak zadanie to było wykonalne, poszczególne aspekty
faktycznie muszą różnić się od siebie. Nie jest tak chociażby w poniższym przykładzie:
procedure Cap(S: string); overload;
...
Wywołanie
Cap(S);
pasuje bowiem do obydwu aspektów — kompilator uzna drugi z nich za próbę redefinicji pierwszego i
zasygnalizuje błąd.
Przeciążanie procedur i funkcji jest chyba najbardziej oczekiwaną nowością Object Pascala od czasu Delphi 1 i,
jakkolwiek bardzo pożądane i użyteczne, stanowi jedno z odstępstw od rygorystycznej kontroli typów danych
(tak charakterystycznej dla początków Pascala). Mamy wszak do czynienia z różnymi procedurami (funkcjami),
kryjącymi się pod tą samą nazwą — przypomina to identycznie nazwane procedury (funkcje) rezydujące w
różnych modułach. Należy więc korzystać z przeciążania rozsądnie, a w żadnym wypadku nie należy go
nadużywać.
Zagadnieniem podobnym do przeciążania procedur i funkcji jest przeciążanie metod, którym zajmiemy się w
dalszej części niniejszego rozdziału.
Domyślne parametry
To kolejna nowość wprowadzona w Delphi 4, umożliwiająca uproszczenie wywołań procedur i funkcji poprzez
pominięcie jednego lub więcej końcowych parametrów listy. W treści procedury (funkcji) parametry te posiadać
będą wartości domyślne, ustalone w jej definicji. Oto przykład deklaracji procedury z jednym parametrem
domyślnym:
procedure HasDefVal( S: String; I: Integer = 0);
Parametry z wartościami domyślnymi muszą wystąpić w końcowej części listy — nie mogą być „przemieszane”
z pozostałymi parametrami, tak więc poniższa deklaracja
Procedure NoProperDefs( X: Integer = 1; Y : Real);
Parametry z deklarowaną wartością domyślną nie mogą być przekazywane przez referencję (var), lecz jedynie
przez wartość lub przez stałą (const); wiąże się to z oczywistym wymogiem, aby parametr aktualny wywołania
był wyrażeniem o dającej się ustalić wartości. Wymóg ten narzuca również ograniczenie na typ parametru,
któremu przypisuje się wartość domyślną — nie mogą w tej roli wystąpić rekordy, zmienne wariantowe, pliki,
tablice i obiekty. Ograniczeniu podlega także sama wartość domyślna przypisywana parametrowi — może ona
być wyrażeniem typu porządkowego, wskaźnikowego lub zbiorowego.
Pewnego komentarza wymaga użycie parametrów domyślnych w połączeniu z przeciążaniem procedur i funkcji.
Należy uważać, aby nie uniemożliwić kompilatorowi jednoznacznego zidentyfikowania właściwego aspektu
procedury (funkcji) przeciążanej — rozróżnienie takie nie jest możliwe chociażby w poniższym przykładzie:
procedure Confused(I: Integer); overload;
...
var
X: Integer;
begin
...
Confused(X); // Ta instrukcja spowoduje błąd kompilacji
wyświetlającą komunikat w środkowej linii okna, chcielibyśmy wzbogacić w możliwość jawnego wskazania
linii (poprzez drugi parametr). Nawet jeżeli przyjmiemy, iż podanie 0 jako numeru linii oznaczać będzie
tradycyjne wyświetlanie w środkowej linii, to i tak nie zwolni nas to z modyfikacji wszystkich wywołań
procedury MyMessage() w kodzie programu. Ściślej — byłoby tak, gdyby nie mechanizm domyślnych
parametrów, bowiem począwszy od Delphi 4 możemy dopuścić wywołanie procedury MyMessage() z jednym
parametrem, przyjmując w takiej sytuacji, iż opuszczony, domyślny drugi parametr ma wartość 0:
Procedure MyMessage ( Msg : String; Line: byte = 0 );
Wówczas wywołanie
MyMessage('Hello', 1)
Zmienne
W języku C i w Javie możliwe jest deklarowanie zmiennych dopiero w momencie, gdy faktycznie okazują się
potrzebne, na przykład:
void foo(void)
{
int x = 1;
x++;
int y = 2;
float f;
// ... i tak dalej
}
Natomiast w języku Pascal wszystkie deklaracje zmiennych muszą być zlokalizowane przed blokiem kodu danej
procedury, funkcji lub programu głównego:
Procedure Foo;
var
x, y : integer;
f : double;
begin
x := 1;
Inc(x);
y := 2;
(*
itd.
*)
end;
Może się to wydawać nieco krępujące, lecz w rzeczywistości prowadzi do programu bardziej czytelnego i mniej
podatnego na błędy — co jest charakterystyczne dla Pascala, stawiającego raczej na bezpieczeństwo aplikacji niż
hołdowanie określonym konwencjom.
Notatka
Object Pascal i Visual Basic, w przeciwieństwie do Javy i C, niewrażliwe są na wielkość liter w nazwach
elementów składniowych. Wrażliwość taka zwiększa co prawda elastyczność języka, lecz niepomiernie
zwiększa również prawdopodobieństwo popełnienia trudnych do wykrycia błędów. W Pascalu raczej trudno
odczuć brak takiej elastyczności, za to programista ma dość dużą swobodę stylistyczną w pisowni nazw,
na przykład nazwa
prostesortowaniemetodąprzesiewaniazograniczeniami
staje się bardziej czytelna, gdy jest zapisana w tzw. notacji „wielbłądziej”
ProsteSortowanieMetodąPrzesiewaniaZOgraniczeniami
Deklaracje zmiennych tego samego typu mogą być łączone, na przykład deklarację
var
Zmienna1 : integer;
Zmienna2 : integer;
Jest to jednak dopuszczalne wyłącznie dla zmiennych globalnych; kompilator nie zezwoli na inicjowanie w ten
sposób zmiennych lokalnych w procedurach i funkcjach.
Wskazówka
Kompilator dokonuje również automatycznej inicjalizacji wszystkich zmiennych globalnych bez deklarowanej
wartości początkowej, zerując zajmowaną przez nie pamięć; tak więc wszystkie zmienne całkowitoliczbowe
otrzymują wartość 0, zmiennoprzecinkowe — wartość 0.0, łańcuchy stają się łańcuchami pustymi, wskaźniki
otrzymują wartość NIL itp.
Stałe
Stałe (ang. constants) są synonimami konkretnych wartości występujących w programie. Deklaracja stałych
poprzedzona jest słowem kluczowym const i składa się z jednego lub więcej przypisań wartości nazwom
synonimicznym, na przykład:
const
DniWTygodniu = 7;
Stanowisk = 7;
TaboretyNaStanowisku = 4;
TaboretyOgolem = TaboretyNaStanowisku * Stanowisk;
Komunikat = 'Przerwa śniadaniowa';
Zasadniczą różnicą między Object Pascalem a językiem C — w zakresie deklaracji stałych — jest to, że Pascal
nie wymaga deklarowania typów stałych; kompilator sam ustala typ stałej na podstawie przypisywanej wartości.
Ponadto, stałe synonimiczne typów skalarnych nie zajmują dodatkowego miejsca w pamięci programu, gdyż
istnieją jedynie w czasie jego kompilacji. Więc na przykład następujące deklaracje języka C:
const float AdecimalNumber = 3.14
const int i = 10
const char *ErrorString = "Uwaga, niebezpieczeństwo";
const
AdecimalNumber = 3.14;
i = 10;
ErrorString = 'Uwaga, niebezpieczeństwo';
Kompilator, ustalając typ stałej, wybiera typy o możliwie najmniejszej liczebności. Typ stałej
całkowitoliczbowej ustalany jest w następujący sposób:
Tabela 2.1 Ustalane przez kompilator typy stałych całkowitoliczbowych
Wartość Typ
od –2^63 do –2.147.483.649 Int64
od –128 do –1 Shortint
od 0 do 127 0 .. 127
od 2.147.483.648 do Cardinal
4.294.967.295
Ponadto
• stałe o wartościach rzeczywistych są stałymi typu Extended;
• stałe łańcuchowe, zależnie od ustawienia przełącznika kompilacji $H, są albo łańcuchami typu
ShortString, albo łańcuchami typu AnsiString;
• dla zbiorów (sets) liczbowych i znakowych rozmiar zajętej pamięci wynika bezpośrednio z ich postaci.
Aby uzyskać większą kontrolę nad danymi, programista może jawnie wskazać typ deklarowanej stałej, na
przykład:
const
ADecimalNumber : Double = 3.14;
i : integer = 10;
ErrorString : string = 'Uwaga, niebezpieczeństwo';
Stała ADecimalNumber jest teraz stałą typu Double, bez jawnego wskazania typu byłaby natomiast stałą typu
Extended.
Jawne wskazywanie typów w deklaracjach stałych zasługuje na nieco więcej uwagi. Programista znający Turbo
Pascal rozpozna w tych konstrukcjach zmienne inicjowane, które zostały nazwane stałymi przez
nieporozumienie, gdyż w rzeczywistości zachowują się w programie jak zwykłe zmienne; mógł się o tym
przekonać każdy, kto chciał użyć tak zdefiniowanej „stałej” jako np. indeksu granicznego w deklaracji tablicy.
Tak było we wszystkich wersjach Turbo Pascala i w Delphi 1, natomiast wersja Delphi 2 przyniosła pewną
nowość w tej materii: otóż przy włączonym przełączniku {$J} wszystko jest po staremu, jednak jego
wyłączenie spowoduje, iż kompilator zabroni modyfikowania tak deklarowanych stałych. Zdecydowanie zaleca
się tę drugą ewentualność — stałe pozostają wówczas naprawdę stałymi, zaś zmiennym można nadawać
początkowe wartości za pomocą dyrektywy var.
W wyrażeniach przypisywanych stałym (oraz przy inicjowaniu zmiennych) Object Pascal zezwala na wy-
korzystanie następujących funkcji wbudowanych: Abs(), Chr(), Hi(), High(), Length(), Lo(), Low(),
Odd(), Ord(), Pred(), Round(), SizeOf(), Succ(), Swap() i Trunc() — na przykład w taki sposób:
type
A = array [ 1 .. 2 ] of Integer;
const
W : Word = SizeOf(byte)
var
i : integer = 8;
j : SmallInt = Ord('a');
L : Longint = Trunc(3.14159);
x : ShortInt = Round(2.71828);
B1 : byte = High(A);
B2 : byte = Low(A);
C : Char = Chr(46);
Wskazówka
Ku rozczarowaniu wielu programistów, Object Pascal nie posiada preprocesora podobnego do tego z języka
C. Nie można więc definiować makroinstrukcji i stąd brak mechanizmu odpowiadającego słowu kluczowemu
#define języka C (dyrektywa $define ma znaczenie zupełnie inne — definiuje tzw. symbole warunkowe
kompilacji). Jednak, począwszy od Delphi 6, można wykorzystać dyrektywy $IF i $ELSEIF umożliwiające
użycie definiowanych stałych na równi z symbolami kompilacji warunkowej.
Operatory
Operatory są symbolami języka służącymi — mówiąc najogólniej — do manipulowania danymi. Istnieją
operatory arytmetyczne — dodawania, odejmowania, mnożenia i dzielenia wartości liczbowych, operator
przypisania, wyboru elementu z tablicy itp. W niniejszym podrozdziale rozpatrzymy większość operatorów
Object Pascala i przedstawimy ich odpowiedniki w językach C, Visual Basic i Java.
Operator przypisania
Operator przypisania służy do przypisania zmiennej wartości; jest to bodaj najprostszy, lecz jednocześnie jeden z
najważniejszych operatorów języka. Oto jeden z przykładów jego zastosowania:
Number1 := 5;
Operatory porównania
Operatory porównania w Delphi i w Visual Basicu są niemalże identyczne. Służą do stwierdzenia równości lub
nierówności dwóch wartości albo ich porównania pod względem relacji mniejszości. W tabeli 2.2 przedstawione
zostały łącznie operatory porównania i operatory logiczne, w tym miejscu chcemy jedynie zwrócić uwagę na
istotną różnicę między operatorem porównania (= w Delphi, == w C i Javie) a operatorem przypisania (:= w
Delphi, = w C i Javie).
Operator badający nierówność dwóch wielkości, w języku C mający sugestywną postać !=, w Pascalu ma postać
<>, na przykład:
if x <> y
Then
Cokolwiek
Operatory logiczne
Operatory logiczne realizują (w ograniczonym zakresie) operacje wynikające z algebry Boole’a (stąd często
nazywane bywają operatorami boolowskimi — ang. Boolean operators). Ich typowym zastosowaniem jest
jednoczesne testowanie kilku warunków, na przykład:
if (warunek1) and (warunek2)
Then
Cokolwiek
While (warunek1) or (warunek2) do
Cokolwiek
Operatory logiczne obecne są w każdym języku programowania, chociaż ich postać jest różnorodna. Tabela 2.2
przedstawia operatory porównania oraz operatory logiczne w Pascalu, C, Javie i Visual Basicu.
Tabela 2.2. Operatory przypisania, porównania i operatory logiczne
Operator Pascal Java i C Visual Basic
Przypisania := = =
Logiczne „lub” or || Or
not ! Not
Zaprzeczenie
Operatory arytmetyczne
Tabela 2.3 prezentuje operatory arytmetyczne Pascala, C, Javy i Visual Basica.
Jak wynika z tabeli, Pascal i Visual Basic rozróżniają dzielenie liczb całkowitych (wynik jest liczbą całkowitą)
od dzielenia liczb rzeczywistych (wynik jest liczbą rzeczywistą); Java i C nie czynią takiego rozróżnienia.
Ostrzeżenie
Wykonując dzielenie, zawsze używaj operatorów stosownych do operandów i oczekiwanego wyniku. Kom-
pilator Object Pascala nie zezwoli na dzielenie całkowite operandów, z których co najmniej jeden nie jest
liczbą całkowitą. Równie powszechnym błędem jest próba przypisania zmiennej całkowitej wyniku dzielenia
rzeczywistego (operator /), co ilustruje poniższy przykład:
Var
i : integer;
r : real;
begin
i := 4/3 // tu wystąpi błąd kompilacji
r := 3.4 div 2.3; // ta linia również jest błędna
i := Trunc(4/3); // ta linia jest poprawna
r := 3.4 / 2.3; // ta linia również jest poprawna
end;
Jako ciekawostkę odnotować należy fakt, iż wiele języków nie wykonuje dzielenia całkowitego i w związku z
tym posiada jeden, uniwersalny operator dzielenia. Dzielenie dwóch liczb całkowitych przebiega więc
następująco: konwersja na typ zmiennoprzecinkowy, wykonanie dzielenia zmiennoprzecinkowego oraz
konwersja wyniku (po zaokrągleniu lub obcięciu — różnie bywa) na typ całkowity. Jest to działanie kosztowne
oraz nieefektywne w sytuacji, gdy procesor posiada instrukcje dzielenia całkowitego (posiadają je wszystkie
procesory 80×86).
Operatory bitowe
Operatory bitowe służą do operowania na poszczególnych bitach wartości binarnej reprezentującej dane
wyrażenie. Operacje te zaliczyć można do jednej z dwóch kategorii: logiczne operacje na bitach oraz
przesuwanie bitów. Tabela 2.4 przedstawia operatory bitowe dla czterech rozpatrywanych tu języków.
Tabela 2.4. Operatory bitowe
i jest przez kompilator przekładana na pojedynczą instrukcję INC lub DEC kodu maszynowego.
Operatory w postaci dwuargumentowej
Inc(zmienna, dystans);
Dec(zmienna, dystans);
powodują zmniejszenie albo zwiększenie zmiennej o wartość zadaną jawnie w postaci drugiego argumentu;
operacja jest realizowana przez kompilator w postaci rozkazu ADD albo SUB.
Notatka
Kompilator Delphi w wersji 2 i następnych jest na tyle „inteligentny”, że sam rozpoznaje operację
zmniejszania/zwiększania zmiennej za pomocą zwykłej operacji dodawania lub odejmowania, tak więc
przekład instrukcji x := x + 1 nie różni się od przekładu instrukcji Inc(x)1, dlatego główną korzyścią
wynikającą z użycia omawianych operatorów jest raczej wygoda programisty.
Zestawienie operatorów zwiększania i zmniejszania dla omawianych języków przedstawia tabela 2.5.
Tabela 2.5. Operatory zwiększania i zmniejszania
Operator Pascal Java i C Visual Basic
Zwiększania Inc() ++ nie istnieje
Zmniejszania Dec() –– nie istnieje
Porównanie typów
Większość typów Object Pascala posiada swe odpowiedniki w C, Javie i Visual Basicu. Zestawienie widoczne w
tabeli 2.6 może być niezwykle użyteczne w przypadku wykorzystywania w którymś z tych języków bibliotek
DLL stworzonych w innym języku.
1
Z wyjątkiem sytuacji, gdy x jest właściwością klasy (przyp. tłum.)
całkowity 8-bitowy Byte nie istnieje BYTE, unsigned Byte
bez znaku short
całkowity 16- SmallInt short short Short
bitowy ze znakiem
całkowity 16- Word nie istnieje unsigned nie istnieje
bitowy bez znaku short
całkowity 32- Integer, int int, long Integer,
bitowy ze znakiem LongInt Long
całkowity 32- Cardinal, nie istnieje unsigned long nie istnieje
bitowy bez znaku LongWord
całkowity 64- Int64 long __int64 nie istnieje
bitowy ze znakiem
zmiennoprzecinko Single float float Single
wy 4-bajtowy
zmiennoprzecinko Real48 nie istnieje nie istnieje nie istnieje
wy 6-bajtowy
zmiennoprzecinko Double double double Double
wy 8-bajtowy
zmiennoprzecinko Extended nie istnieje long double nie istnieje
wy 10-bajtowy
stałoprzecinkowy Currency nie istnieje nie istnieje Currency
64-bitowy
data/czas 8-bajtowy TDateTime nie istnieje nie istnieje Date
wariantowy 16- Variant, VARIANT** Variant
nie istnieje
bajtowy Olevariant, Variant†, (domyślny)
TVarData Olevariant†
znak 1-bajtowy Char nie istnieje char nie istnieje
znak 2-bajtowy WideChar char WCHAR Char
** — oznacza powszechnie używaną klasę lub typ, nie zaś rodzimy typ języka
Wskazówka
Podczas przenoszenia aplikacji z Delphi 1 bądź świadom tego, że typy Integer i Cardinal, 16-bitowe w
Delphi 1, są już 32-bitowe w Delphi 2 oraz w następnych wersjach. W Delphi 4 zmieniło się też znaczenie typu
Cardinal: w Delphi 2 i 3 jego zakres tożsamy był z nieujemną połową typu integer (bit znaku był po prostu
ignorowany), natomiast począwszy od Delphi 4 jest on pełnoprawną 4-bajtową liczbą całkowitą bez znaku o
zakresie 0 ÷ 4294967296.
Ostrzeżenie
W Delphi 4 zmieniło się również znaczenie identyfikatora Real. W Delphi 1, 2 i 3 oznaczał on — specyficzny
dla Turbo Pascala — 6-bajtowy format liczby zmiennoprzecinkowej, obsługiwany całkowicie w sposób
programowy i nie mający odpowiednika w formatach danych (ko)procesorów. Począwszy od Delphi 4, iden-
tyfikator Real jest synonimem typu Double; wspomnianemu formatowi 6-bajtowemu odpowiada natomiast
identyfikator Real48. Możliwe jest jednak przywrócenie dawnego znaczenia identyfikatora Real — poprzez
użycie dyrektywy kompilatora {$REALCOMPATIBILITY ON}
Znaki
W Delphi istnieją trzy typy reprezentujące pojedynczy znak:
• AnsiChar — to powszechny w większości dotychczasowych języków 1-bajtowy znak ANSI,
• Char — w obecnej wersji Delphi jest to typ identyczny z AnsiChar, lecz Borland zastrzegł sobie prawo
ewentualnego utożsamienia go z typem WideChar w przyszłych wersjach.
Fakt, że znak niekoniecznie jest teraz jednobajtowy, stanowi przesłankę nieco ostrożniejszego kodowania, a
przy ustalaniu rozmiaru struktur zawierających znaki, wskazane jest korzystanie z funkcji SizeOf().
Wskazówka
Mnogość łańcuchów…
Łańcuchem (string) nazywamy ciąg znaków i oczywiście reprezentujący go obiekt języka programowania. To
interesujące, że różnorakość implementacji łańcuchów w różnych językach programowania jest znacznie
większa niż w jakimkolwiek innym aspekcie języka.
W Object Pascalu obsługa łańcuchów znaków zrealizowana została w postaci następujących typów:
• AnsiString — podstawowy typ łańcuchów danych w Object Pascalu. Jest ciągiem (o potencjalnie
nieograniczonej długości) znaków AnsiChar. Zgodny jest z typem reprezentującym ciąg jednobajtowych
znaków zakończony bajtem zerowym.
• ShortString — to stary pascalowy znajomy — łańcuch znaków AnsiChar o ustalonej z góry
maksymalnej długości, nie przekraczającej 255 znaków. Podstawowy typ łańcuchowy w Delphi 1.
• WideString — stanowi ciąg znaków WideChar i, podobnie jak AnsiString, nie ma limitowanej
długości i zakończony jest znakiem zerowym.
• PChar — to wskaźnik do ciągu znaków typu Char zakończonego bajtem zerowym, na wzór typów char*
lub lpstr w języku C.
• PAnsiChar — to wskaźnik do zakończonego znakiem zerowym ciągu znaków typu AnsiChar.
• PWideChar — jest to wskaźnik do zakończonego znakiem zerowym ciągu znaków typu WideChar.
Zmienna deklarowana jako String jest zmienną typu AnsiString lub ShortString, zależnie od ustawienia
przełącznika kompilacji $H:
var
{$H-}
S1 : String // zmienna S1 jest typu ShortString
{$H+}
S2 : String // zmienna S2 jest typu AnsiString
Zmienna deklarowana jako String z wyspecyfikowaną maksymalną długością (nie większą niż 255) jest jednak
zawsze typu ShortString:
var
{$H-}
S1[63] : String // zmienna S1 jest typu ShortString
{$H+}
S2[63] : String // zmienna S2 jest typu ShortString
Typ AnsiString
Typ AnsiString, zwany również potocznie long string lub po polsku „długi łańcuch” pojawił się po raz
pierwszy w Delphi 2. Uosabia on tę samą łatwość obsługi, jaką miały „klasyczne” łańcuchy pascalowe i jest
jednocześnie wolny od bardzo dotkliwego ograniczenia długości do 255 znaków — długość łańcucha typu
AnsiString jest praktycznie nieograniczona.
Obsługa długich łańcuchów wiąże się z dość wyrafinowaną, niewidoczną dla użytkownika gospodarką pamięcią
operacyjną, polegającą na jej przydziale stosownie do potrzeb i odzyskiwaniu (ang. garbage collection) wtedy,
gdy nie jest już potrzebna (za chwilę zajmiemy się tym interesującym zagadnieniem w szerszym kontekście).
Uwalnia to programistę od „ręcznej” obsługi pamięci, tak uciążliwej w C++ i wersji 7.0 Borland Pascala (typ
PChar). Strukturę łańcucha AnsiString w pamięci operacyjnej przedstawia rysunek 2.1.
Ostrzeżenie
Wewnętrzny format długich łańcuchów Delphi nie został udokumentowany — firma Borland pozostawiła sobie
w ten sposób możliwość jego modyfikacji w przyszłości. Aplikacje bazujące na tym formacie niosą więc ze
sobą ryzyko ewentualnej niezgodności z przyszłymi wersjami Delphi. Z podobnym problemem zetknęli się
swego czasu użytkownicy Delphi 1 zakładający, iż bieżąca długość łańcucha reprezentowana jest przez jego
początkowy bajt.
Jeżeli więc mimo wszystko prezentujemy tutaj wewnętrzną strukturę długiego łańcucha, to czynimy to
wyłącznie w celu poglądowego wytłumaczenia zasad jego funkcjonowania.
Jak widać na rysunku 2.1, długi łańcuch reprezentowany jest w zmiennej jako wskaźnik do ciągu znaków.
Systemowe procedury zarządzające obsługą łańcuchów gwarantują ponadto, że jest on zakończony bajtem
zerowym. Powoduje to, że długie łańcuchy mogą być używane jako parametry wywołania procedur i funkcji
Win32 API, wymagających łańcuchów z zerowym ogranicznikiem. Najbardziej nieoczywistym elementem
rysunku jest natomiast z pewnością licznik odwołań. Otóż, w celu zminimalizowania zużycia pamięci, Object
Pascal stara się zapamiętywać w pojedynczym egzemplarzu zawartość dwóch (lub więcej) zmiennych
łańcuchowych o (aktualnie) identycznej zawartości, kontrolując jedynie odwołania do tego egzemplarza za
pomocą wspomnianego licznika. Tak więc przypisanie zmiennej łańcuchowej zawartości innej zmiennej nie
spowoduje fizycznego kopiowania, lecz tylko powielenie wskaźnika (fizyczną reprezentacją zmiennych
łańcuchowych są bowiem wskaźniki) i zwiększenie licznika odwołań. Modyfikacja którejś z tych zmiennych
spowoduje jednak zerwanie jej dotychczasowego związku ze wspomnianym egzemplarzem, zmniejszenie
licznika odwołań z nim związanego i utworzenie nowego, niezależnego egzemplarza o zmienionej zawartości.
Poniższy przykład z pewnością dostatecznie wyjaśnia tę koncepcję:
var
S1, S2 : AnsiString;
begin
S1 := 'Taki sobie napis ... '
{
licznik odwołań długiego łańcucha wskazywanego
przez S1 jest równy 1
}
S2 := S1;
{
S1 i S2 wskazują na ten sam długi łańcuch, którego
licznik odwołań jest teraz równy 2
}
{
Nastąpiło utworzenie niezależnego egzemplarza dla
zmiennej S2,
S1 i S2 wskazują teraz na dwa różne obszary pamięci.
Licznik odwołań łańcucha wskazywanego przez S1 znowu
jest równy 1
}
<ramka>
Pojęcie „czasu życia” wynika z pojęcia zakresu widoczności deklaracji zmiennej. W Pascalu zmienne globalne
„żyją” więc przez cały czas realizacji programu, zmienne lokalne procedur i funkcji — jedynie w czasie
realizacji tychże procedur i funkcji. Ten oczywisty poniekąd fakt nie powodował żadnych szczególnych
implikacji — aż do pojawienia się Delphi 2 i łańcuchów AnsiString, na potrzeby których, „w tle”, realizowany
jest skomplikowany scenariusz gospodarowania pamięcią.
W Turbo Pascalu, po zakończeniu realizacji programu, zwalniana była cała przydzielona mu pamięć — i tym
samym unicestwiane były wszystkie zmienne globalne. Podobne „unicestwienie” zmiennych lokalnych
procedur i funkcji sprowadzało się po prostu do zdjęcia ich ze stosu. Rozważmy jednak poniższy przykład:
procedure MyProc;
var
X: Pointer;
begin
GetMem(X,10000);
...
...
FreeMem(X,10000);
end;
Obszar wskazywany przez wskaźnik X istnieje tylko w czasie realizacji procedury MyProc i nie ma poza nią
żadnego znaczenia z tego prostego względu, iż jest na zewnątrz niej niedostępny. Załóżmy teraz, iż
programista zapomniał o końcowej instrukcji FreeMem. Konsekwencją tego faktu byłaby po prostu strata
10000 bajtów pamięci przy każdym wywołaniu procedury. Stąd ważny wniosek, iż do zniwelowania skutków
przydziału pamięci nie wystarczy proste zdjęcie zmiennej X ze stosu.
Ten prosty przykład wyjaśnia istotę problemu, który pojawił się w momencie wprowadzenia do Pascala
zmiennych, na potrzeby których gospodarka pamięcią nie posiada — mówiąc najprościej — odzwierciedlenia
w kodzie źródłowym programu, lecz wykonywana jest „w tle”. Konkretnie — w przypadku zmiennych typu
AnsiString, w związku z zakończeniem procedury nie wystarczy już zwykłe zdjęcie ze stosu zmiennej,
zawierającej li tylko wskaźnik do danych zasadniczych; z drugiej strony wykluczone są jakiekolwiek jawne
instrukcje zwalniające, gdyż gospodarka pamięcią odbywa się tu bez związku z kodem źródłowym programu.
Stąd wniosek, iż integralną częścią procesu niejawnego gospodarowania pamięcią powinno być jej
automatyczne zwalnianie w przypadku zakończenia czasu życia odnośnej zmiennej. Proces taki rzeczywiście
ma miejsce, a zmienne, których on dotyczy, noszą nazwę zmiennych z kontrolowanym czasem życia (lifetime
memory-managed). Opisywane w niniejszym punkcie zmienne typu AnsiString i WideString są właśnie
takimi zmiennymi — inne przykłady zmiennych tej kategorii przedstawimy w dalszej części rozdziału i w
rozdziałach następnych.
Dokładniej — proces obsługi zmiennych o kontrolowanym czasie życia daje się pokrótce opisać następująco:
zmienne globalne inicjowane są automatycznie podczas wykonywania sekcji initialization modułu, w
którym zostały zdefiniowane (lub podczas rozpoczynania programu, o ile moduł takiej sekcji nie posiada).
Wszystkie czynności związane z zakończeniem „czasu życia” zmiennych odbywają się — również
automatycznie — w czasie kończenia sekcji finalization tego modułu (lub podczas kończenia programu,
jeśli moduł sekcji finalization nie posiada). W stosunku do zmiennych lokalnych procedur i funkcji
odpowiednie działania następują podczas wejścia do procedury i bezpośrednio przed wyjściem z niej. Skutek
jest taki, jak gdyby treść procedury „zanurzona” została w dodatkowym bloku try…finally (oczywiście —
tylko pojęciowo), co ilustruje następujący przykład:
procedure Foo;
var
S: AnsiString;
begin
...
...
end;
procedure Foo;
var
S: AnsiString;
begin
S := ''; // inicjalizacja
try
...
...
finally
end;
end;
Nie jest to zresztą nic nadzwyczajnego. Wróćmy na chwilę do procedury MyProc — jeżeli programista
chciałby uniknąć zagubienia owych 10000 bajtów pamięci na skutek wystąpienia wyjątku, powinien swą
procedurę sformułować mniej więcej tak:
procedure MyProc;
var
X: Pointer;
begin
X := NIL;
try
GetMem(X,10000);
...
...
finally
if X <>NIL
Then
FreeMem(X,10000);
end;
end;
<ramka>
Operacje na łańcuchach
Do połączenia (konkatenacji) dwóch łańcuchów służy operator + lub funkcja Concat()— ta ostatnia
zachowana została ze względów kompatybilności i należy jej raczej unikać. Oto przykłady łączenia łańcuchów
Object Pascala:
Var
S, S2 : AnsiString;
begin
S := 'Szafa';
S2 := ' grająca';
S := S + S2; { Szafa grająca }
end.
Var
S, S2 : AnsiString;
begin
S := 'Szafa';
S2 := ' grająca';
S := Concat(S,S2); { Szafa grająca }
end.
Wskazówka
Notatka
Funkcja Concat() stanowi przykład „magicznych” funkcji kompilatora — owa „magia” polega na tym, iż
funkcji tej nie da się napisać w Pascalu. Takich „funkcji” i „procedur” jest w Pascalu więcej — że wspomnimy
tylko o najpopularniejszych Readln(), Writeln(), New(), SizeOf(). Ich lista jest zamknięta i ściśle
określona — przy ich przekładzie na kod wynikowy kompilator posługuje się specjalnie zdefiniowanymi na tę
okazję modułami (ang. helper functions) zawartymi w bibliotece RTL i w module System.
Niezależnie od „magicznych” funkcji oraz procedur istnieje dość pokaźny zestaw „pascalowych”
podprogramów operujących na łańcuchach; są one w większości zlokalizowane w module SysUtils, a ich
wykaz można odnaleźć w systemie pomocy pod hasłem „String-handling routines (Pascal-
style)”. Dodatkowo, kilka użytecznych podprogramów operujących na łańcuchach możesz znaleźć w
module StrUtils, znajdującym się na dołączonym do książki krążku CD-ROM w katalogu \Source\Utils.
Długość łańcucha i alokacja pamięci
Bezpośrednio po zadeklarowaniu, zmienna łańcuchowa nie posiada przydzielonej pamięci i nie reprezentuje
żadnego napisu. Przypisanie jej jakiegoś napisu spowoduje przydział pamięci lub ustalenie wskazania na obszar
już przydzielony. Ilustruje to poniższy przykład:
Var
S1, S2 : AnsiString;
begin
S1 := 'Pierwszy';
{
zmiennej S1 przydzielono obszar pamięci o wielkości co
najmniej 9 bajtów, zawierający obecnie napis 'Pierwszy'
zakończony bajtem zerowym
}
S2 := 'Drugi';
{
zmiennej S2 przydzielono obszar pamięci o wielkości co
najmniej 6 bajtów, zawierający obecnie napis 'Drugi'
zakończony bajtem zerowym
}
S1 := S2;
{
zmienne S1 i S2 wskazują teraz na ten sam łańcuch. Obszar
poprzednio wskazywany przez zmienną S2 nie jest już do
niczego potrzebny i może zostać ponownie wykorzystany
}
Mechanizm zarządzania pamięcią na potrzeby długich łańcuchów jest bardzo wyrafinowany, w jednym wszakże
przypadku nie zapewnia on dostatecznej długości łańcucha: wtedy, gdy odwołujesz się do niego na wzór
tablicowy, używając indeksu explicite:
Var
S : AnsiString;
begin
S[1] := 'a';
W powyższym przykładzie zmiennej S nie została jeszcze przydzielona pamięć, jej zawartością jest pusty
wskaźnik, a więc odwołanie do S[1] gwarantuje wystąpienie błędu wykonania. Jeżeli jednak odwołanie
powyższe poprzedzone zostanie przypisaniem łańcuchowi S napisu co najmniej jednoznakowego, błąd nie
wystąpi:
Var
S, T : AnsiString;
begin
S := 'b';
S[1] := 'a'
T := 'Ala ma kota';
T[1] := 'U';
Obecnie treścią zmiennej S jest jednoznakowy napis 'a', natomiast treścią zmiennej T jest zdanie 'Ula ma
kota'.
Istnieje jednak prostszy sposób na wymuszenie przydziału pamięci dla długiego łańcucha — służy do tego
funkcja SetLength():
Var
S : AnsiString;
begin
Setlength(S,1);
S[1] := 'a'
To jednak jeszcze nie koniec: jak wynika z rysunku 2.1, długiemu łańcuchowi towarzyszą dodatkowe informacje
organizacyjne. Procedury Win32 API, traktując parametry jako ciągi znaków zakończone bajtem zerowym (i nic
ponadto), nie są owych „dodatków” świadome. Z tego też względu, po wykonaniu procedury
GetWindowsDirectory() w powyższym przykładzie, zmienna S nie jest jeszcze „rasowym” długim
łańcuchem — programista musi sam uaktualnić informacje dodatkowe. Da się to łatwo wykonać za pomocą
wspomnianej funkcji SetLength() lub nieco wygodniejszej funkcji RealizeLength() znajdującej się w
module StrUtils:
procedure RealizeLength ( var S : String );
begin
SetLength( S, StrLen(PChar(S)) );
end;
Ostrzeżenie
Ponieważ długi łańcuch podlega procesowi odzyskiwania pamięci (garbage collection), należy zachować
ostrożność podczas jego rzutowania na typ PChar — należy mianowicie upewnić się, iż zmienna stanowiąca
argument rzutowania nie zakończyła jeszcze swego życia. W kontekście przedstawionych wcześniej
informacji na temat zmiennych o kontrolowanym czasie życia, wymaganie takie wydaje się zupełnie
oczywiste.
StrVar := PCharVar;
zamiast niegdysiejszego
StrVar := StrPas(PCharVar);
Typ ShortString
Klasyczne pascalowe łańcuchy dostępne są nadal pod postacią typu ShortString, funkcjonującego niezależnie
od ustawienia przełącznika $H. Dla uproszczenia będziemy ów typ nazywać „krótkim łańcuchem”.
Pod względem strukturalnym krótki łańcuch jest tablicą jednobajtowych znaków, indeksowaną począwszy od 1 i
poprzedzoną bajtem zawierającym bieżącą długość (bajt ten uważany jest także za zerowy element wspomnianej
tablicy). Ilustruje to rysunek 2.2.
Zarządzanie krótkimi łańcuchami nie wiąże się z dynamicznym gospodarowaniem pamięcią — pamięć dla
zmiennej przydzielana jest od razu zgodnie z jej zadeklarowaną długością, zmienne typu ShortString nie są
więc zmiennymi o kontrolowanym czasie życia. Operacje na krótkich łańcuchach są więc bardzo efektywne,
jednak dotkliwe jest ograniczenie ich maksymalnej długości do 255 znaków.
Oto przykład przypisania wartości krótkiemu łańcuchowi:
var
S : ShortString;
begin
S := 'Krótki napis';
Dla oszczędności pamięci, możemy ograniczyć maksymalną długość krótkiego łańcucha, wskazując ją (w
deklaracji) w nawiasach kwadratowych, na przykład:
var
Nazwisko: string[20];
Zgodnie z powyższą deklaracją zmienna Nazwisko zajmuje w pamięci 21 bajtów. Gdybyśmy zadeklarowali ją
jako
var
Nazwisko: ShortString;
Przypisywanie krótkiemu łańcuchowi napisu zbyt długiego w stosunku do zadeklarowanej długości jest
bezpieczne dla aplikacji — z zastrzeżeniem, iż końcówka napisu zostaje utracona. W wyniku wykonania
poniższej sekwencji
var
Napis: String[8];
…
Nie jest natomiast bezpieczne odwoływanie się do poszczególnych pozycji łańcucha poza zadeklarowaną
długością — poniższa sekwencja z dużym prawdopodobieństwem spowoduje błąd wykonania, w każdym razie
jej skutki są nieokreślone (chyba że ustawiono przełącznik $R+, wtedy wykryte zostanie przekroczenie
dopuszczalnego zakresu):
var
Napis: String[8];
i: integer;
…
i := 10;
Napis[i] := '*';
Notabene gdy napiszemy to bardziej bezpośrednio
var
Napis: String[8];
…
Napis[10] := '*';
błąd zostanie wykryty już na etapie kompilacji — na takie prymitywne sztuczki kompilator jest bowiem za
mądry.
Wskazówka
Mimo iż ustawienie przełącznika $R+ może przyczynić się do wykrycia wielu błędów związanych m.in. z
przekroczeniem zadeklarowanego zakresu tablicy lub łańcucha, związane z tym testy generalnie spowalniają
wykonanie aplikacji, więc po (wystarczającym) przetestowaniu aplikacji, należy ów przełącznik wyłączyć.
W przeciwieństwie do długich łańcuchów, krótkie łańcuchy zupełnie nie nadają się na parametry wywołań
większości funkcji Win32 API, nie posiadają bowiem zerowego ogranicznika. Ich przekształcenie do postaci z
zerowym ogranicznikiem nie sprowadza się do zwykłego rzutowania typów, lecz wymaga pewnych
dodatkowych operacji. Zajmuje się tym poniższa funkcja z modułu StrUtils:
Oto jeszcze jeden przykład przekształcenia długiego łańcucha w łańcuch z zerowym ogranicznikiem:
Function ShortToASCIIZ ( var S : ShortString ) : PChar;
// © A. Grażyński
var
k: byte;
begin
k := Length(S);
if k > High(S) // to na wypadek, gdyby S[0] zawierało zbyt dużą wartość
then
k := High(S);
Move(S[1], S[0], k);
S[k] := #0;
ShortToASCIIZ := @S[0];
end;
Procedura ShortToASCIIZ nie wymaga ograniczenia długości łańcucha wejściowego, powoduje jednak
zniszczenie jego zawartości. Możemy jednak tę zawartość odzyskać, dokonując prostego wywołania
S := StrPas(X);
gdzie X jest wskaźnikiem o wartości zwróconej przez funkcję ShortToASCIIZ (wskaźnik ten straci oczywiście
swą ważność):
var
S: ShortString;
X: PChar;
begin
...
S := 'Jakiś napis';
X := ShortToASCIIZ(S)
...
JakasFunkcjaAPI(X);
...
S := StrPas(X);
Typ WideString
Łańcuchy typu WideString są odpowiednikami łańcuchów AnsiString w świecie dwubajtowych znaków
WideChar. Są one również dynamicznie alokowane, a ich zmienne są zmiennymi o kontrolowanym czasie
życia. Ponadto typy AnsiString oraz WideString są ze sobą zgodne w sensie przypisania, istnieją jednak trzy
podstawowe różnice pomiędzy nimi:
• Łańcuchy WideString składają się z dwubajtowych znaków WideChar, co czyni je odpowiednimi do
przechowywania słów utworzonych nad alfabetem kompatybilnym z kodem Unicode.
• Pamięć dla łańcuchów WideString alokowana jest za pomocą funkcji API SysAllocStrLen(), co czyni
je kompatybilnymi z charakterystycznymi dla OLE łańcuchami typu BSTR.
• W odniesieniu do łańcuchów WideString nie jest prowadzona „oszczędnościowa polityka” współdzielenia
egzemplarzy na podstawie licznika odwołań. Oznacza to, że łańcuchy WideString, identyczne pod
względem zawartości, lecz związane z różnymi zmiennymi zawsze istnieją w postaci oddzielnych
egzemplarzy. Oznacza to również, iż każda operacja przypisania pomiędzy dwiema zmiennymi typu
WideString realizowana jest przez rzeczywiste kopiowanie zawartości łańcucha. Czyni to oczywiście
łańcuchy WideString mniej efektywnymi od łańcuchów AnsiString pod względem szybkości
operowania oraz wykorzystania pamięci.
Jak wspomniano wcześniej, typy AnsiString oraz WideString są ze sobą zgodne w sensie przypisania —
wszystkie niezbędne konwersje wykonywane są automatycznie. Poniższy fragment programu jest więc
poprawny w Object Pascalu:
var
W: WideString;
S: AnsiString;
begin
W := 'Margaritaville';
S := W;
S := 'Come Monday';
W := S;
end;
Łańcuchy WideString mogą także występować jako parametry standardowych funkcji operujących na
łańcuchach — Concat(), Copy(), Insert(), Pos(), SetLength(), Length() itp. — mogą być również
argumentami operatorów +, = i <>, na przykład:
var
W1, W2 : WideString;
P: Integer;
begin
W1 := 'Enfield';
W2 := 'field';
if W1 <> W2
Then
P := Pos(W1, W2);
end;
Dopuszczalne jest również odwoływanie się do poszczególnych znaków łańcucha WideChar, na przykład:
var
W: WideString;
C: WideChar;
begin
W := 'Ebony and Ivory living in perfect harmony';
C := W[Length(W)]; // C zawiera ostatni znak łańcucha W
end;
Łańcuchy z zerowym ogranicznikiem
Łańcuchy takie, zwane w oryginale null terminated strings, stanowią ciąg znaków z wyróżnionym (np. przez
wskaźnik) pierwszym znakiem. Wszystkie następne znaki aż do znaku o kodzie zero stanowią zawartość
łańcucha, znak zerowy nie jest już jego częścią i pełni rolę ogranicznika. Stąd prosty wniosek, że repertuar
znaków reprezentowalnych w łańcuchach omawianego typu jest zubożony o znak o kodzie zero.
W poprzednich wersjach Pascala, do Delphi 1 włącznie, znak reprezentowany był przez jeden bajt pamięci, stąd
też istniał jedynie jeden typ łańcuchów z zerowym ogranicznikiem — typ PChar. Typ ten istnieje nadal w
następnych wersjach Delphi, ze względów kompatybilności oraz na potrzeby interfejsu Win32 API. Ze względu
jednakże na trzy typy znaków (Char, WideChar i AnsiChar) w wprowadzono w Delphi 2 dodatkowe typy
omawianych łańcuchów: PWideChar i PAnsiChar.
Jak łatwo wywnioskować z powyższego opisu, łańcuch z zerowym ogranicznikiem reprezentowany jest przez
wskaźnik do pierwszego znaku (rysunek 2.3), a wymienione trzy typy PChar, PWideChar i PAnsiChar są
według terminologii języka Pascal typami wskaźnikowymi (pointers).
Tabela 2.7. Funkcje przydziału i zwalniania pamięci operacyjnej na potrzeby łańcuchów z zerowym
ogranicznikiem
Funkcja przydzielająca Funkcja zwalniająca
AllocMem() FreeMem()
GlobalAlloc() GlobalFree()
GetMem() FreeMem()
New() Dispose()
StrAlloc() StrDispose()
StrNew() StrDispose()
VirtualAlloc() VirtualFree()
Choć naruszenie reguł powyższej tabeli nie zawsze jest błędem (doświadczony programista wie przecież, że np.
StrAlloc() oraz StrNew() korzystają z procedury GetMem()), to jednak ich przestrzeganie zmniejsza ryzyko
popełnienia błędu.
Poniższy przykład ilustruje wykorzystanie łańcuchów z zerowym ogranicznikiem.
Var
P1, P2 : PChar;
S1, S2 : AnsiString;
begin
P1 := StrAlloc(64 * Sizeof(Char));
{ P1 wskazuje na 63 znakowy łańcuch }
S1 := 'vademecum profesjonalisty';
{ Do łańcucha S1 zostaje wpisana konkretna zawartość }
P2 := StrNew(PChar(S1));
{ P2 wskazuje na kopię S1 }
StrCat(P1, P2);
{ konkatenacja P1 i P2 }
S2 := P1
{ S2 zawiera napis 'Delphi 6 vademecum profesjonalisty' }
StrDispose(P1);
StrDispose(P2);
{ zwolnienie przydzielonej pamięci}
end.
Zwróć uwagę, że rozmiar przydzielanej pamięci zostaje obliczony za pomocą konstrukcji SizeOf(Char) —
jest to prostą konsekwencją faktu, że znak typu Char być może nie będzie już jednobajtowy w następnych
wersjach Delphi.
Funkcja StrCat() wykonuje konkatenację dwóch łańcuchów typu PChar — nie można w tym celu
wykorzystać operatora +, jak to miało miejsce w przypadku łańcuchów AnsiString, WideString i
ShortString.
Funkcja StrNew() tworzy kopię łańcucha podanego jako parametr. Ponieważ funkcje operujące na łańcuchach
z zerowym ogranicznikiem nie posiadają żadnej informacji o wielkości pamięci przydzielanej na ich potrzeby,
odpowiedzialność za przydzielenie wystarczająco dużego obszaru spoczywa całkowicie na programiście.
Najczęstszym błędem jest przydzielanie zbyt małego obszaru — w poniższym przykładzie funkcja StrCat()
usiłuje przypisać 13-znakowy napis „Witaj świecie” do łańcucha zdolnego pomieścić napis co najwyżej 6-
znakowy:
var
P1, P2 : PChar;
begin
P1 := StrNew('Witaj ');
P2 := StrNew('świecie!');
StrCat (P1, P2 ); // tu następuje wyjście poza przydzieloną pamięć
......
Wskazówka
Opis funkcji oraz procedur operujących na łańcuchach z zerowym ogranicznikiem znaleźć można w systemie
pomocy pod hasłem „String-handling routines (null-terminated)”. Wiele użytecznych funkcji zawiera również
moduł STRUTILS w katalogu \SOURCE\UTIL na załączonym krążku CD-ROM.
Typy wariantowe
Typ Variant jest w Pascalu zupełną nowością; konsekwencją wspomnianego wcześniej bezpieczeństwa typów
jest absolutne ustalenie typu każdej zmiennej już na etapie kompilacji. Takie podejście nie da się jednak
pogodzić ze standardami programowania w Windows, szczególnie w aspekcie mechanizmu OLE, o czym będzie
mowa w dalszej części książki. Wprowadzono więc możliwość dynamicznego „przepoczwarzania” się zmiennej,
czyli zmiany jej typu stosownie do kontekstu wykonywanego programu2. Najważniejszą przesłanką powstania
typu wariantowego była niewątpliwie konieczność przekazywania w jednolity sposób danych różnych typów w
ramach mechanizmu automatyzacji OLE. Nieprzypadkowo więc „delphicka” implementacja typu Variant jest
2
Zjawisko dynamicznej zmiany typu znane było już np. w popularnym języku Clipper — w wersji 87 nie istniała nawet
możliwość deklarowania typu zmiennej, wszystkie zmienne miały charakter wariantowy. W Visual Basicu domyślnym
typem (nie zadeklarowanej) zmiennej jest właśnie typ Variant. W Turbo Pascalu namiastkę zmiennych wariantowych
stanowiły parametry amorficzne (ang. untyped parameters) oraz rzutowanie typów (ang. typecasting) — konstrukcje
zaczerpnięte z języka Ada. O mechanizmie zmiennych wariantowych myśleli już w latach sześćdziesiątych (!) twórcy jednego
z pierwszych języków wysokiego poziomu — języka Algol 60 — lecz szybko uznali oni, że moc obliczeniowa współczesnych
komputerów nie byłaby w stanie zapewnić wystarczającej efektywności językowi implementującemu zmienne wariantowe
(przyp. tłum.).
niemal identyczna ze stosowaną w OLE, choć jej użyteczność wykracza daleko poza ów kontekst — oferując
wyjątkowo silne narzędzie programistyczne. W chwili obecnej Object Pascal jest jedynym całkowicie
kompilowalnym językiem implementującym zmienne wariantowe zarówno w aspekcie dynamicznej zmiany
typu w czasie wykonywania programu, jak i pod kątem pełnoprawnego typu danych w składniowej
i semantycznej konwencji kompilatora.
W Delphi 3 wprowadzono dodatkowo inny typ wariantowy — OLEvariant. Od swego pierwowzoru
(Variant) różni się zakresem reprezentowalnych typów, ograniczonym do typów wykorzystywanych przez
automatyzację OLE. W niniejszym rozdziale skoncentrujemy się głównie na typie Variant, a typ OLEvariant
będziemy przywoływać tylko w kontekście porównań ze swym pierwowzorem.
Zmienna typu Variant może podczas wykonywania programu przechowywać wartości całkowite,
zmiennoprzecinkowe, łańcuchy znaków, wartości boolowskie, znaczniki daty/czasu, kwoty pieniężne
(Currency) i obiekty automatyzacji OLE. Może ona również reprezentować tablicę heterogeniczną, tj. taką,
której rozmiary i typy elementów ulegają dynamicznej zmianie, w szczególności — tablicę, której elementy są
wskaźnikami do innych tablic wariantowych.
3
Nie należy mylić ze sobą dwóch bytów o zbliżonym nazewnictwie, lecz różnych znaczeniach: rekordu z częścią wariantową
(jakim jest rekord TVarData) oraz zmiennych wariantowych. Część wariantowa rekordu jest pascalowym odpowiednikiem
unii języka C, w ramach której kilka różnych danych zajmuje ten sam obszar pamięci. Jedynym związkiem wspomnianego
rekordu i zmiennej wariantowej jest prezentowana struktura.
varUnknown: (VUnknown: Pointer);
varShortInt: (VShortInt: ShortInt);
varByte: (VByte: Byte);
varWord: (VWord: Word);
varLongWord: (VLongWord: LongWord);
varInt64: (VInt64: Int64);
varString: (VString: Pointer);
varAny: (VAny: Pointer);
varArray: (VArray: PVarArray);
varByRef: (VPointer: Pointer);
);
1: (VLongs: array[0..2] of LongInt);
);
2: (VWords: array [0..6] of Word);
3: (VBytes: array [0..13] of Byte);
end;
Jak łatwo policzyć, zmienna wariantowa zajmuje 16 bajtów pamięci. Pierwsze dwa bajty (VType) określają
aktualny typ zawartości zmiennej:
varEmpty = $0000; { vt_empty }
varNull = $0001; { vt_null }
varSmallint = $0002; { vt_i2 }
varInteger = $0003; { vt_i4 }
varSingle = $0004; { vt_r4 }
varDouble = $0005; { vt_r8 }
varCurrency = $0006; { vt_cy }
varDate = $0007; { vt_date }
varOleStr = $0008; { vt_bstr }
varDispatch = $0009; { vt_dispatch }
varError = $000A; { vt_error }
varBoolean = $000B; { vt_bool }
varVariant = $000C; { vt_variant }
varUnknown = $000D; { vt_unknown }
//varDecimal = $000E; { vt_decimal } {nie obsługiwane}
{ undefined $0f } {nie obsługiwane}
varShortInt = $0010; { vt_i1 }
varByte = $0011; { vt_ui1 }
varWord = $0012; { vt_ui2 }
varLongWord = $0013; { vt_ui4 }
varInt64 = $0014; { vt_i8 }
//varWord64 = $0015; { vt_ui8 } {nie obsługiwane}
Jak łatwo zauważyć, nie istnieje możliwość reprezentowania w zmiennej wariantowej wskaźników ani obiektów.
Wskazówka
Nie to jest jednak najważniejsze; jak zobaczymy za chwilę, bezpośrednie operowanie polami struktury
TVarData niesie ze sobą niebezpieczeństwo dezorganizacji zarządzania pamięcią.
Wszystko odbywa się tu automatycznie — programista nie musi się martwić o zarządzanie pamięcią na potrzeby
łańcucha reprezentowanego przez zmienną V. Delphi, realizując powyższą sekwencję, nadaje początkowo
zmiennej wariantowej wartość nieokreśloną (Unassigned). Następnie przypisuje polu VType identyfikator
varString, natomiast do pola VString kopiuje wskaźnik do łańcucha reprezentowanego przez S, jednocześnie
zwiększając jego licznik odwołań. Kiedy zmienna V zakończy swój czas życia (to znaczy — gdy zakończy się
wykonywanie procedury ShowVariant), łańcuch ten jest traktowany tak, jak gdyby był reprezentowany przez
„zwykłą” zmienną łańcuchową — jego licznik odwołań jest zmniejszany o 1, a jeżeli osiągnie przez to wartość
zero, zwalniana jest cała pamięć przydzielona łańcuchowi. Można to przedstawić poglądowo jako zanurzenie
całej procedury ShowVariant() w wyimaginowanym bloku try…finally:
Z podobnym, choć trochę bardziej złożonym przypadkiem automatycznego zwalniania zasobów, mamy do
czynienia w sytuacji zmiany aktualnego typu zmiennej wariantowej — z łańcuchowego na inny, na przykład:
To, co dzieje się podczas realizacji powyższej sekwencji, można by zapisać następująco (instrukcje wyróżnione
kursywą niekoniecznie są poprawnymi konstrukcjami Object Pascala):
Procedure ChangeVariant(S:String; I:Integer);
var
V: Variant;
begin
V := Unassigned;
try
// skojarz zmienną V z łańcuchem S
V.Vtype := varString;
V.VString := S;
// zwiększ licznik odwołań łańcucha S
Inc(S.RefCount);
ShowMessage(V);
V.VType := varInteger;
V.VInteger := I;
finally
zwolnij zasoby przydzielone dla zmiennej V
end;
end;
Powyższy schemat mógłby stanowić inspirację do wykonania całego scenariusza za pomocą bezpośredniego
operowania na polach struktury TVarData:
Procedure ChangeVariant(S:String; I:Integer);
var
V: Variant;
begin
V := S;
ShowMessage(V);
TVarData(V).VType := varInteger;
TVarData(V).VInteger := I;
end;
I tu właśnie kryje się pułapka: w powyższym kodzie nie istnieje bowiem miejsce, w którym kompilator mógłby
stwierdzić, iż zerwany zostaje związek zmiennej V z łańcuchem S (struktura TVarData nie jest traktowana w
żaden szczególny sposób). W efekcie licznik odwołań łańcucha S nie zostanie zmniejszony i zarządzanie
pamięcią na jego potrzeby ulegnie pewnemu zachwianiu.
Wniosek — należy unikać operowania wprost na strukturze TVarData.
Również wartość każdego ze wspomnianych typów może być w sposób jawny przypisana zmiennej
wariantowej, przykładowo
V := 2;
V := 1.6;
V := 'Hello';
V := TRUE;
I vice versa — zmienna wariantowa może być obsadzana w roli dopuszczalnych typów, na przykład:
V := 1.6;
S := String(V); // S zawiera wartość '1.6'
I := Integer(V); // I zawiera wartość 2 jako zaokrąglenie 1,6
// do najbliższej liczby całkowitej
B := Boolean(V); // B zawiera wartość TRUE
D := Double(V); // D zawiera wartość 1.6
lecz i to nie jest konieczne, ponieważ powyższe konstrukcje można by równie dobrze zapisać jako:
V := 1.6;
S := V; // S zawiera wartość '1.6'
I := V; // I zawiera wartość 2 jako zaokrąglenie 1,6
// do najbliższej liczby całkowitej
B := V; // B zawiera wartość TRUE
D := V; // D zawiera wartość 1.6
Zmienne wariantowe w wyrażeniach
Zmienne zadeklarowane jako Variant mogą być argumentami następujących operatorów: +, –, =, *, /, div,
mod, shl, shr, and, or, xor, not, :=, <>, <, >, <= i >=. Znaczenie danego operatora może być uzależnione
od bieżącej zawartości zmiennych wariantowych stanowiących jego argumenty — np. operator + może
oznaczać dodanie dwóch liczb albo konkatenację łańcuchów. Jeżeli argumenty operacji różnią się pod
względem typu, Delphi przeprowadza konwersję na wspólny typ, którym jest typ „silniejszy” — ranking
„siły” poszczególnych typów przedstawia się następująco:
• double
• integer
• string
Może to niekiedy prowadzić do zaskakujących rezultatów (zaskakujących, jeśli nie zna się powyższej reguły).
Spójrzmy na poniższy przykład:
var
V1, V2, V3 : Variant;
begin
V1 := '100'; // łańcuch
V2 := '50'; // łańcuch
V3 := 200; // liczba całkowita
V1 := V1 + V2 + V3;
end;
Po wykonaniu powyższej sekwencji wartością zmiennej V1 jest nie 350 (jak mogłoby się niektórym wydawać),
lecz 10250.0. Istotnie: pierwsza operacja — V1 + V2 — jest konkatenacją łańcuchów, a jej wynikiem jest
'10050'. Kolejna operacja jest dodawaniem łańcucha '10050' do liczby 200 — zgodnie z przedstawionym
rankingiem łańcuch '10050' konwertowany jest na liczbę całkowitą 10050, ta zaś dodawana jest do liczby
(całkowitej) 200, co daje w wyniku (uwaga!) liczbę rzeczywistą4 (double) 10250.0.
Oczywiście nie każda operacja na zmiennych wariantowych jest wykonalna. W poniższej sekwencji
var
V1, V2: Variant;
begin
V1 := 77;
V2 :='Hello';
V1 := V1 / V2;
end;
Delphi spróbuje skonwertować zawartość zmiennej V2 na postać liczbową (integer lub double), co
oczywiście jest niewykonalne; w efekcie otrzymamy wyjątek EVariantError z komunikatem Invalid variant
type conversion5.
Niekiedy celowe może okazać się jawne konwertowanie zawartości zmiennej wariantowej na wskazany typ.
Operacja ta sprawia, że kod wynikowy jest bardziej zwięzły i poprawia efektywność jego wykonywania —
poniższa sekwencja
V4 := V1 * V2 / V3;
Należy także zauważyć, iż w drugim przypadku mamy do czynienia z zaokrągleniami zawartości V1 i V3.
4
Mogłoby się wydawać, iż wspólnym typem powinien być w tym przypadku integer i wynik powinien być liczbą
całkowitą. Nieprzypadkowo jednak wspólnym typem łańcucha i liczby całkowitej jest double, nie integer — w
przeciwnym razie niewykonalne byłyby tak oczywiste obliczenia, jak np. dodanie łańcucha '3.7' do liczby 2 (wynik
takiego dodawania to liczba 5.7) (przyp. tłum.).
5
Sytuacja nie zmieniłaby się, gdyby zamiast operatora / wystąpił operator +; wbrew pozorom Delphi nie zinterpretowałoby
prezentowanej operacji jako konkatenacji łańcuchów, bowiem wspólnym typem łańcucha i liczby całkowitej jest double,
nie string (przyp. tłum.).
Jest rzeczą oczywistą, że zmienne wariantowe są wyraźnym odstępstwem od zasady bezpieczeństwa typów. To
prawda, jednak bez nich wykorzystanie mechanizmu OLE byłoby jedynie iluzją. Zresztą, są one również
użyteczne w zastosowaniach o wiele bardziej banalnych, na przykład:
Var
V1, V2 : Variant;
L : Word;
.............
if L < 0 Then
begin
V1 := 'brakuje ';
V2 := L;
end
Else if L > 0 then
begin
V1 := 'nadmiar ';
V2 := L;
end
else
begin
V1 := 'nie brakuje ';
V2 := 'żadnych';
end;
V1 := V1 + V2 + ' pozycji';
Po wykonaniu powyższego fragmentu, zmienna V1 zawiera łańcuch stanowiący czytelny raport na temat
ewentualnych braków czy nadmiaru w wykazie.
Ostrzeżenie
Mimo wielkiej użyteczności zmiennych wariantowych — wykorzystuje je biblioteka VCL, korzystają z nich
kontrolki ActiveX — ich elastyczność stanowi jednocześnie pułapkę dla wygodnego programisty. Wrażenie,
że deklarowanie zmiennych jako Variant wszędzie, gdzie tylko się da, ułatwi mu życie, jest złudne; gdy
przyjdzie do testowania programu, prawdopodobne trudności w znalezieniu przyczyny ewentualnego błędu
stanowić będą zbyt wysoką cenę za wygodnictwo (i, być może, fałszywie pojętą elastyczność kodu). Ponadto,
ze względu na znacznie bardziej skomplikowany sposób obsługi zmiennych wariantowych, wydłuża się kod
programu i spada jego ogólna efektywność. Zalecamy więc rozsądnie używać zmiennych wariantowych.
Tablice wariantowe
Wspominaliśmy przed chwilą, iż jedną z wartości reprezentowanych przez zmienną wariantową może być
wskazanie na (być może heterogeniczną) tablicę. Poniższy fragment programu
var
V: variant;
6
Chyba że drugi operand posiada wartość UNASSIGNED (przyp. tłum.).
I, J : Integer;
begin
J := 1
I := V[J];
...
jest syntaktycznie poprawny i kompiluje się bezbłędnie, ale próba jego wykonania skończy się niepowodzeniem,
ponieważ zmienna V nie reprezentuje aktualnie żadnej tablicy. W celu utworzenia tablicy wariantowej można
skorzystać z jednej z dwu przeznaczonych do tego funkcji Object Pascala: VarArrayCreate() lub
VarArrayOf().
Funkcja VarArrayCreate()
Funkcja VarArrayCreate() deklarowana jest w module System w taki oto sposób:
Function VarArrayCreate( const Bounds: array of Integer; VarType: Integer):
Variant;
Funkcja ta tworzy tablicę wariantową na podstawie zadanych par indeksów granicznych i wskazanego typu
elementów. Pary indeksów granicznych są zawartością tablicy przekazanej jako pierwszy parametr (notabene
parametr ten stanowi przykład tablicy otwartej — tablicami otwartymi zajmiemy się w dalszej części rozdziału),
natomiast drugi parametr identyfikuje typ elementów tablicy (w konwencji wartości wpisywanych w pole
VType struktury TVarData).
Oto prosty przykład utworzenia jednowymiarowej tablicy o czterech elementach typu Integer:
Var
V: Variant;
begin
V := VarArrayCreate( [1 , 4], varInteger );
...
V[1] := 1;
V[2] := 3;
V[3] := 5;
V[4] := 7;
...
Z kolei poniższy przykład ilustruje tworzenie macierzy jednostkowej o wymiarze 10×10, zawierającej elementy
typu Double; na przekątnej macierzy wpisywane są jedynki, poza przekątną — zera:
// © A.Grażyński
Const
VDim = 10;
Var
V: Variant;
i, j : Integer
begin
V := VarArrayCreate( [1 , VDim, 1, VDim], varDouble );
For i := 1 to VDim do
begin
For j := 1 to VDim do
begin
V[i, j] := (I div J) * (J div I); // 1, gdy i=j, 0 gdy i<>j
end;
end;
end;
Oczywiście nic nie stoi na przeszkodzie, aby elementy tablicy same były zmiennymi wariantowymi, w
szczególności — zawierały wskazanie na tablice wariantowe! Umożliwia to tworzenie tablic wyższego rzędu,
posiadających ciekawą cechę nieortogonalności. Tablicę nazywamy ortogonalną, jeśli da się ona przedstawić jako
wektor zmiennych jednakowego typu — i tak, np. tablica array [ 1 .. 10 ] of real jest 10-elementowym
wektorem zmiennych typu real, macierz array [ 1 .. 5, 2 .. 20 ] of integer może być
rozpatrywana bądź jako 5-elementowy wektor tablic array [ 2 .. 20 ] of integer, bądź 19-elementowy
wektor tablic array [ 1 .. 5 ] or integer. Ogólnie rzecz biorąc, każda „zwykła” tablica pascalowa jest
tablicą ortogonalną, lecz tablice wariantowe wcale nie muszą posiadać tej cechy. Oto prosty przykład — poniższa
sekwencja tworzy trójkątną tablicę stanowiącą „górny” trójkąt macierzy 10×10 elementów typu Double:
// © A.Grażyński
var
V : Variant;
i : integer;
const
VDim = 10;
begin
V := VarArrayCreate ( [1 , VDim], varVariant);
for i := 1 to VDim do
begin
V[i] := VarArrayCreate ( [1 , VDim-i+1], varDouble);
end;
…
Funkcja VarArrayOf()
Funkcja VarArrayOf() deklarowana jest w module System następująco:
Function VarArrayOf(const Values: array of Variant): Variant;
i — jak łatwo się domyślić — służy do zgrupowania w jednowymiarową tablicę wariantową wartości
stanowiących kolejne elementy wektora podanego jako parametr. Po wykonaniu poniższej instrukcji
V := VarArrayOf( [ 1, 'Delphi', 2.2] );
V[1] zawiera liczbę całkowitą 1, V[2] zawiera łańcuch 'Delphi', natomiast V[3] jest liczbą rzeczywistą o
wartości 2.2. Tablica utworzona w ten sposób może więc być tablicą heterogeniczną, tj. posiadającą elementy
różnych typów.
Funkcja VarIsArray() dokonuje prostego sprawdzenia, czy przekazany parametr jest tablicą wariantową:
function VarIsArray(const A: Variant): Boolean;
begin
Result := TVarData(A).VType and varArray <> 0;
end;
Funkcja VarArrayDimCount() zwraca liczbę wymiarów tablicy, natomiast dolną oraz górną granicę każdego
wymiaru poznać można dzięki funkcjom VarArrayLowBound() i VarArrayHighBound().
Procedura VarArrayRedim() umożliwia zmianę górnej granicy najwyższego w hierarchii wymiaru tablicy —
to ten wymiar, który identyfikowany jest przez ostatni (skrajny, prawy) indeks. Istniejące elementy tablicy
zostają zachowane, ewentualne nowe elementy otrzymują wartości o reprezentacji zerowej.
Funkcja VarArrayRef() otrzymując tablicę wariantową, tworzy zmienną wariantową zawierającą wskazanie
na tę tablicę. Ta dziwna na pozór czynność podyktowana została potrzebami wynikającymi z użytkowania
serwerów automatyzacji OLE, które wymagają tablicy wariantowej w takiej właśnie postaci.
function VarArrayRef(const A: Variant): Variant;
begin
if TVarData(A).VType and varArray = 0
then
Error(reVarNotArray);
_VarClear(Result);
TVarData(Result).VType := TVarData(A).VType or varByRef;
if TVarData(A).VType and varByRef <> 0
then
TVarData(Result).VPointer := TVarData(A).VPointer
else
TVarData(Result).VPointer := @TVarData(A).VArray;
end;
Jeżeli więc, na przykład, VA oznacza tablicę wariantową, to wywołanie dowolnej funkcji API związanej z
serwerem powinno mieć postać
Server.PassvariantArray(VarArrayRef(VA));
Następuje tutaj wykonanie kolejno 10000 przypisań, z których każde jest dość kosztowne, wymaga bowiem
wykonania dosyć skomplikowanych kontroli i obliczeń, wynikających m.in. ze złożonej struktury samej tablicy
V. Okazuje się, iż możliwa byłaby znaczna redukcja tych operacji, gdyby założyć, iż w trakcie owych 10000
przypisań tablica nie zmienia swej struktury ani położenia w pamięci. Taką właśnie rolę pełni funkcja
VarArrayLock() — „zablokowuje” tablicę w tym sensie, iż do czasu jej „odblokowania” za pomocą funkcji
VarArrayUnlock() niedopuszczalne jest wywołanie w stosunku do niej funkcji VarArrayRedim().
Operowanie na zablokowanej tablicy wariantowej redukuje znacznie liczbę wykonywanych weryfikacji i tym sa-
mym zwiększa ogólną efektywność programu. Dodatkową użyteczną informację niesie wynik funkcji
VarArrayLock(): jest on wskaźnikiem do fizycznego wektora elementów w pamięci operacyjnej, dzięki czemu
możliwe jest wykonywanie pewnych operacji „na skróty” — w tym konkretnym przypadku kopiowanie
elementów może zostać wykonane przez jedno wywołanie procedury Move():
var
V: Variant;
A: Array [ 1 .. 10000] of byte;
P: Pointer;
....
wskaźnik P powinien być traktowany tak, jak gdyby wskazywał na tablicę postaci
array [ 6 .. 30, 2 .. 50, 1 .. 100 ] of byte
Procedura VarCast() dokonuje konwersji zawartości zmiennej wariantowej na wskazany typ, zapisując wynik
w innej zmiennej wariantowej:
procedure VarCast(var Dest: Variant; const Source: Variant; VarType: Integer);
Funkcja VarType() zwraca typ zmiennej wariantowej, a dokładniej — zawartość pola VType struktury
TVarData „nałożonej” na tę zmienną:
function VarType(const V: Variant): Integer;
asm
MOVZX EAX,[EAX].TVarData.VType
end;
Funkcja VarAsType() jest bliźniaczą siostrą procedury VarCast(), zwraca bowiem zmienną wariantową
stanowiącą rezultat konwersji argumentu na zadany typ:
function VarAsType(const V: Variant; VarType: Integer): Variant;
begin
_VarCast(Result, V, VarType);
end;
Podobne zadanie spełnia funkcja VarIsNull(), sprawdzająca, czy zmienna wariantowa reprezentuje wartość
NULL:
function VarIsNull(const V: Variant): Boolean;
begin
Result := TVarData(V).VType = varNull;
end;
Funkcja VarToStr() tworzy znakową reprezentację zmiennej wariantowej, przy czym wartości varNull
odpowiada łańcuch pusty:
function VarToStr(const V: Variant): string;
begin
if TVarData(V).VType <> varNull
then
Result := V
else
Result := '';
end;
Zwróć uwagę na ciekawy fakt, iż zasadnicza konwersja dokonywana jest tu automatycznie przez podprogramy
biblioteki RTL, uruchamiane w wyniku pojedynczej instrukcji przypisania
Result := V;
Typ OLEvariant
Typ ten jest niemal identyczny z typem Variant — różnica sprowadza się do niemożności reprezentowania
przez jego zmienne typów niekompatybilnych z mechanizmem automatyzacji OLE. Wyjątkiem jest typ
AnsiString, reprezentowany przez wartość varString w polu VType — przypisanie go do zmiennej typu
OleVariant spowoduje uprzednią jego konwersję na typ BSTR, w wyniku czego pole VType posiadać będzie
wartość varOleStr, zaś pole VOleStr wskazywać będzie na łańcuch typu BSTR (czyli łańcuch znaków
WideChar zakończony zerowym ogranicznikiem).
Typ Currency
Ten typ pojawił się po raz pierwszy w Delphi 2 i z założenia przeznaczony jest do przechowywania liczb
rzeczywistych reprezentujących wielkości, co do których wymagana jest bezwzględna dokładność — głównie
kwot pieniężnych. Wewnętrzną jego reprezentacją jest 64-bitowa liczba całkowita ze znakiem, zawierająca
wartość 10000 razy większą niż wartość faktycznie reprezentowana — na przykład dla liczby 4,67 wartość ta
równa jest 46700. Jest to więc typ rzeczywisty stałoprzecinkowy — notabene jedyny tego rodzaju typ w Object
Pascalu — zapewniający dokładność czterech cyfr dziesiętnych i maksymalną wartość bezwzględną (263 –
1)/ 10000 = 922337203685477.5807.
Przy przenoszeniu aplikacji z Delphi 1 wskazane jest przeanalizowanie danych i „przeprogramowanie” na postać
typu Currency danych finansowych reprezentowanych dotychczas przez typy zmiennoprzecinkowe Real,
Single, Double i Extended.
Tablice
Tablice stanowią uporządkowany ciąg (a właściwie — wektor) zmiennych tego samego typu. Typem elementu
tablicy może być dowolny typ, również zdefiniowany przez użytkownika. Poniższa deklaracja definiuje tablicę
ośmiu liczb całkowitych:
Type
Int8Arr = array [ 0 .. 7 ] of integer;
Od tej chwili typ Int8Arr staje się pełnoprawnym typem danych, a więc jest możliwe definiowanie zmiennych
tego typu:
Var
A : Int8Arr;
Wskazówka
Na szczególną uwagę — gdy chodzi o indeksowanie — zasługują tablice znakowe (array […] of Char);
deklarowane z zerową wartością dolnego indeksu, stają się kompatybilne z typem PChar (było tak już w wersji
7.0 Turbo Pascala). Wskazane jest zatem ich deklarowanie z zerową graniczną wartością indeksu, jeżeli nie
sprzeciwiają się temu inne względy projektowe.
Repertuar tablic w Object Pascalu nie ogranicza się do tablic jednowymiarowych. Możliwe jest deklarowanie
tablic o większej liczbie wymiarów; deklaracje poszczególnych par indeksów granicznych oddzielone są od
siebie przecinkami, na przykład:
var
G: array [ 1 .. 3, 4 .. 656, –10 .. 10 ] of byte;
Tablice dynamiczne
Tablice dynamiczne pojawiły się po raz pierwszy w Delphi 4. Deklaracja tablicy dynamicznej definiuje liczbę jej
wymiarów i typ elementów, jednak nie definiuje a priori indeksów granicznych — „rozpiętości” poszczególnych
wymiarów określone zostaną dopiero w trakcie wykonywania programu. Zajmijmy się na początek jednowymia-
rowymi tablicami dynamicznymi, za chwilę natomiast uogólnimy rozważania na tablice wielowymiarowe.
Oto przykładowa deklaracja jednowymiarowej tablicy dynamicznej:
var
A: array of string;
Deklaracja ta definiuje zmienną A jako wektor łańcuchów tekstowych, nie określając jednakże rozmiaru tego
wektora. Określenie tego rozmiaru, i jednocześnie przydzielenie odpowiedniej ilości pamięci, wykonywane jest
przez funkcję SetLength():
Readln(N);
...
SetLength(A, N);
Ostatnia z powyższych instrukcji ustala rozmiar tablicy A na N elementów (biorąc oczywiście pod uwagę bieżącą
wartość zmiennej N).
Dolną wartością graniczną indeksu tablicy dynamicznej jest zawsze zero, toteż indeksami granicznymi wektora
A będą wartości 0 oraz N-1; innymi słowy, jeśli w czasie wykonania instrukcji SetLength(…) wartością N
było (powiedzmy) 5, to od tej pory tablicę A wykorzystywać można na równi ze „statyczną” tablicą
zadeklarowaną jako
array [ 0 .. 4 ] of string;
Opóźniona deklaracja wielkości wymiaru (wymiarów) tablicy dynamicznej nie jest jednakże jedyną istotną
cechą odróżniającą ją od tablic „statycznych”. Jej specyfika wiąże się również z dynamicznym przydziałem
pamięci, dokonującym się dopiero w momencie wywołania procedury SetLength(); fizyczną reprezentacją
zmiennej określającej tablicę dynamiczną (w tym wypadku — zmiennej A) jest wskaźnik.
Tablice dynamiczne należą ponadto do zmiennych o kontrolowanym czasie życia. Oznacza to, że po zakończeniu
czasu życia tablicy dynamicznej (która jest np. zmienną lokalną funkcji/procedury) przydzielona do niej pamięć
jest automatycznie zwalniana (ang. garbage-collected). Możemy też wymusić wcześniejsze wykonanie tej
czynności, podstawiając pod zmienną tablicową wartość NIL:
A := NIL; // zwolnienie pamięci przydzielonej dla tablicy dynamicznej A
Jest to zalecane szczególnie w odniesieniu do dużych tablic dynamicznych, których zawartość przestała już być
potrzebna.
Innym mechanizmem charakterystycznym dla tablic dynamicznych jest oszczędność gospodarowania pamięcią
na podstawie licznika odwołań (podobnie jak w przypadku łańcuchów AnsiString). Dwie tablice dynamiczne
o identycznej zawartości mają w rzeczywistości wspólną reprezentację pamięciową, a fakt jej współdzielenia jest
odzwierciedlany przez wartość licznika odwołań równą 2. Z tego faktu wynika pewna niespodzianka.
Przyjrzyjmy się poniższemu fragmentowi
var
A1, A2 : array of Integer;
begin
SetLength(A1, 4);
A2 := A1;
A1[0] := 1;
A2[0] := 26;
...
i zgadnijmy, co kryje się pod elementem A1[0]?
Poprawna odpowiedź brzmi: 26. Otóż przypisanie A2 := A1 jest de facto utożsamieniem tablic A1 i A2, a
wspomniana instrukcja dokonuje tylko przepisania wskaźnika oraz zwiększenia licznika odwołań. Każda zmiana
w obrębie tablicy A1 skutkować będzie identyczną zmianą w obrębie tablicy A2 i vice versa — ergo: przypisanie
A2[0] := 26 ustala wartość elementu A1[0] na 26.
Możliwe jest jednakże faktyczne powielenie tablicy dynamicznej — do tego celu służy funkcja standardowa
Copy(). Po wykonaniu poniższej sekwencji
var
A1, A2 : array of Integer;
begin
SetLength(A1, 4);
A2 := Copy(A1);
A1[0] := 1;
A2[0] := 26;
wycina z tablicy A1 elementy A1[2] i A1[3], tworząc z nich zawartość tablicy A2 — identycznie do poniższej
sekwencji:
SetLength(A2,2);
A2[0] := A1[2];
A2[1] := A1[3];
Wielowymiarowe tablice dynamiczne deklaruje się poprzez zagnieżdżanie klauzuli array of; oto przykład
tablicy dwuwymiarowej:
var
B: array of array of Integer;
Należy w tym miejscu zaznaczyć, iż możliwe jest tworzenie nieortogonalnych tablic dynamicznych (pojęcie
ortogonalności tablicy wyjaśnione zostało przy opisie tablic wariantowych). Jest taka możliwość, gdyż macierz
może być rozpatrywana jako wektor wektorów, które w przypadku tablicy dynamicznej (i tablic wariantowych)
nie muszą być identyczne. Poniższy przykład przedstawia tworzenie trójkątnej macierzy łańcuchów:
var
A : array of array of string;
I, J : Integer;
begin
SetLength(A, 10);
for I := Low(A) to High(A) do
begin
SetLength(A[I], I);
for J := Low(A[I]) to High(A[I]) do
A[I,J] := IntToStr(I) + ',' + IntToStr(J) + ' ';
end;
end;
Rekordy
Rekord — w przeciwieństwie do tablicy — nie ma charakteru struktury jednorodnej, lecz stanowi agregat
potencjalnie różnych typów. Odpowiednikiem pascalowego rekordu są: struktura języka C definiowana za
pomocą słowa kluczowego struct oraz typ definiowany (user-defined type) Visual Basica. Oto przykład
rekordu w języku Object Pascal oraz jego odpowiedniki w C i Visual Basicu:
7
Podobny efekt dał o sobie znać w momencie, gdy autorzy Delphi 1 zadecydowali, iż wszelkie obiekty reprezentowane będą
w programie przez wskaźniki do fizycznej reprezentacji. Od tej pory zwykła instrukcja przypisania pomiędzy zmiennymi
obiektowymi powoduje jedynie powielenie wskaźnika do istniejącego obiektu, zaś fizyczne powielenie reprezentacji
wykonywane jest w sposób jawny przez metodę Assign() (przyp. tłum.).
{ Pascal }
Type
MyRec = Record
i : integer;
d : double
end;
/* C */
typedef struct {
int i;
double d;
} MyRec;
Składowe rekordu nazywane są jego polami (fields), a odwołania do nich mają postać odwołań kwalifikowanych
— po nazwie zmiennej następuje kropka rozdzielająca i nazwa pola:
var
N : MyRec;
begin
N.i := 23;
N.d := 3.4;
end;
Aby uniknąć żmudnego powtarzania nazwy zmiennej (w odwołaniach kwalifikowanych), można użyć tzw.
instrukcji wiążącej with, powodującej, że odwołania do pól rekordu dotyczą konkretnej zmiennej. Oto
poprzedni przykład po zastosowaniu instrukcji wiążącej:
var
N : MyRec;
begin
with N do
begin
i := 23;
d := 3.4;
end;
end;
Rekord pascalowy może posiadać tzw. część zmienną, zwaną również częścią wariantową (uwaga: nie mylić ze
zmiennymi typu Variant!). Interpretacja części zmiennej rekordu może odbywać się na jeden ze
zdefiniowanych z góry sposobów. Znawcy języka C natychmiast rozpoznają w tym odpowiednik unii (union).
Oto przykład rekordu z częścią zmienną oraz jego odpowiednik w C++:
Type
TVariantRecord = record
NullStrField : PChar;
IntField : Integer;
Case Integer of
0 : (D: Double);
1 : (I: Integer);
2 : (C: Char);
End;
struct TUnionStruct
{
char * StrField;
int IntField;
union
{
double D;
int I;
char C;
};
};
Zgodnie z powyższą definicją, pola D, I oraz C zajmują ten sam obszar pamięci.
Część zmienna rekordu musi wystąpić na jego końcu. Nie ma przeciwwskazań, by w części zmiennej pojawiło
się pole będące rekordem zawierającym także część zmienną.
Wskazówka
Reguły Object Pascala zabraniają definiowania w zmiennej części rekordu pól będących zmiennymi o
kontrolowanym czasie życia.
Zbiory
Zbiory (sets) są konstrukcją unikatową, właściwą jedynie Pascalowi (chociaż C++Builder implementuje klasę-
szablon Set emulującą zbiory pascalowe). Zbiory oferują wyjątkowo efektywny mechanizm reprezentowania
kolekcji złożonych z elementów typów porządkowych, znakowych lub wyliczeniowych. Zbiory deklaruje się za
pomocą klauzuli set of, na przykład
type
TCharSet = set of Char;
Liczba elementów zbioru nie może przekraczać 256, natomiast numery porządkowe jego elementów (Ord())
nie mogą wykraczać poza przedział 0 ÷ 255. Z tego względu poniższe deklaracje są błędne:
TShortIntSet = Set of ShortInt;
// Ord(Low(ShortInt)) < 0
Każdy kandydat na element zbioru reprezentowany jest przez pojedynczy bit: jedynka oznacza przynależność do
zbioru, zero — brak elementu w zbiorze. Rozmiar zmiennej zbiorowej zależny jest więc od liczności (mocy)
typu, na bazie którego zbiór zdefiniowano — nie przekracza on więc nigdy 32 bajtów. W szczególności, zbiory
oparte na typach o mocy nie przekraczającej 32 elementów cechują się szczególną efektywnością — ich zmienne
nie przekraczają rozmiaru czterech bajtów, mogą więc być w całości ładowane do rejestrów procesora.
Stałe oznaczające zbiory zapisuje się w nawiasach prostokątnych jako ogranicznikach, na przykład:
Var
Robocze, Parzyste, Wolne, Happy: WeekDaysSet;
...
Robocze := [ Pn .. Pt ];
Parzyste := [ Wt, Cz, So ];
Wolne := [ So, Ni ];
Happy := [];
Operatory zbiorowe
Ideą typu zbiorowego jest odwzorowanie algebry zbiorów, co znajduje odzwierciedlenie w zestawie właściwych
temu typowi operatorów.
Do testowania relacji zawierania zbiorów służy operator <=. Zbiór A zawiera się w zbiorze B (co oznaczamy A
<= B), jeżeli każdy element zbioru A jest jednocześnie elementem zbioru B (niekoniecznie na odwrót).
var
X1, X2 : WeekDaysSet;
...
if X1 <= X2
Then
.....
Szczególnym przykładem tworzenia sumy zbiorów może być dołączanie elementu do zbioru:
var
C : Char;
ObtainedChars : Set of Char;
....
ObtainedChars := [];
......
ObtainedChars := ObtainedChars + C;
ReservedChars := ReservedChars - C;
Począwszy od wersji 7.0 Turbo Pascala dostępne są procedury Include() i Exclude() dokonujące dołączania
elementu do zbioru i wykluczania z niego elementu:
Include(ObtainedChars, C);
// to samo co:
// ObtainedChars := ObtainedChars + C;
oraz
Exclude(ReservedChars, C);
// to samo co:
// ReservedChars := ReservedChars - C;
Wskazówka
Należy używać procedur Include() i Exclude() wszędzie tam, gdzie jest to możliwe. Są one bowiem
realizowane w sposób niezwykle efektywny — za pomocą pojedynczej (!) instrukcji procesora, natomiast
realizacja sumy (różnicy) zbiorów wymaga 13 + 6n instrukcji (n oznacza tu liczbę bitów zajmowanych przez
zbiór).
Iloczyn zbiorów
Iloczyn zbiorów A i B — oznaczany A * B — tworzą te elementy, które należą jednocześnie do obydwu
zbiorów. Oto przykładowy test, czy dwa zbiory posiadają wspólne elementy:
var
A, B : set of integer;
...
if A * B <> []
Then
.....
Obiekty
Obiekty stanowią zasadniczy trzon Delphi oraz języka Object Pascal — w szczególności obiektami są wszystkie
komponenty wizualne. Obiekty podobne są do rekordów, mogą jednak dodatkowo zawierać — jako składowe
— procedury i funkcje. Ta uproszczona definicja nie oddaje w pełni sensu obiektu pascalowego (obiektom
poświęcony jest obszerny fragment w dalszej części tego rozdziału), jednak w tym miejscu interesują nas jedynie
podstawy składniowe obiektu jako jednego z elementów języka Object Pascal.
Ogólna postać definicji obiektu jest następująca:
Type
TPochodny = class(TMacierzysty)
JakiesPole : integer;
Procedure JakasProcedura;
End;
Choć obiekty C++ różnią się od obiektów Delphi, to ich deklaracja jest trochę podobna:
class TPochodny : public TMacierzysty {
int JakiesPole;
void jakasProcedura()
}
Procedury oraz funkcje stanowiące składowe obiektu nazywane są jego metodami (methods). Wewnątrz
deklaracji typu obiektowego znajduje się jedynie nagłówek metody, który musi być rozwinięty w tekście
programu; kompletna definicja metody zawiera — oprócz jej nazwy — nazwę typu obiektowego, którego
składową stanowi:
Procedure TPochodny.JakasProcedura;
begin
{ treść metody }
end;
Kropka stanowiąca separator odwołania kwalifikowanego podobna jest do operatora :: języka C oraz operatora
. (kropki) Visual Basica. Mimo iż wszystkie trzy języki posiadają obsługę klas, jedynie Object Pascal i C++
umożliwiają definiowanie nowych klas w sposób całkowicie zgodny z kanonami programowania obiektowego
(powrócimy do tej kwestii w dalszej części rozdziału).
Notatka
Obiekty języka C++ zostały zrealizowane w zupełnie inny sposób niż obiekty języka Object Pascal; ich
łączenie w ramach jednej aplikacji możliwe jest tylko w wyniku zastosowania specjalnych zabiegów (więcej
szczegółów znaleźć można w rozdziale 13. „Zaawansowane techniki programistyczne” książki „Delphi 4.
Vademecum profesjonalisty”). Wyjątkiem od tej zasady są obiekty C++Buildera deklarowane z użyciem dyrek-
tywy __declspec(delphiclass). Są one jednak niekompatybilne z „regularnymi” obiektami C++.
Wskaźniki
Wskaźniki (pointers) stanowią wskazanie na zmienną, znajdującą się w pamięci. Przykładem typu
wskaźnikowego jest poznany już typ PChar, stanowiący wskazanie na ciąg znaków (bądź na pierwszy znak
ciągu, zależnie od kontekstu). Każdy typ posiada swój odpowiednik wskaźnikowy (regułę tę należy stosować
rekurencyjnie — istnieją wskaźniki do wskaźników). W Pascalu występuje również tzw. wskaźnik amorficzny
(untyped pointer) stanowiący wskazanie na obszar pamięci operacyjnej bez związku z konkretnym typem i
deklarowany jako Pointer. Ponieważ stanowi on jednak odstępstwo od zasady bezpieczeństwa typów, nie
powinien być nadużywany.
Foo = record
Nazwisko : string;
Wiek : byte;
end;
PFoo = ^Foo;
{ Typ PFoo wskazuje na typ Foo }
var
P: Pointer {wskaźnik amorficzny}
P2: PFoo; {wskaźnik rekordu typu Foo}
Notatka
Należy zaznaczyć, że wartością wskaźnika jest adres pamięci zajmowanej przez odnośną zmienną. Ewentualna
alokacja wskazywanej przez wskaźnik pamięci leży całkowicie w gestii programisty. Możliwe jest uwolnienie
wskaźnika od jakiegokolwiek wskazania; wartością „nie wskazującą na nic” jest w Pascalu wartość NIL (w
Delphi 2 i następnych dodatkowo NULL). Reprezentacją takiego „pustego” wskaźnika nie są binarne zera.
Odwołanie się do wskazywanej zmiennej następuje przez użycie operatora ^ (zwanego operatorem dereferencji).
Najlepiej wyjaśnić to na przykładach:
Program PtrTest;
Type
MyRec = record
I : Integer;
S : String;
R : Real;
End;
PMyRec = ^MyRec;
Var
Rec : PMyRec;
begin
New(Rec); {tworzy dynamiczny rekord typu MyRec,
zmienna Rec zawiera wskazanie na niego}
Rec^.I := 10;
Rec^.S := 'Coś dla odmiany ... '
Rec^.R := 6. 384;
......
<ramka>
Jak alokować i zwalniać pamięć?
Jeżeli przydzielasz pamięć dla zmiennej ściśle określonego typu, zawsze używaj procedury New().
Gwarantuje to przydzielenie pamięci w ilości odpowiedniej do typu struktury. Nie zapomnij o zwolnieniu tak
przydzielonej pamięci za pomocą procedury Dispose().
Odwołanie się do pamięci poza przydzielonym obszarem jest błędem i najczęściej kończy się wyjątkiem
(Access Violation), jednak na szczęście zasada bezpieczeństwa typów redukuje znacznie
prawdopodobieństwo takiego zjawiska; jego niebezpieczeństwo wzrasta jednak, gdy używamy wskaźników
amorficznych.
<ramka>
Z typami wskaźnikowymi wiąże się bardzo ciekawa osobliwość Turbo Pascala dotycząca zgodności typów
(mogąca przyprawić o ból głowy programistów przyzwyczajonych do języka C): otóż identyczna deklaracja
dwóch typów nie jest jeszcze gwarancją ich zgodności (w sensie reguł kompilatora). Oto typowy przykład:
var
a : ^integer;
b : ^integer;
begin
.....
a := b; //tu kompilator zgłosi błąd
Dla programistów „wychowanych” na języku C stanowi to nie lada zaskoczenie — wszak zgodnie z deklaracją
int *a;
int *b;
begin
.....
a := b; {tu wszystko jest w porządku}
Aliasy typów
Object Pascal umożliwia definiowanie typów równoważnych typom już zdefiniowanym — służy do tego prosta
dyrektywa zrównania typów, np.
Type
Numerki = Integer;
Od tej chwili typ Numerki nie będzie się dla kompilatora różnił od typu Integer.
8
Procedura New(P) jest równoważna GetMem(P, SizeOf(P^)), natomiast Dispose(P) odpowiada
FreeMem(P,SizeOf(P^). Nie dotyczy to jednak w pełni typów obiektowych, chociaż stosowanie do nich procedur
New() i Dispose() z jednym parametrem również nie jest typowe (przyp. tłum.).
Nowością Delphi, niedostępną w Turbo Pascalu są tzw. aliasy typów (strongly typed aliases) oznaczające
zgodność, lecz nie identyczność typów.
Deklaracja aliasu ma postać:
Type
nowy_typ = type typ_bazowy;
Jej konsekwencją jest wzajemna zgodność obydwu typów w sensie przypisania. Na przykład w wyniku poniższej
deklaracji
Type
Licznik = type Integer;
Var
L : Licznik;
K : Integer;
obydwa przypisania
L := K;
...
K := L;
są poprawne.
Typy Licznik i Integer nie są jednak w rozumieniu składni Object Pascala typami identycznymi, co czyni je
niezgodnymi w kontekście przekazywania do procedur i funkcji parametrów opatrzonych klauzulami var i out.
Poniższe fragmenty zostaną więc przez kompilator odrzucone:
procedure Dolicz(var X: Licznik);
begin
...
end;
var
L: Integer;
N: Licznik;
.......
Dolicz(L); // błąd
Verify(N); // błąd
var
L: Integer;
N: Licznik;
.......
Zalicz(L);
Granica(N);
Rozróżnialność pomiędzy typem bazowym a jego aliasem ma szczególne znaczenie w przypadku właściwości
klas, pozwala bowiem tworzyć odrębne edytory właściwości dla dwóch różnych typów o identycznej strukturze.
Zajmiemy się tą kwestią obszerniej w rozdziale 12.
Rzutowanie i konwersja typów
Rzutowanie typów (typecasting) stanowi jeden ze sposobów osłabienia rygorystycznej kontroli typów
wykonywanej przez kompilator. Nie każde naruszenie zasad bezpieczeństwa typów jest błędem — wszystko
zależy od tego, czy programista wie, co robi. Przeanalizujmy poniższy przykład:
var
c : Char;
b : Byte;
begin
...
c := 's';
b := c; { tu kompilator zaprotestuje}
Ostatnie przypisanie zostanie przez kompilator zakwestionowane, gdyż zmienne b i c są zupełnie różnych
typów. Intencją programisty było prawdopodobnie potraktowanie obszaru pamięci na dwa sposoby: raz jako
znaku, raz jako bajtu (przypomina to nieco zmienną część rekordu). Możemy to osiągnąć, nakazując
kompilatorowi, by potraktował zmienną c jak bajt9 — czyli rzutując tę zmienną na typ byte:
var
c : Char;
b : Byte;
begin
...
c := 's';
b := byte(c); { to kompilator zaakceptuje}
{ b zawiera kod znaku 's' }
Rzutowanie typów jest narzędziem bardzo silnym (a więc dla aplikacji potencjalnie niebezpiecznym), chociaż
stosowane właściwie, daje wyraźne korzyści. Jest ono w zasadzie tylko inną interpretacją bitowego wzorca
zmiennej, w szczególności — nie jest związane z żadnymi konwersjami. Niezrozumienie tego faktu prowadzi do
tworzenia bezsensownych konstrukcji, jak w poniższym przykładzie:
Var
k : integer;
s : single;
begin
s := 1.0;
k := integer(s);
W Delphi 6 ostatnia instrukcja jest niepoprawna; była ona poprawna jeszcze w Delphi 5, mimo to, nawet
wówczas naiwnością byłoby oczekiwać, że wartością zmiennej k po wykonaniu ostatniego przypisania jest 1 (w
rzeczywistości zmienna ta miałaby wartość 1065353216 — przyp. tłum.). Zmienne typu integer i single
przechowywane są w pamięci w zupełnie różny sposób i wartość ta stanowi wynik interpretacji wzorca zmiennej
typu single na modłę typu integer.
Intencją programisty było tu zapewne przekształcenie liczby zmiennoprzecinkowej na odpowiadającą jej liczbę
całkowitą. Takie przekształcenie — zwane konwersją typów — wykonywane jest w Object Pascalu w sposób
jawny, za pomocą funkcji standardowych, bądź też w sposób niejawny przez kompilator, na przykład:
Var
s : single;
k : integer;
begin
.....
s := 4.7;
k := Trunc(s); // przekształcenie jawne, k = 4
k := Round(s); // przekształcenie jawne, k = 5
.....
k := 3;
s := k; // przekształcenie ukryte, s = 3.0
Zakres i reguły przekształceń domyślnych są ściśle określone — wrócimy do tego w dalszej części książki.
9
O takiej samej reprezentacji bitowej (przyp. tłum.).
Rzutowanie typów obiektowych wymaga oddzielnego omówienia — zajmiemy się tym w dalszej części
rozdziału.
Notatka
Warunkiem koniecznym (lecz nie wystarczającym) wykonalności rzutowania typów jest identyczny rozmiar
zmiennej podlegającej rzutowaniu i typu docelowego.
Zasoby łańcuchowe
W Object Pascalu stałe tekstowe (łańcuchy) przechowywane są standardowo w kodzie aplikacji. Koncepcja ta
narodziła się jeszcze w Turbo Pascalu, a jej konsekwencją było z jednej strony odciążenie ograniczonego do 64
kB segmentu danych, z drugiej zaś — po pojawieniu się DPMI i sprzętowych mechanizmów ochrony — do-
datkowe zabezpieczenie stałych tekstowych przed modyfikacją, zamierzoną lub przypadkową. W środowisku
Win32, wobec rezygnacji z segmentowanego modelu pamięci, aktualna jest jedynie druga z wymienionych
przesłanek.
Wraz z Delphi 3 pojawił się alternatywny sposób przechowywania stałych tekstowych, mianowicie — w
zasobach łańcuchowych (string resources), umieszczanych przez kompilator w generowanym pliku zasobowym
*.RES. Nowością są oczywiście nie same zasoby łańcuchowe, lecz sposób ich integracji z kodem projektu. Otóż
łańcuchy przeznaczone do przechowania w takich szczególnych zasobach deklarowane są tak, jak zwykłe stałe
tekstowe, z tą różnicą, iż zamiast dyrektywy const występuje dyrektywa resourcestring, jak w poniższym
przykładzie:
resourcestring
HelloMsg = 'Hello';
WorldMsg = 'world';
var
String1: String;
begin
String1 := HelloMsg + ' ' + WorldMsg + '!';
Writeln(String1);
...
end;
Program wypisuje historyczne już dla Pascala powitanie Hello world!. Stałe tekstowe HelloMsg oraz WorldMsg
zdefiniowane zostały przy użyciu dyrektywy resourcestring; skutkuje to z jednej strony umieszczeniem ich
przez kompilator w generowanych zasobach i automatycznym dołączaniem tych zasobów do aplikacji, z drugiej
natomiast — automatycznym ich ładowaniem w czasie wykonania programu, za pomocą niejawnych wywołań
funkcji LoadString(). I to wszystko bez zaprzątania uwagi programisty.
Najważniejszą jednak korzyścią, płynącą z oddzielenia stałych tekstowych od kodu aplikacji, jest niewątpliwie
ułatwiona zmiana jej wersji językowej, która manifestuje się przede wszystkim w postaci wypisywanych
komunikatów (choć nie tylko). Łańcuchy deklarowane z użyciem dyrektywy resourcestring dołączane są do
pliku .EXE (lub .DLL) jako zasoby, zatem kwestia zmiany wersji językowej aplikacji sprowadza się wówczas do
wymiany tychże zasobów, bez konieczności ponownej kompilacji kodu źródłowego.
Instrukcje warunkowe
W kolejnych punktach przedstawimy dwie instrukcje warunkowe: instrukcję if oraz instrukcję wyboru case.
Zakładamy, że nie są one dla Czytelnika nowością, ograniczymy się do ich porównania z odpowiednikami w C i
Visual Basicu.
Instrukcja If
Instrukcja if umożliwia uzależnienie wykonania pewnej instrukcji od spełnienia określonego warunku. Oto
przykłady:
{ Pascal }
if x = 4
then
y := x;
/* C */
if ( x == 4 ) y = x;
Ostrzeżenie
Pamiętaj, że w Pascalu operatory boolowskie mają wyższy priorytet niż operatory porównania. Testując więc
koniunkcję czy alternatywę kilku warunków, nie zapomnij o ujęciu każdego z nich w nawiasy, na przykład:
Instrukcja uzależniona od spełnienia warunku, zwana instrukcją uwarunkowaną, może być instrukcją złożoną;
innymi słowy, warunek może wiązać — między „nawiasami pionowymi” begin i end — kilka instrukcji, jak w
poniższym przykładzie:
if x = 6 Then
begin
Cokolwiek;
ATerazCosInnego;
IJeszczeCos;
end;
Instrukcja wyboru
Instrukcja ta pozwala na wykonanie jednej instrukcji z podanego zestawu, na podstawie wartości wyrażenia
testowego, zwanego selektorem. Instrukcja wyboru może być zastąpiona „kaskadową” instrukcją if, lecz jest od
niej zdecydowanie zgrabniejsza i wygodniejsza:
case x of
100:
FunkcjaDlaSetki;
200:
SpecjalnieDla200;
300:
IteracjaDla300;
Else
begin
SytuacjaAwaryjna;
UstawIndeks;
end;
end;
Notatka
Selektor w instrukcji wyboru musi być wyrażeniem typu porządkowego (ordinal type), natomiast wartości
określające poszczególne warianty muszą być stałymi. Oznacza to, że instrukcja wyboru nie nadaje się np. do
testowania wariantów łańcuchowych.
switch (x)
{
case 100:
FunkcjaDlaSetki; break;
case 200:
SpecjalnieDla200; break;
case 300:
IteracjaDla300; break;
default:
{
SytuacjaAwaryjna;
UstawIndeks;
}
}
Pętle
Pętle służą do kontrolowanego powtarzania bloków instrukcji. Przedstawimy trzy różne rodzaje pętli w Object
Pascalu — w języku C istnieją podobne konstrukcje, w Visual Basicu są one nieco bardziej rozbudowane.
Pętla For
Typowym zastosowaniem pętli For jest wykonanie danego bloku instrukcji pewną — z góry określoną — liczbę
razy. Oto przykład obliczający sumę kwadratów pierwszych stu liczb nieparzystych:
Var
i, k, sum : integer;
begin
sum := 0;
for i := 1 to 100 do
begin
k := i + i - 1; // kolejna liczba nieparzysta
sum := sum + k * k;
end;
.....
end;
{
k = i + i - 1;
sum += k * k
}
...
}
i w Visual Basicu:
sum := 0;
For i = 1 to 100
k = i + i - 1;
sum = sum + k * k;
Next i
Notatka
Modyfikacja zmiennej sterującej pętli For, dozwolona (choć mocno problematyczna) w Delphi 1 oraz po-
przednich wersjach Pascala, począwszy od Delphi 2 jest wyraźnie zabroniona; bez tego ograniczenia
możliwości optymalizacji pętli przez kompilator byłyby mocno uszczuplone.
Pętla While...Do
Instrukcja pętli While…Do powoduje cykliczne powtarzanie instrukcji uwarunkowanej tak długo, jak długo
spełniony jest określony warunek. Warunek sprawdzany jest przed wykonaniem instrukcji uwarunkowanej, więc
możliwe jest, że instrukcja ta nie zostanie wykonana ani razu. Jednym z typowych zastosowań pętli While…Do
jest przetwarzanie kolejnych linii pliku tekstowego, aż do jego wyczerpania (warunek końca pliku Eof() jest
prawdziwy, jeżeli w pliku nie ma już żadnej linii do pobrania10).
Program CzytajTekst;
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
closeFile(F);
end.
Pascalowa pętla While…Do funkcjonuje w sposób analogiczny do pętli While w języku C i pętli Do While w
Visual Basicu.
Pętla Repeat...Until
W przeciwieństwie do instrukcji While...Do, warunek uzależniający wykonywanie bloku instrukcji
sprawdzany jest po jego wykonaniu, więc pętla zostaje wykonana przynajmniej raz. Poniższy przykład sumuje
kwadraty kolejnych liczb nieparzystych do momentu, kiedy wynik przekroczy wartość 10000:
Program Sumowanie;
{$APPTYPE CONSOLE}
var
liczba, sum: integer;
begin
sum := 0;
liczba := -1;
repeat
Inc(liczba, 2);
Inc(sum, liczba * liczba);
until sum > 10000;
writeln('Ostatnią uwzględnioną liczbą jest ', liczba);
end.
10
a nie po nieudanej próbie odczytania linii, dlatego musi być sprawdzany przed odczytem (przyp. tłum.)
Procedura Break()
Wywołanie procedury Break wewnątrz pętli While, Repeat lub For powoduje jej natychmiastowe
zakończenie. Jeżeli pętle są zagnieżdżone, następuje zakończenie najbardziej wewnętrznej pętli zawierającej
wywołanie procedury Break.
Oto fragment programu czytający i wypisujący zawartość pliku tekstowego aż do napotkania pustej linii:
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
Repeat
Readln(F,S);
Writeln(S);
Until S = '';
CloseFile(F);
End.
Program ten jednak załamie się, jeżeli plik nie będzie zawierał pustej linii — próba wykonania instrukcji
Readln po wyczerpaniu jego zawartości spowoduje wygenerowanie wyjątku. Należy więc sprawdzać warunek
Eof(F) przed wykonaniem tej instrukcji i gdy jest prawdziwy, przerywać pętlę repeat:
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
Repeat
if Eof(f)
then
Break;
Readln(F,S);
Writeln(S);
Until S = '';
CloseFile(F);
End.
Procedura Continue ()
Wywołanie procedury Continue wewnątrz pętli While, Repeat lub For powoduje porzucenie bieżącego
„cyklu” pętli i natychmiastowe przejście do sprawdzania jej warunku. Oto zmodyfikowany poprzedni przykład
— jeżeli odczytana linia zaczyna się od średnika, nie jest w ogóle wypisywana:
Program CzytajTekst;
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
Begin
AssignFile(F, 'PLIK.TXT');
Reset(F);
Repeat
if EOF(F)
Then
Break;
readln(F,S);
if Copy(S,1,1) = ';'
Then
continue; // to samo co "skok" do linii Until
writeln(S);
Until S = '';
CloseFile(F);
End.
Procedury i funkcje
Procedury i funkcje stanowią wydzielone części aplikacji, wykonujące ściśle określony, zamknięty blok
instrukcji. Dodatkowo, funkcja zwraca pod swą nazwą wartość określonego typu. W języku C istnieją wyłącznie
funkcje; odpowiednikiem procedury jest funkcja, która zwraca wynik nieznaczący (void). Przykłady użycia
funkcji i procedur przedstawia wydruk 2.1.
{$APPTYPE CONSOLE}
var
Num : Integer;
begin
Num := 23;
Ponad10(Num);
if Nieujemna(Num)
Then
Writeln ('Nieujemna.')
Else
Writeln ('Ujemna.');
end.
Na specjalny komentarz zasługuje zmienna Result wykorzystana w funkcji Nieujemna. Symbolizuje ona
wartość zwracaną przez funkcję. Gdy występuje po lewej stronie operatora przypisania — jest równoważna
nazwie funkcji. Nie można jej jednak zastąpić nazwą funkcji, gdy występuje po prawej stronie operatora
przypisania: wystąpienie w tym miejscu nazwy funkcji oznacza jej wywołanie (w tym wypadku rekursywne), zaś
wystąpienie zmiennej Result — jej wartościowanie. Poniższa funkcja jest całkowicie poprawna
function SignPower(X:Real; N:Integer):Real;
var
i: integer;
Y : real;
begin
Result := 1.0;
for i := 1 to Abs(N) do
begin
Result := Result * X;
end;
if N < 0
then
Result := 1.0/Result;
end;
Jeżeli jednak zamienimy zmienną Result na nazwę funkcji, otrzymamy kod błędny syntaktycznie, bo funkcja
SignPower wywoływana jest bez parametrów:
function SignPower(X:Real; N:Integer):Real;
var
i: integer;
Y : real;
begin
SignPower := 1.0;
for i := 1 to Abs(N) do
begin
SignPower := SignPower * X; // ta instrukcja jest błędna syntaktycznie
end;
if N < 0
then
SignPower := 1.0/ SignPower; // ta instrukcja jest błędna syntaktycznie
end;
Gdybyśmy wykonali podobny zabieg z funkcją bezparametrową, program wpadłby w nieskończoną rekurencję.
Ostrzeżenie
Nie należy mylić przypisania do zmiennej Result z instrukcją return języka C — ta ostatnia, wskazując
zwracaną wartość, powoduje jednocześnie zakończenie działania funkcji (podobnie jak pascalowa instrukcja
Exit); przypisanie wartości zmiennej Result jest natomiast tylko przypisaniem i może odbywać się w ciele
funkcji wielokrotnie.
Użycie zmiennej Result dopuszczalne jest tylko wtedy, gdy ustawiony jest przełącznik $X+ (lub zaznaczona
jest opcja Extended Syntax na karcie Compiler opcji projektu).
var
k: integer;
begin
k := 3;
TakaSobie(k);
{ wartość k wynosi teraz 4 }
....
Zasada bezpieczeństwa typów wymaga, by parametr aktualny przekazywany przez referencję był dokładnie tego
samego typu, co odpowiadający mu parametr formalny; w przeciwnym wypadku wystąpi błąd kompilacji.
Przekazywanie parametru przez zmienną nie powoduje obciążenia stosu, na którym „odkładany” jest jedynie
wskaźnik do parametru aktualnego — gdyż nie jest sporządzana kopia tego parametru.
W języku C++ odpowiednikiem pascalowego przekazywania parametru przez referencję jest przekazywanie
referencji parametru aktualnego (z użyciem operatora &).
11
Nie ma jednak gwarancji, iż do procedury (funkcji) faktycznie zostanie przekazany adres parametru — kompilator może
zastosować przekazanie przez wartość, jeśli uzna to za bardziej optymalne. Nie należy ponadto zapominać, iż parametrem
aktualnym może być w tym przypadku wyrażenie — obliczona wartość tego wyrażenia odkładana jest w tymczasowej
zmiennej roboczej i to właśnie adres tej zmiennej przekazywany jest do procedury (funkcji); może się tak stać nawet
wówczas, gdy wyrażenie to jest L-wyrażeniem (np. zmienną prostą). Jeżeli więc zdefiniujemy następującą funkcję
function AddrOfCParm(const X:SmallInt):pointer;
begin
result := @X;
end;
nie mamy gwarancji, że wywołanie AddrOfCParm(MyVar) zwróci adres zmiennej MyVar (przyp. tłum.).
12
Można jednak z łatwością oszukać kompilator, przekazując w zagnieżdżonym wywołaniu wskaźnik parametru
przekazanego przez stałą, jak w poniższym przykładzie:
Type
PMyRec = ^TMyRec;
TMyRec = record
a, b: integer
Deklaracja parametru przekazywanego przez stałą polega na poprzedzeniu jego nazwy słowem kluczowym
const:
type
TMyRecord = record
Counters: array[0..1000] of integer;
Labels: array[0..1000] of String[30];
end;
…
Procedure InitPivotElement ( const X : TMyRecord );
begin
with X do
begin
Counters[0] := 0; // ta instrukcja spowoduje błąd kompilacji
Labels[0] := 'Pivot'; // ta również
end;
end;
Należy przyjąć zasadę, że przekazanie parametru przez stałą jest sposobem najodpowiedniejszym w przypadku,
gdy parametr jest parametrem wejściowym (tzn. nie przekazuje informacji zwrotnej) i nie zamierzamy
wykorzystywać jego kopii lokalnej jako obszaru roboczego.
end;
W miejsce parametru X powyższych procedur można przekazać dowolną tablicę liczb typu integer, można też
wpisać zawartość tablicy explicite, za pomocą tzw. konstruktora tablicowego, wymieniającego kolejne jej
elementy, na przykład konstruktor
[1,2,3,4,5]
definiuje pięcioelementową tablicę, której elementami są początkowe liczby naturalne13. Ponieważ z punktu
widzenia składni konstruktor tablicowy jest stałą, nie można przekazać go przez referencję.
Aby w treści procedury (funkcji) poznać wielkość tablicy przekazanej jako parametr aktualny (w miejsce
parametru formalnego będącego tablicą otwartą), można wykorzystać funkcje Low() i High(). Ale uwaga: z
punktu widzenia procedury (funkcji), jej parametr, będący tablicą otwartą, postrzegany jest jako tablica
indeksowana od zera (zero-based array), tak więc funkcja Low() zwraca dla niej zawsze 0, zaś funkcja High()
— wartość mniejszą o 1 od faktycznej liczby elementów. Poniższy fragment programu wypisuje wartości 0 i 4:
…
procedure WypiszRozmiary(var X:array of real);
begin
writeln(Low(X),' ',High(X));
end;
…
var
Vector: array [3 .. 7] of Real;
…
WypiszRozmiary(Vector);
…
Oto jeszcze jeden przykład zastosowania tablicy otwartej — poniższa funkcja oblicza średnią arytmetyczną
elementów wektora:
Result := Result/(High(aVector)-Low(aVector)+1);
end;
13
Zwróć uwagę, iż konstruktor tablicowy może wyglądać tak samo jak zbiór (set), dlatego jedynym dozwolonym jego
zastosowaniem jest rola parametru aktualnego procedury (funkcji) — nie można użyć go w wyrażeniu ani w instrukcji
przypisania (przyp. tłum.).
Prawdziwą rewolucją w zakresie składni Pascala są z pewnością tablice heterogeniczne. Tablica heterogeniczna
to tablica zawierająca elementy różnych typów; nie da się jej zdefiniować jako typ, a jedynie zapisać w postaci
konstruktora tablicowego, na przykład:
Tablicy heterogenicznej nie można użyć w wyrażeniach; jedynym jej zastosowaniem jest rola parametru
aktualnego procedur i funkcji. Parametr formalny odpowiadający tablicy heterogenicznej deklarowany jest za
pomocą frazy array of const:
procedure CoMyTuMamy(A: array of const);
procedure ZobaczmyJeszczeTo (const A: array of const);
Mimo iż możliwe jest zadeklarowanie procedury (funkcji) z tablicą heterogeniczną przekazywaną przez
referencję
procedure ToTezCiekawe(var A: array of const);
to parametrem aktualnym odpowiadającym takiej deklaracji może być tylko inny parametr array of const,
na przykład:
W treści procedury (funkcji) liczbę elementów tablicy heterogenicznej możemy poznać za pomocą funkcji
High() — wszak jest to odmiana tablicy otwartej. Jeżeli natomiast chodzi o typy poszczególnych elementów, to
tablica heterogeniczna postrzegana jest wewnątrz procedury (funkcji) jako tablica o strukturze
array [ 0 .. … ] of TVarRec
Informację o typie elementu — czyli wariancie rekordu TVarRec odpowiadającym elementowi — zawiera pole
VType. Znaczenie poszczególnych jego wartości jest następujące:
vtInteger = 0;
vtBoolean = 1;
vtChar = 2;
vtExtended = 3;
vtString = 4;
vtPointer = 5;
vtPChar = 6;
vtObject = 7;
vtClass = 8;
vtWideChar = 9;
vtPWideChar = 10;
vtAnsiString = 11;
vtCurrency = 12;
vtVariant = 13;
vtInterface = 14;
vtWideString = 15;
vtInt64 = 16;
Zasięg deklaracji
Zasięg deklaracji (scope) jest pojęciem związanym z obowiązywaniem poszczególnych deklaracji w
poszczególnych fragmentach programu. I tak, zmienne globalne, deklarowane w programie głównym
(„projekcie”) widoczne są w całym programie, natomiast zmienne lokalne deklarowane w procedurze nie są
widoczne na zewnątrz niej. Oto prosty przykład:
var
ZmiennaGlobalna : Integer;
R : Real;
Procedure Przykladowa ( var R : Real );
var
ZmiennaLokalna : real;
begin
ZmiennaLokalna := 10.0;
R := R - ZmiennaLokalna;
end;
Na poziomie globalnym definiowane są tutaj trzy elementy: stała StalaGlobalna oraz zmienne
ZmiennaGlobalna i R. Procedura Przykladowa deklaruje w swym wnętrzu zmienną lokalną
ZmiennaLokalna — próba użycia tej zmiennej poza procedurą spowoduje błąd kompilacji. Procedura ta
deklaruje także parametr o nazwie R — ma on taką samą nazwę, jak jedna ze zmiennych globalnych i tym
samym przesłania tę ostatnią w treści procedury. Identyfikator R ma więc różne znaczenie wewnątrz procedury i
na zewnątrz niej.
Moduły
Moduły (units) stanowią podstawowe jednostki programu, grupujące deklaracje oraz procedury i funkcje,
osiągalne zarówno z programu głównego, jak i z poziomu poszczególnych modułów. Każdy moduł składa się —
obowiązkowo — z następujących elementów:
• Dyrektywa UNIT. Stanowi ona pierwszą linię modułu i zawiera jego nazwę poprzedzoną słowem unit.
Nazwa modułu musi być tożsama z nazwą pliku, w którym się on znajduje; moduł o nazwie BANKI musi
znajdować się w pliku BANKI.PAS.
• Część publiczna. Rozpoczyna się od dyrektywy interface i zawiera te części modułu (stałe, zmienne,
nagłówki procedur i funkcji itp.), które mają być widoczne dla innych modułów oraz programu głównego.
Zwracamy uwagę, że deklaracje procedur i funkcji w części publicznej ograniczają się jedynie do
nagłówków.
• Część prywatna. Rozpoczyna się od dyrektywy implementation, oznaczającej zarazem koniec części
publicznej, i zawiera te elementy modułu, które nie mają być widoczne na zewnątrz niego. Zawiera również
pełne teksty procedur oraz funkcji zadeklarowanych w części publicznej (można pominąć parametry, o ile
nie korzysta się z przeciążania i parametrów domyślnych — lecz nie jest to konieczne).
Ponadto, w module mogą opcjonalnie wystąpić następujące elementy:
• Część inicjacyjna. Rozpoczyna się od dyrektywy initialization, a kończy dyrektywą end, kończącą
równocześnie cały moduł, bądź też słowem finalization (patrz następny punkt). Instrukcje znajdujące
się w części inicjacyjnej wykonywane są jednokrotnie, podczas rozpoczynania pracy programu, a kolejność
wykonywania części inicjacyjnych poszczególnych modułów zależy od ich wzajemnego uzależnienia
wynikającego z dyrektyw uses (o tym za chwilę). Programy nie powinny uzależniać swego działania od
kolejności wykonywania części inicjacyjnych poszczególnych modułów, gdy kolejność ta może się zmienić
na skutek drobnej nawet modyfikacji któregoś z modułów.
• Część kończąca. Jeżeli w ogóle występuje, to jest ostatnią częścią modułu. Rozpoczyna się od dyrektywy
finalization a kończy dyrektywą end, kończącą zarazem cały moduł. Pojawiła się w Delphi 2,
zastępując znany z Turbo Pascala mechanizm tzw. funkcji kończących ExitProc (mechanizm ten istniał
jeszcze w Delphi 1, wspierany dodatkowo przez funkcję AddExitProc). Podobnie jak w przypadku części
inicjującej, nie należy zakładać żadnej określonej kolejności wykonywania części kończących
poszczególnych modułów.
Dyrektywa uses
Dyrektywa uses w module lub programie głównym specyfikuje listę modułów, do których występują
odwołania. Nazwy poszczególnych modułów na liście oddzielone są przecinkami:
uses
Scans, Convert, Arith;
Dyrektywa uses może wystąpić w części publicznej i (lub) w części prywatnej. Nazwy znajdujące się na liście
uses w części publicznej, obowiązujące są w całym module; lista uses w części prywatnej modułu nie jest
natomiast widoczna w jego części publicznej. Niedopuszczalne jest wystąpienie tej samej nazwy na obydwu
listach.
Oto schemat prostego modułu:
unit FooBar;
interface
uses
BarFoo;
…
// tu deklaracje części publicznej
…
implementation
uses
BarFly;
…
// tu deklaracje i definicje części prywatnej
…
initialization
…
// część inicjująca
…
finalization
…
// część kończąca
…
end.
Notatka
implementation
....
end.
UNIT B;
interface
uses
C;
implementation
....
end.
UNIT C;
interface
uses
A;
implementation
....
end.
implementation
uses
C;
....
end.
pozbędziemy się odwołania cyklicznego (jest to oczywiście możliwe tylko wtedy, gdy w części publicznej
modułu B nie ma elementów odwołujących się do modułu C).
Pewnego wyjaśnienia wymaga relacja zwana zależnością pseudocykliczną. Występuje wtedy (przy założeniu, że
dwa moduły nazwaliśmy A i B), gdy moduł A odwołuje się do modułu B w części publicznej, zaś moduł B
odwołuje się do modułu A w części prywatnej — jak w poniższym przykładzie:
UNIT A;
Interface
uses
B;
Implementation
End.
UNIT B;
Interface
Implementation
uses
A;
End.
{ program główny }
USES
A,B;
BEGIN
....
END.
Począwszy od wersji 7.0 Turbo Pascala relacja taka nie ma dla kompilatora żadnego szczególnego znaczenia i
nie powoduje nigdy błędu kompilacji.
Pakiety
W Delphi 3 pojawiła się możliwość podziału kodu wynikowego aplikacji na kilka oddzielnych fragmentów, które
mogą być współdzielone przez kilka aplikacji. Takie „fragmenty”, mające postać bibliotek DLL i stanowiące
kolekcje skompilowanych modułów (units), nazywane są w Delphi pakietami (packages); poszczególne pakiety
przyłączane są do aplikacji w czasie jej wykonania, nie w czasie kompilacji i konsolidacji. Przeniesienie części
kodu aplikacji do pakietów powoduje „odchudzenie” głównego modułu .EXE lub .DLL, jednak z istotną
oszczędnością kodu mamy do czynienia w sytuacji, gdy pojedynczy pakiet wykorzystywany jest równocześnie
przez kilka aplikacji.
Istnieją cztery typy pakietów:
• Pakiety wykonywalne (runtime packages) — poprawniej byłoby nazwać je „pakietami etapu wykonania” —
to pakiety wykorzystywane bezpośrednio przez działającą aplikację. Ich obecność jest konieczna do
uruchomienia aplikacji; przykładem pakietów tej kategorii jest „firmowy” pakiet Delphi 6 VCL60.BPL.
• Pakiety środowiskowe (design-time packages) — zwane również „pakietami etapu projektowania” —
zawierają elementy niezbędne do przeprowadzenia procesu projektowania aplikacji: komponenty, edytory
komponentów i właściwości, programy ekspertowe itp.; przykładami pakietów tej kategorii są w Delphi 6
pliki DCL*.BPL. Pakiety środowiskowe wykorzystywane są wyłącznie przez środowisko IDE; instaluje się
je za pomocą opcji Component|Install Package menu głównego. Zajmiemy się nimi dokładniej w
rozdziale 11.
• Pakiety uniwersalne łączą w sobie funkcjonalność pakietów wykonywalnych i środowiskowych. Dzięki
zintegrowaniu całej funkcjonalności w pojedynczym pliku, pakiet tej kategorii jest nieco wygodniejszy w
dystrybucji, jednak podczas wykorzystywania go przez działającą aplikację wynikową spora część kodu —
ta dotycząca środowiska IDE — nie jest wykorzystywana i bezproduktywnie powiększa rozmiar pliku.
• Pakiety nie mieszczące się w żadnej z powyższych kategorii spotykane są bardzo rzadko i pełnią zazwyczaj
rolę pomocniczą w stosunku do innych pakietów, nie są zaś bezpośrednio wykorzystywane ani przez
uruchomione aplikacje, ani przez środowisko IDE.
Wykorzystywanie pakietów
Wszelkie czynności niezbędne do wygenerowania kodu aplikacji z podziałem na pakiety sprowadzają się do
…zaznaczenia pola Build with Runtime Packages na karcie Packages opcji projektu i skompilowania
projektu w trybie Build. Należy przy tym pamiętać, iż wszystkie wygenerowane pakiety stanowią integralną
część aplikacji i niezbędne są do jej uruchomienia.
Składnia pakietu
Każdy pakiet reprezentowany jest przez plik źródłowy z rozszerzeniem .DPK. Taki plik tworzony jest przez
edytor pakietów (Package Editor) uruchamiany za pomocą polecenia File|New|Package menu głównego.
Zawartość pliku .DPK posiada następujący format:
package nazwa_pakietu
requires Pakiet1, Pakiet2, ...;
contains
Unit1 in 'Unit1.pas',
Unit2 in 'Unit2.pas',
...;
end.
Lista requires zawiera nazwy pakietów niezbędnych do funkcjonowania danego pakietu; są to najczęściej
pakiety zawierające moduły (units) wykorzystywane przez moduły wymienione na liście contains. Lista
contains zawiera nazwy modułów włączanych do pakietu; żadna z tych nazw nie może wystąpić na liście
contains któregokolwiek pakietu wymienionego na liście requires; inaczej mówiąc — każdy moduł (unit)
musi być ładowany przez dany pakiet jednokrotnie. Każdy z modułów wykorzystywanych przez moduły
wymienione na liście contains dołączany jest do pakietu automatycznie (chyba że znalazł się już w pakiecie z
tytułu przynależności do listy requires).
Idea, na której opiera się filozofia obiektów, jest — jak wszystkie genialne pomysły — bardzo klarowna. Zrywa
mianowicie z dotychczasową ideą aplikacji rozumianej jako współpraca dwóch światów — danych i
operującego na nich kodu. W dużych aplikacjach każdy z owych „światów” przejawiał nierzadko tendencje
rozrostu do rozmiarów (niemalże) wszechświata, komplikując coraz bardziej i tak niełatwą już pracę
programistów i projektantów. Cała sprawa znacznie by się uprościła, gdyby współpracę tę zorganizować na
zasadzie istnienia swoistych mikroświatów, z których każdy stanowi cząstkę logicznie powiązanych danych i
kodu. Owe „mikroświaty”, nazwane (może trochę banalnie) obiektami, stanowią podstawę nowego paradygmatu
programowania, zwanego właśnie programowaniem zorientowanym obiektowo (OOP — Object Oriented
Programming). Mimo iż sama idea programowania obiektowego niekoniecznie prowadzi do łatwego
programowania, to zwykle efektem jej zastosowania jest klarowny kod, który łatwo jest utrzymywać i w którym
łatwo jest znajdować ewentualne błędy.
• Enkapsulacja zwana też hermetyzacją. Polega na ścisłym powiązaniu kodu oraz danych służących temu
samemu celowi, poprzez zamknięcie ich w ramach jednego bytu — typu obiektowego, z jednoczesnym
ukryciem szczegółów implementacyjnych. Umożliwiając izolowanie poszczególnych fragmentów kodu,
przyczynia się do modularyzacji programu.
• Dziedziczenie. W złożonych aplikacjach nie sposób nie zauważyć podobieństw między poszczególnymi
fragmentami danych i kodu. Stąd pomysł, by przy definiowaniu nowych typów obiektowych nie zaczynać
pracy ab initio, lecz wykorzystać cechy obiektów już istniejących. Rysunek 2.4 przedstawia zastosowanie
filozofii dziedziczenia cech wśród różnych rodzajów owoców; im dalej od „korzenia”, tym więcej
konkretnych cech rozważanego obiektu.
• Polimorfizm. Tłumaczony dosłownie oznacza „wielopostaciowość” i określa sytuację, w której jednakowo
identyfikowane cechy mogą manifestować się w różny sposób, w zależności od konkretnego egzemplarza
obiektu. Na gruncie Object Pascala oznacza to różne funkcjonowanie identycznie nazwanych metod w
odniesieniu do różnych klas obiektów.
Rysunek 2.4. Ilustracja koncepcji dziedziczenia
<ramka>
Zwróć uwagę na ważny fakt, iż na rysunku 2.4 dla każdego obiektu istnieje dokładnie jedna ścieżka łącząca
go z korzeniem, a więc każdy obiekt dziedziczy swe cechy od dokładnie jednego obiektu macierzystego. Taki
właśnie charakter ma dziedziczenie cech obiektowych w Object Pascalu. Język C++ jest pod tym względem
znacznie bardziej rozbudowany; oferuje dziedziczenie od wielu obiektów jednocześnie (multiple inheritance)
— co można by uwidocznić na wspomnianym rysunku, gdyby udało nam się wyhodować krzyżówkę np.
renety i arbuza.
Brak wielokrotnego dziedziczenia w Object Pascalu postrzegany jest rozmaicie — przez niektórych jako
dotkliwe ograniczenie, przez innych natomiast jako brak jeszcze jednej okazji do popełniania błędów.
Niezależnie od subiektywnego spojrzenia na brak wielokrotnego dziedziczenia, warto zastanowić się nad
rozwiązaniami stanowiącymi jego odpowiednik — a te są w Object Pascalu dwojakie. Jedno z nich,
wykorzystane m.in. przy budowie biblioteki VCL, polega na zawieraniu się w danym obiekcie obiektów klasy
„macierzystej” — obiekt reprezentujący krzyżówkę renety i arbuza mógłby wywodzić się z klasy „arbuz” i
zawierać w sobie (jako jedno z pól) obiekt klasy „reneta”. Druga koncepcja polega na implementowaniu przez
pojedynczy obiekt elementów zachowań kilku specyficznych klas, zwanych interfejsami — zajmiemy się nimi
w dalszej części rozdziału.
<ramka>
Z pojęciem obiektu, jako typu w języku programowania, wiążą się ponadto trzy następujące terminy:
• Pole (field) — zwane także „zmienną egzemplarza” (instance variable) stanowi odmianę zmiennej
funkcjonującej w kontekście obiektu. W języku C++ pola nazywane są „elementami danych” (data
members).
• Metoda (method) — jest procedurą lub funkcją działającą na rzecz pól obiektu. W C++ metody nazywane są
„funkcjami składowymi” (member functions).
• Właściwość (property) — stanowi połączenie koncepcji pola i metody i realizuje mechanizm dostępu do pól
i metod obiektu. Operowanie właściwościami obiektu (w przeciwieństwie do bezpośredniego operowania
jego polami) uniezależnia sposób korzystania z niego od jego szczegółów implementacyjnych.
Wskazówka
Bezpośrednie operowanie na polach obiektu, choć możliwe, jest zasadniczo sprzeczne z filozofią
programowania obiektowego, koncepcyjnie stanowi bowiem rodzaj ingerencji w szczegóły implementacyjne
obiektu. Powinniśmy go unikać i posługiwać się właściwościami.
Posiadając już zdefiniowany typ obiektowy, możemy zdefiniować jego egzemplarz (instance):
Var
FooObject : TFooObject;
To jednak dopiero początek; w przeciwieństwie do Turbo Pascala w wersji 5.5 – 7.0, nie ma w Delphi
możliwości definiowania statycznych egzemplarzy obiektów, natomiast powyższa deklaracja określa zmienną
przechowującą wskaźnik do dynamicznie tworzonego egzemplarza klasy TFooObject.
Do dynamicznego tworzenia egzemplarzy klas służą wyróżnione metody zwane konstruktorami. W Object
Pascalu każda klasa posiada przynajmniej jeden konstruktor o nazwie Create(). Jego zestaw parametrów (lub
ich brak) zależy od konkretnej klasy; dla uproszczenia w dalszej części rozdziału ograniczymy się do jego wersji
bezparametrowej.
W przeciwieństwie do C++, konstruktory w Object Pascalu muszą być wywoływane w sposób jawny. Instrukcja
powodująca utworzenie egzemplarza obiektu, zgodnie ze zdefiniowanym wcześniej typem, ma następującą
postać:
FooObject := TFooObject.Create;
Zwróćmy przy tym uwagę na sposób wywołania konstruktora: jest on wywoływany na rzecz określonego typu
(klasy), nie zaś konkretnego egzemplarza (obiektu). Jest to zrozumiałe wobec faktu, iż przed wywołaniem
konstruktora nie istnieje jeszcze egzemplarz obiektu.
Inicjalizacja obiektu wykonywana przez konstruktor wiąże się między innymi z wyzerowaniem całego
przydzielonego dla obiektu obszaru pamięci. Powoduje to, że wszystkie liczby (będące rzecz jasna polami
obiektu) stają się równe zero, łańcuchy stają się pustymi napisami (''), a wskaźniki — pustymi wskazaniami
(NIL).
Destrukcja obiektu
Po wykorzystaniu obiektu należy zwolnić zajętą przez niego pamięć. Wcześniej muszą zostać wykonane
charakterystyczne dla danego typu czynności kończące. Zadanie to wykonuje wyróżniona metoda zwana
destruktorem. Każda klasa w Object Pascalu zawiera destruktor zwany Destroy(). Teoretycznie, możliwe jest
jego aktywowanie dla konkretnego egzemplarza obiektu, który mamy zamiar unicestwić:
FooObject.Destroy;
14
Zgodnie jednak z przyjętą konwencją, pod pojęciem klasy kryje się w Delphi konkretny typ danych, natomiast określenie
„obiekt” używane jest w odniesieniu do konkretnego egzemplarza tego typu (przyp. tłum.).
Zamiast tego zaleca się jednak wywołanie metody Free():
FooObject.Free;
Metoda ta sprawdza wpierw, czy zmienna obiektowa (FooObject) nie zawiera pustego wskazania (NIL) —
jeżeli nie, następuje wywołanie metody Destroy() dla wskazywanego obiektu.
Ostrzeżenie
W C++ destruktor obiektu zadeklarowanego statycznie wywoływany jest automatycznie w momencie, gdy
sterowanie opuszcza zasięg deklaracji tego obiektu; obiekty tworzone dynamicznie muszą być jednak
zwalniane w sposób jawny, za pomocą słowa kluczowego delete. W Delphi nie ma obiektów statycznych,
musimy więc jawnie zwalniać każdy egzemplarz obiektu, mając na uwadze dwa (z szeregu innych)
uwarunkowania. Po pierwsze, zwalniany obiekt dokonuje jednoczesnego zwolnienia wszystkich innych
obiektów, dla których jest właścicielem; po drugie — istnieją współdzielone przez kilka aplikacji obiekty,
których wykorzystanie opiera się na tzw. liczniku odwołań (reference counter), i które są zwalniane dopiero
wówczas, gdy licznik ten osiągnie wartość 0 (czyli ostatnia z aplikacji zakończy swe operacje na obiekcie).
Przykładami takich współdzielonych obiektów są obiekty klas TInterfacedObject i TComObject.
Nasuwa się pytanie, skąd bierze się obecność konstruktora Create(), destruktora Destroy() i metody
Free() w każdym typie obiektowym? Odpowiedź na to pytanie wskazuje jeszcze jedną różnicę między Turbo
Pascalem a Delphi. W Delphi każdy obiekt bez wskazanej jawnie klasy bazowej jest traktowany jako typ
pochodny klasy TObject, tak więc deklaracja
Type TFoo = class;
Wymienione metody — Create(), Destroy() i Free() są częścią klasy TObject — powrócimy za chwilę
do tej kwestii.
Metody
Metody są tym aspektem typu obiektowego, który pobudza obiekt do życia i decyduje o jego zachowaniu
(trudno to powiedzieć o polach obiektu, które są co najwyżej „pożywką” dla metod). Przykładami metod są
poznane przed chwilą konstruktory i destruktory.
Deklarowanie własnej metody przebiega dwuetapowo. Etap pierwszy to umieszczenie nagłówka metody
wewnątrz deklaracji klasy, na przykład:
Type
TDyskoteka = class;
Taniec : Boolean;
Procedure ZatanczSambe;
End;
Konkretyzacja treści procedury odbywa się w drugim etapie, w części implementacyjnej modułu zawierającego
deklarację klasy:
Procedure TDyskoteka.ZatanczSambe;
begin
Taniec := TRUE;
end;
Zwróć uwagę na to, iż właściwa nazwa procedury poprzedzona jest nazwą klasy, dla której ta procedura jest
metodą. Podobną (kwalifikowaną) postać mają odwołania do metody — nazwa metody poprzedzona jest
określeniem obiektu, na rzecz którego metoda ta jest wywoływana:
Var
Maxim : TDyskoteka;
........
Maxim.ZatanczSambe;
Podobnie jak w przypadku rekordów, odwołania kwalifikowane można zastąpić instrukcją with:
with Maxim do
ZatanczSambe;
W treści metod danej klasy odwołania do pól jej obiektów nie mają postaci kwalifikowanej, gdyż treść metody
jest dla tych pól zakresem ich widoczności (vide pole Taniec w metodzie TDyskoteka.ZatanczSambe).
Typy metod
Metoda klasy w Object Pascalu może być metodą statyczną, wirtualną, dynamiczną i komunikacyjną. Oto
przykład deklaracji metod każdego z wymienionych rodzajów
TFoo = class
Procedure Statyczna;
Procedure Wirtualna;virtual;
Procedure Dynamiczna;dynamic;
Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage;
End;
Metody statyczne
Metoda, której deklaracja nie jest opatrzona żadnymi dodatkowymi klauzulami, jest metodą statyczną.
Funkcjonuje ona podobnie do „zwykłej” procedury lub funkcji, jej adres znany jest już w czasie kompilacji, a jej
wywołanie przebiega bardzo efektywnie. Metody statyczne nie udostępniają jednak żadnych korzyści płynących
z polimorfizmu.
Wskazówka
W przeciwieństwie do C++, klasy Object Pascala nie mogą posiadać statycznych pól. W Object Pascalu pole
jest zawsze częścią egzemplarza klasy (czyli obiektu) — zmiana zawartości pola w jednym obiekcie nie ma
wpływu na jego zawartość w innym; pole statyczne jest natomiast częścią klasy, wspólną dla wszystkich jej
obiektów, ma więc dla nich charakter globalny. Symulacją (do pewnego stopnia) globalnych pól klasy może
być w Object Pascalu wykorzystanie zmiennych globalnych modułu (w jego części prywatnej) — w treści
metod zmienne takie zachowują się tak, jak zachowywałyby się pola statyczne (gdyby istniały)15.
Metody wirtualne
Dziedziczenie wiąże się z możliwością przedefiniowywania (overriding) metod obiektu. Oznacza to, że metoda
o danej nazwie może mieć zupełnie różne działanie dla różnych klas (macierzystej i pochodnej). Innymi słowy,
kompilator, znając nazwę metody, nie potrafi określić jej konkretnego adresu, gdy nie zna konkretnego obiektu
(a właściwie jego typu), na rzecz którego jest ona aktywowana.
Zjawisko różnego zachowania metod o tej samej nazwie w odniesieniu do różnych typów w całym poddrzewie
typów pochodnych danej klasy nosi nazwę polimorfizmu — metoda o danej nazwie ma jak gdyby „wiele
twarzy”. Zachowuje się różnie, w zależności od typu obiektu, na rzecz którego zostanie wywołana. Dla realizacji
polimorfizmu Object Pascal utrzymuje struktury zwane tablicami VMT (Virtual Method Tables), po jednej dla
każdej klasy. Każda tablica VMT zawiera adresy wszystkich metod wirtualnych swej klasy (także tych,
które dziedziczone są z klasy bazowej bez zmian), a więc metody wirtualne przyczyniają się w pewnym
15
Zmienne globalne modułu w połączeniu z mechanizmem właściwości pozwalają na symulację statycznych pól nie tylko w
treści metod — niebawem powrócimy do tej kwestii (przyp. tłum.).
stopniu do obciążenia pamięci. Obciążenie to można do pewnego stopnia zmniejszyć, za cenę nieznacznego
pogorszenia efektywności, używając metod dynamicznych.
Metody dynamiczne
Metody dynamiczne wykorzystywane są dokładnie tak samo, jak metody wirtualne, jednak ich realizacja
ukierunkowana została przede wszystkim na efektywne wykorzystanie pamięci, kosztem efektywności ich
wywoływania. Dla każdej klasy deklarującej chociaż jedną metodę dynamiczną kompilator utrzymuje strukturę
zwaną tablicą DMT (Dynamic Method Table). Tablica DMT zawiera adresy tylko tych metod, które są
przedefiniowane w stosunku do klasy macierzystej; klasy nie deklarujące własnych metod dynamicznych nie
posiadają w ogóle tablicy DMT. Metody dynamiczne nie obciążają więc pamięci brzemieniem dziedziczonym z
klas macierzystych, ich wywoływanie jest jednak mniej efektywne (niż w przypadku metod wirtualnych),
ponieważ bardziej złożony jest algorytm poszukiwania adresu konkretnej metody.
Metody komunikacyjne
Metody tej kategorii stanowią reminiscencję „klasycznego” programowania w Windows i służą do obsługi
wybranych komunikatów — identyfikator komunikatu zawarty jest w klauzuli message w deklaracji metody.
Metody komunikacyjne nie są raczej przeznaczone do bezpośredniego wywoływania — należą do tzw. funkcji
zwrotnych (callback), wywoływanych automatycznie przez system operacyjny. Szczegółami obsługi
komunikatów systemowych zajmiemy się w rozdziale 3.
Przedefiniowywanie metod
Przedefiniowywanie metod jest praktyczną realizacją polimorfizmu. Na gruncie danej klasy i wszystkich jej klas
pochodnych metoda o danej nazwie może wykazywać różne zachowanie w zależności od konkretnej klasy (lub
klasy konkretnego obiektu). Przedefiniowywane mogą być tylko metody wirtualne i dynamiczne; fakt
przedefiniowania metody zaznacza się klauzulą override w jej deklaracji. W poniższym przykładzie klasa
TFooChild przedefiniowuje metody Wirtualna i Dynamiczna, odziedziczone z klasy macierzystej TFoo:
TFooChild = class(TFoo)
Procedure Wirtualna;override;
Procedure Dynamiczna;override;
End;
metody Wirtualna i Dynamiczna nie mają nic wspólnego z identycznie nazwanymi metodami klasy TFoo.
Gdy kompilator napotka taką sytuację, wygeneruje ostrzeżenie, iż metoda klasy pochodnej zasłania identycznie
nazwaną metodę klasy macierzystej.
Reintrodukcja metody
Opisane przed chwilą „zasłonięcie” metody klasy bazowej i zapoczątkowanie nowego łańcucha metod o
identycznej nazwie może być niekiedy działaniem całkowicie zamierzonym. Dla podkreślenia, iż nie mamy do
czynienia z pomyłką — i jednocześnie dla wyeliminowania ostrzeżeń ze strony kompilatora — możemy w
sposób jawny zasygnalizować ten fakt, opatrując deklarację metody klauzulą reintroduce, jak w poniższym
przykładzie:
TFoo = class
Procedure Statyczna;
Procedure Wirtualna;virtual;
Procedure Dynamiczna;dynamic;
Procedure Komunikacyjna ( var M: TMessage ); message wm_SomeMessage;
End;
TFooOrphan = class(TFoo)
Procedure Wirtualna;reintroduce;
Procedure Dynamiczna;reintroduce;
End;
Klauzula reintroduce nie wyklucza oczywiście wystąpienia innych klauzul (virtual, dynamic i message)
w deklaracji metody.
Przeciążanie metod
Podobnie jak „zwykłe” procedury i funkcje, również metody mogą być przeciążane — umożliwia to opatrzenie
wspólną nazwą wielu aspektów konkretnej metody (w konkretnej klasie) różniących się zestawem parametrów.
Oto przykład:
Type
TSomeClass = class
procedure Amethod(I: Integer);overload;
procedure Amethod(S: String);overload;
procedure Amethod(D: Double);overload;
end;
Poniższy przykład (zaczerpnięty z systemu pomocy Delphi) ilustruje ciekawy przypadek, gdy różne aspekty
danej metody należą do różnych klas:
type
T1 = class(TObject)
procedure Test(I: Integer); overload; virtual;
end;
T2 = class(T1)
procedure Test(S: string); reintroduce; overload;
end;
...
SomeObject := T2.Create;
SomeObject.Test('Hello!'); // wywołuje T2.Test()
SomeObject.Test(7); // wywołuje T1.Test()
Identyfikator Self
Aby w treści metody możliwe były kwalifikowane odwołania do pól, metod i właściwości obiektu, konieczne
jest użycie identyfikatora tegoż obiektu — takim uniwersalnym identyfikatorem jest Self reprezentujący w
treści konkretnej metody obiekt, na rzecz którego wywołana została ta metoda. Jego zawartość, stanowiąca
wskaźnik do wspomnianego obiektu, przekazywana jest niejawnie jako dodatkowy parametr wywołania
wszystkich metod.
Właściwości
Natura właściwości (property) obiektu jest nieco bardziej abstrakcyjna niż natura pola czy metody.
Koncepcyjnie właściwość zbliżona jest do pola, gdyż podobnie jak pole przechowuje (modyfikowalną) wartość
określonego typu; bardziej skomplikowany jest natomiast sposób nadawania i odczytywania tej wartości.
Spójrzmy wpierw na deklarację przykładowej właściwości:
TMyObject = Class
private
SomeValue : Integer;
Procedure SetSomeValue(Avalue: Integer);
public
Property Value: Integer read SomeValue write SetSomeValue;
End;
Klasa TMyObject definiuje pole SomeValue, metodę SetSomeValue i właściwość Value; ta ostatnia
powiązana jest z pozostałymi elementami za pomocą klauzul read i write. Klauzula read określa sposób
odczytywania właściwości: ponieważ specyfikuje ona nazwę pola, aktualna wartość tego pola przyjmowana jest
jako wartość właściwości. Klauzula write specyfikuje natomiast nazwę metody — a to oznacza, że przypisanie
właściwości nowej wartości zostanie fizycznie zrealizowane jako wywołanie tejże metody z przypisywaną
wartością jako parametrem. Konkretnie: dla obiektu MyObj:TMyObject instrukcja
WhatValue := MyObj.Value;
Z kolei instrukcja
MyObj.Value := NewValue;
oznacza to samo, co
MyObj.SetSomeValue(NewValue);
W każdej z klauzul read i write może wystąpić bądź nazwa pola, bądź nazwa metody; żadna z klauzul read i
write nie jest obowiązkowa — na przykład opuszczenie klauzuli write powoduje, iż właściwości nie można
przypisywać explicite nowej wartości. Metody specyfikowane w ramach klauzul read i write umożliwiają
pełną kontrolę nad odczytywaniem i modyfikacją właściwości; stanowią one jedyny sposób dostępu do
właściwości i z tego względu nazywane są jej metodami dostępowymi (property access methods).
Właściwości komponentów VCL stanowią podstawowy środek ich komunikacji z aplikacjami; ich właściwości
opublikowane (published) dostępne są za pośrednictwem inspektora obiektów.
Wskazówka
A oto zapowiadana symulacja statycznych pól klasy za pomocą właściwości — właściwość StaticValue
zachowuje się (prawie) tak, jak statyczne pole w C++:
var
Type
MyStaticClass = class
private
private
end;
begin
Result := GlobalField;
end;
begin
GlobalField := Value;
end;
(przyp. tłum.).
Wskazówka
Ewentualne początkowe elementy deklaracji klasy nie opatrzone żadnym kwalifikatorem widoczności są, w
zależności od ustawienia przełącznika kompilacji $M, opublikowane {$M+} bądź publiczne {$M–}(przyp.
tłum.).
Możliwa jest zmiana kwalifikatora widoczności dziedziczonego elementu w klasie pochodnej, ale tylko w
kierunku „rosnącym” — na przykład element chroniony (protected) może zostać uczyniony elementem
publicznym (public), ale nie prywatnym (private) (przyp. tłum.).
Klasy zaprzyjaźnione
„Zaprzyjaźnienie” klas w C++ oznacza dostęp do prywatnych elementów definicji danej klasy z poziomu
innych klas (zwanych „klasami zaprzyjaźnionymi” — friend classes). Koncepcja ta, mimo iż nie nazwana w
sposób wyraźny, jest jednak faktycznie obecna w Object Pascalu, chociaż w sposób zdecydowanie mniej
selektywny — wszystkie klasy definiowane w tym samym module są dla siebie nawzajem klasami
zaprzyjaźnionymi.
Wewnątrz obiektów
Używanie zmiennych obiektowych w Object Pascalu wiąże się z pewną niekonsekwencją, która dla
niewprawnego użytkownika może być nieco myląca. Otóż zmienne obiektowe, mimo iż są używane w sposób
charakterystyczny dla zmiennych statycznych, są w istocie 32-bitowymi wskaźnikami do obiektów; te ostatnie
alokowane są na stercie i wspomniane wskaźniki stanowią jedyny sposób dostępu do nich — nie ma w Object
Pascalu możliwości ich bezpośredniego reprezentowania. Zgodnie z ogólnymi regułami Pascala odwołanie się
do wskaźnika wymaga użycia operatora dereferencji (^) i wydawałoby się, iż zamiast
Button1.Caption
Notatka
type
TMyRecord = record
A,B : integer;
end;
var
P: ^TMyRecord;
instrukcja
P.A := 1;
powinna być uznana za błędną z powodu braku operatora dereferencji — i taką też jest w Turbo Pascalu.
Object Pascal jednak, jakby odgadując intencje programisty, traktuje ją jako poprawną, zakładając niejawną
dereferencję. W przeciwieństwie do zmiennych obiektowych, jawne użycie operatora ^
P^.A := 1;
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer):
HResult;virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
Widzimy tu „starych znajomych” — konstruktor Create, destruktor Destroy i metodę Free. Znaczenie każdej
z deklarowanych metod opisane jest w systemie pomocy Delphi. Dociekliwym czytelnikom proponujemy
ponadto, by przyjrzeli się kodowi źródłowemu definicji tych metod w module SYSTEM.PAS.
Pewnego wyjaśnienia wymagają metody opatrzone dyrektywą class. Metody takie, notabene analogiczne do
statycznych metod C++, funkcjonują w kontekście klasy jako całości, bez różnicy dla poszczególnych obiektów
— i na przykład metoda ClassName, aktywowana na rzecz konkretnego obiektu, zwraca nazwę jego klasy,
pozostającą bez żadnego związku z jego zawartością16.
Interfejsy
Interfejsy (interfaces) stanowią specjalną kategorię klas, związaną z wykorzystaniem technologii obiektów —
komponentów (COM — Component Object Model); jako odrębny element syntaktyczny zostały wydzielone
dopiero w Delphi 3 — w Delphi 2 funkcjonowały na równi z innymi klasami (jako klasy pochodne w stosunku
do klasy IUnknown). Generalnie, interfejs jest zbiorem funkcji i procedur umożliwiających interakcję z
obiektem; zbiór ten określa pewien aspekt zachowania się obiektu, lecz jedynie w ujęciu intencjonalnym —
interfejs zawiera bowiem jedynie deklaracje wspomnianych procedur i funkcji; nadanie im treści, czyli
powiązanie ich z konkretnymi działaniami, jest kwestią ich implementacji jako metod w konkretnym obiekcie.
Wewnętrzne szczegóły funkcjonowania interfejsów opierają się na koncepcji tzw. klasy czysto wirtualnej (pure
virtual class) — jest nią klasa pozbawiona pól i nie implementująca swych metod; do reprezentowania takiej
klasy wystarczająca jest sama tablica VMT.
Definiowanie interfejsów
Podobnie jak wszystkie klasy Object Pascala wywodzą się z klasy TObject, tak każdy interfejs jest pochodną
interfejsu IUnknown:
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj):
HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Notatka:
16
Ponieważ podmiotem wywołania metody opatrzonej dyrektywą class jest klasa jako całość, inne jest znaczenie
identyfikatora Self w jej treści — zawiera on mianowicie wskaźnik do tzw. punktu zerowego tablicy VMT związanej z
klasą, na rzecz której metoda jest wywoływana; syntaktycznie jest on zmienną metaklasy (patrz następny przypis) (przyp.
tłum.).
W Delphi 6 bazowy interfejs nosi nazwę IInterface, natomiast IUnknown jest synonimem tej nazwy:
type
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
end;
IUnknown = IInterface;
(przyp. tłum.).
Jak widać, deklaracja interfejsu podobna jest do deklaracji klasy; jednak tym, co odróżnia interfejs od klasy, jest
unikatowy identyfikator (GUID — Globally Unique Identifier). Identyfikuje on każdy interfejs jednoznacznie —
dwa różne interfejsy, zdefiniowane w tej samej lub w różnych aplikacjach, stworzonych na tym samym
komputerze lub różnych komputerach, w dowolnym czasie, powinny mieć dwa różne identyfikatory GUID.
Wskazówka
W środowisku IDE unikatowy identyfikator GUID otrzymuje się przez naciśnięcie kombinacji klawiszy
Ctrl+Shift+G.
Metody interfejsu IUnknown związane są ściśle z technologią COM, którą zajmiemy się szczegółowo w drugim
tomie niniejszej książki.
Definiowanie interfejsów pochodnych nie różni się zasadniczo od definiowania klas pochodnych — poniższy
interfejs definiuje własną metodę F1; ponieważ nie wskazano interfejsu bazowego, jest nim domyślnie
IUnknown.
Type
IFoo = interface
['{A77A4BE0-0C82-11D6-AA88-444553540001}']
Function F1: Integer;
end;
Implementowanie interfejsów
Jak wspomnieliśmy wcześniej, docelową rolą interfejsu jest jego implementacja w postaci metod jakiejś klasy.
Poniższy przykład ilustruje implementację interfejsów IFoo i IBar przez klasę TFooBar:
Type
TFooBar = class(TInterfacedObject, IFoo, IBar)
function F1: Integer;
function F2: Integer;
End;
...
Function TFooBar.F1: Integer;
begin
Result := 0;
end;
Zwróć uwagę, iż na liście klas bazowych występuje kilka pozycji; tak naprawdę klasą bazową jest jednak tylko
TInterfacedObject, pozostałe pozycje są nazwami implementowanych interfejsów. Klasa
TInterfacedObject zawiera wszystkie niezbędne mechanizmy implementacji interfejsów, jest więc dla
obiektów implementujących interfejsy klasą bazową:
Jak widać, nazwy metod stanowiących implementację interfejsu tożsame są z nazwami metod tego interfejsu.
Może się jednak zdarzyć, iż dwa różne interfejsy implementowane przez tę samą klasę posiadać będą metody o
tej samej nazwie; jedna z tych metod (lub obydwie) musi być wówczas implementowana pod zmienioną nazwą
(aliasem) — w poniższym przykładzie zmienione zostają nazwy obydwu metod F1:
type
ITool = interface
['{7C9CAAA4-40EB-11D2-A3FB-444553540000}']
Function F1: integer;
End;
ITip = interface
['{7C9CAAA5-40EB-11D2-A3FB-444553540000}']
Function F1: integer;
End;
TPrompt = Class(TInterfacedObject, ITool, IHelp)
Dyrektywa implements
Implementacja interfejsu może mieć również charakter pośredni — mianowicie wskaźnik do implementowanego
interfejsu może być wartością właściwości, jak w poniższym przykładzie:
Type
TSomeClass = class(TInterfacedObject, ICasual)
...
Function GetCasual: TCasual;
Property Casual: TCasual read GetCasual implements ICasual;
...
End;
Dyrektywa implements stanowi dla kompilatora informację, iż implementacji metod interfejsu poszukiwać
należy w innej klasie (w tym przypadku TCasual) — zjawisko to nazywa się więc popularnie
implementowaniem delegowanym (implementation by delegation). Typ właściwości zawierającej dyrektywę
implements musi być zgodny z typem implementowanego interfejsu lub z typem implementującej go klasy.
Powody wprowadzenia implementacji delegowanej (pojawiła się w Delphi 4) są dwojakie. Po pierwsze,
umożliwia ona klarowną realizację koncepcji agregacji obiektów, stanowiącej integrację (na gruncie technologii
COM) kilku klas w celu realizacji wspólnego celu; zajmiemy się tym szczegółowo w jednym z rozdziałów
drugiego tomu niniejszej książki.
Drugi powód wprowadzenia implementowania delegowanego związany jest z oszczędnością zasobów
systemowych. Jeżeli implementacja jakiegoś interfejsu wiąże się np. z dużym obciążeniem pamięci, a interfejs
ten wykorzystywany jest bardzo rzadko, wskazane byłoby tę implementację odłożyć do momentu, gdy
wspomniany interfejs okaże się faktycznie potrzebny. W niniejszym przykładzie aplikacja chcąca skorzystać z
interfejsu ICasual (dokładniej — z jego implementacji) odwoła się w tym celu do właściwości Casual. Przy
pierwszym odwołaniu tego rodzaju metoda dostępowa GetCasual powinna utworzyć obiekt typu Casual i
zwrócić jako wynik jego adres (przy kolejnych odwołaniach powinna zwracać wskaźnik do istniejącego obiektu
— singletonu). Jeżeli odwołania do właściwości Casual nie będzie, nie będzie też tworzony wspomniany obiekt
i nie wystąpi związane z tym obciążenie zasobów systemu.
Korzystanie z interfejsów
W tym miejscu chcielibyśmy zwrócić uwagę na pewne charakterystyczne cechy zmiennych reprezentujących
interfejsy. Po pierwsze, zmienne wskazujące na interfejsy należą do kategorii zmiennych o kontrolowanym
czasie życia (lifetime memory-managed) oraz są inicjowane przez kompilator wartością NIL. Pod drugie —
dostęp do implementowanych interfejsów kontrolowany jest przez liczniki odwołań (reference counters).
Poniższy przykład wyjaśnia w sposób poglądowy „zakulisowe” działania odzwierciedlające obydwa te
mechanizmy:
var
I: ISomeInterface;
begin
// w tym miejscu zmienna I inicjowana jest automatycznie
// wartością NIL
//------------------------------------------------------
-------------------------------------------------------
// kończy się wykonywanie bloku procedury (funkcji),
// kończy się więc czas życia zmiennej I.
// Następuje automatyczne zmniejszenie licznika odwołań
// związanego z interfejsem ISomeInterface.
// Jeżeli w wyniku tego licznik osiągnął wartość zero,
// to następuje również zwolnienie interfejsu.
end;
Inną ważną cechą każdego interfejsu (jako typu) jest jego zgodność w sensie przypisania z każdą
implementującą go klasą. Oto przykład poprawnej instrukcji przypisania (opieramy się tu na przedstawionych
wcześniej definicjach TFooBar i IFoo):
procedure Test(FB: TFooBar)
var
F: IFoo;
begin
...
F := FB; // poprawne, bowiem FB implementuje F
...
I kolejny automatyzm Object Pascala — operator as użyty w kontekście interfejsu powoduje (automatyczne)
wywołanie metody QueryInterface tegoż interfejsu — oto przykład:
var
FB : TFooBar;
F: IFoo;
B: IBar;
begin
FB := TFooBar.Create;
F := FB;
B := F as IBar;
// powyższa instrukcja równoważna jest wywołaniu
// F.QueryInterface(IBar, B);
Gdyby interfejs IFoo nie oferował udostępniania interfejsu IBar, ostatnia instrukcja spowodowałaby wyjątek
EIntfCastError.
uses
Classes, Dialogs;
{$APPTYPE CONSOLE}
Var
F: TextFile;
S: String;
begin
AssignFile(F, 'FOO.TXT');
try
Reset(F);
try
Readln(F,S);
finally
CloseFile(F);
end;
except
on EInOutError do
ShowMessage('Błąd wejścia/wyjścia!');
end;
end;
W przedstawionej konstrukcji try ... finally ... end wykonywana jest najpierw grupa instrukcji
pomiędzy klauzulami try i finally. Po jej zakończeniu — normalnym lub na skutek wyjątku — wykonywana
jest grupa instrukcji pomiędzy finally i end. Ta grupa instrukcji wykonywana jest niezależnie od tego, jaki
był skutek wykonania instrukcji pierwszej grupy. Jest to bardzo wygodne w przypadku, gdy trzeba na przykład
bezwarunkowo zwolnić przydzielone zasoby, czy też — jak w przedstawionym przykładzie — zamknąć otwarte
pliki.
Notatka
Zewnętrzna konstrukcja try ... except ... end jest właśnie podstawowym narzędziem obsługi wyjątków.
Słowo except oddziela grupę instrukcji zasadniczych od bloku dokonującego obsługi wyjątku. Dokonano więc
rozdziału miejsca, w którym wyjątek występuje od miejsca, w którym jest on obsługiwany.
Istnienie dwóch różnych konstrukcji związanych z wyjątkami — try…finally i try…except —
odzwierciedla dwojakiego rodzaju działania zapewniające aplikacji bezpieczne działanie. Oprócz obsłużenia
błędów zapewnia bezwarunkowe wykonanie pewnych krytycznych instrukcji, na przykład zwalniających
przydzieloną pamięć czy zamykających otwarte pliki.
Sama informacja o fakcie wystąpienia wyjątku jest na ogół niewystarczająca — wobec różnorodności
możliwych wyjątków ich obsługa musi być bardziej selektywna. Spójrzmy na poniższy przykład:
{$APPTYPE CONSOLE}
Var
R1, R2 : Double;
begin
While True do
begin
try
Write('Podaj liczbę rzeczywistą:');
Readln(R1);
Write('Podaj inną liczbę rzeczywistą:');
Readln(R2);
Writeln ('Teraz spróbuję podzielić wprowadzone liczby ...');
Writeln ('Iloraz wynosi ', (R1/R2) :5:2 );
except
on EZeroDivide do
Writeln('Próba dzielenia przez zero!');
on EInOutError do
Writeln('Nieprawidłowa postać liczby!');
end;
end;
end;
W powyższym przykładzie mogą być poprawnie obsłużone dwie kategorie wyjątków: dzielenia przez zero oraz
konwersji liczby z postaci znakowej na zmiennoprzecinkową. Pozostałe wyjątki pozostaną nie obsłużone, chyba
że blok obsługi zostanie wzbogacony w tzw. sekcję obsługi domyślnej (default exception handler):
Program Obsluga;
{$APPTYPE CONSOLE}
Var
R1, R2 : Double;
begin
While True do
begin
try
................
except
on EZeroDivide do
Writeln('Próba dzielenia przez zero!');
on EInOutError do
Writeln('Nieprawidłowa postać liczby!');
Else
Writeln('Błąd niesprecyzowany!');
end;
end;
end;
Sekcja obsługi domyślnej rozpoczyna się nie od słowa kluczowego on, lecz od słowa else. Podobny efekt
możemy uzyskać nie specyfikując w bloku obsługi wyjątku żadnej sekcji on ... — cały blok będzie wówczas
stanowił domyślną sekcję:
Program Obsluga;
{$APPTYPE CONSOLE}
Var
R1, R2 : Double;
begin
While True do
begin
try
................
except
Writeln('Błąd przetwarzania – coś jest nie w porządku!');
end;
end;
end;
Ostrzeżenie:
Ostrzeżenie
Własne klasy wyjątków powinny być definiowane na bazie innych, prawidłowo funkcjonujących klas wyjątków,
na przykład klasy Exception. Gwarantuje się w ten sposób użycie niezbędnych, standardowych
mechanizmów obsługi wyjątków.
Ze względu na obiektową naturę wyjątków w Delphi, ich obsługa ma pewien związek ze zjawiskiem
dziedziczenia. Otóż, sekcja zdefiniowana dla określonej klasy wyjątków (po słowie on) jest również sekcją
obsługi wyjątków pochodnych. Na przykład sekcja zdefiniowana dla wyjątku EMathError będzie również
obsługiwać wyjątki EZeroDivide i EOverflow, które są typami pochodnymi w stosunku do EMathError.
Wyjątki nie obsłużone w ramach bloku except podlegają obsłudze w ramach domyślnej (dla aplikacji)
procedury obsługi wyjątków. Standardowo obsługa ta polega na wypisaniu komunikatu — pisaliśmy o tym
szczegółowo w 4. rozdziale „Delphi 4. Vademecum profesjonalisty”, w punkcie „Zmiana domyślnej procedury
obsługi wyjątków”.
Podczas obsługi wyjątku konieczne jest niekiedy uzyskanie dostępu do obiektu reprezentującego ten wyjątek —
na przykład w celu odczytania treści komunikatu ukrywającego się pod właściwością Message. Obiekt ten
dostępny jest za pośrednictwem funkcji ExceptObject (jeżeli wyjątek aktualnie nie występuje, funkcja ta
zwraca NIL), możemy się jednak do niego dostać znacznie prościej, specyfikując w sekcji on reprezentujący go
identyfikator, oddzielony dwukropkiem od identyfikatora klasy, na przykład
try
…
except
on E:ESomeException do
ShowMessage(E.Message);
end;
W powyższym przykładzie identyfikator E reprezentuje bieżący obiekt wyjątku klasy ESomeException. Ten
sam efekt można uzyskać w następujący sposób:
try
…
except
on E:ESomeException do
ShowMessage(ESomeException(ExceptObject).Message);
end;
Ponieważ we frazie else bloku except nie występuje identyfikator klasy wyjątku, nie jest także możliwa
opisana „konstrukcja z dwukropkiem”; jedynym środkiem dostępu do obiektu wyjątku pozostaje wówczas
funkcja ExceptObject.
Poza obsługiwaniem wyjątków, możliwe jest także ich generowanie w aplikacjach. Robi się to za pomocą słowa
kluczowego raise, po którym występuje wyrażenie reprezentujące obiekt wyjątku, na przykład:
raise EBadStuff.Create('Coś jest nie w porządku');
„Samotne” słowo raise, bez podania obiektu wyjątku, powoduje ponowienie wyjątku aktualnie istniejącego.
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
type
EBadStuff = class(Exception);
Procedure Proc3;
begin
try
raise EBadStuff.Create('Dzieje się coś niedobrego!');
finally
ShowMessage('Wystąpił wyjątek, który procedura Proc3
zauważyła');
end;
end;
Procedure Proc2;
begin
try
Proc3;
finally
ShowMessage('Procedura Proc2 również jest świadoma
wyjątku');
end;
end;
Procedure Proc1;
begin
try
Proc2;
finally
ShowMessage('Procedura Proc1 również jest świadoma
wyjątku');
end;
end;
end.
Wykonanie generującej wyjątek instrukcji raise (w procedurze Proc3()) powoduje przekazanie sterowania do
bloku finally; wyjątek pozostaje nieobsłużony, sterowanie wraca do procedury Proc2().
Z punktu widzenia procedury Proc2() instrukcja wywołująca procedurę Proc3() jest instrukcją powodującą
wyjątek; sterowanie wędruje więc do bloku finally (oczywiście w procedurze Proc2()); wyjątek pozostaje
nieobsłużony, sterowanie wraca do Proc1().
Tu sytuacja się powtarza — instrukcja wywołująca Proc2() uważana jest za instrukcję powodującą wyjątek;
sterowanie trafia do bloku finally, a następnie do procedury Button1Click(). Wyjątek pozostaje
nieobsłużony.
Z punktu widzenia procedury Button1Click() za wyjątek odpowiedzialna jest instrukcja wywołująca
procedurę Proc1(). Instrukcja ta znajduje się w obszarze konstrukcji try…except…end, sterowanie wędruje
więc do bloku except i wyjątek zostaje wreszcie obsłużony.
Wskazówka
Opisaną sytuację możesz prześledzić samodzielnie, ustawiając punkt przerwania (breakpoint) na instrukcji
wywołującej procedurę Proc1() i kontynuując wykonanie w sposób krokowy. Musisz jedynie zadbać o to, by
nie przeszkadzały Ci w tym procedury obsługi wyjątków w zintegrowanym debuggerze — wyłącz je poprzez
usunięcie zaznaczenia opcji Stop on Delphi Exceptions na karcie Language Exceptions opcji
debuggera (Tools|Debugger Options).
Ponowienie wyjątku
Gdy wskutek wystąpienia wyjątku sterowanie trafia do odpowiedniej sekcji on… w bloku except (lub w ogóle
do bloku except w przypadku braku podziału na sekcje), po wyjściu sterowania z tego bloku wyjątek jest
obsłużony i reprezentujący go uprzednio obiekt już nie istnieje. Nie dotyczy to jednak wyjątków zaistniałych w
trakcie realizacji bloku except — wędrują one do bloku except na wyższym poziomie zagnieżdżenia.
Gdy w poniższej funkcji RPower wystąpi wyjątek, jedynym jego sygnałem będzie zwrócenie zerowego wyniku
— gdyż do tego sprowadza się obsługa wszelkich wyjątków w bloku except.
Function RPower(X, Y: Real):Real;
begin
try
Result := exp(Y*ln(X));
except
Result := 0.0;
end;
end;
Rozsądniej będzie jednak powierzyć obsługę ewentualnego wyjątku zewnętrznym blokom except, regenerując
(ponawiając) go za pomocą instrukcji raise:
RTTI
Pod tytułowym skrótem kryje się mechanizm udostępniający uruchomionej aplikacji informacje o jej obiektach
— ang. Runtime Type Information. Informacja ta wykorzystywana jest także przez środowisko IDE, warunkując
jego prawidłową współpracę z komponentami na etapie projektowania aplikacji.
Elementy zapewniające łączność obiektu ze strukturami danych RTTI wbudowane są już w klasę TObject,
posiada je zatem każdy obiekt Delphi. Najważniejsze z metod udostępniających informację RTTI opisane są w
tabeli 2.8.
Tabela 2.8. Ważniejsze metody klasy TObject udostępniające informację z kategorii RTTI
Metoda Typ wyniku Znaczenie
ClassName() String Nazwa klasy
ClassType() TClass Klasa jako typ17
InheritsFrom() Boolean Informuje o istnieniu lub nieistnieniu relacji
dziedziczenia między klasami
ClassParent() TClass Typ macierzysty w stosunku do danego
ClassInfo() Pointer Wskaźnik do bloku RTTI w pamięci
Do kategorii RTTI należą także dwuargumentowe operatory is i as. Pierwszy z nich zwraca wartość boolowską
informującą o tym, czy obiekt stanowiący lewy operand zalicza się do klasy identyfikowanej18 przez prawy
operand. Poniższa funkcja zwraca nazwę klasy obiektu przekazanego jako parametr; jeżeli jednak obiekt ten jest
obiektem klasy TEdit lub pochodnej, zwracana jest także zawartość jego właściwości Text:
function CoZaObiekt(X:TObject):String;
begin
17
W Object Pascalu zbiór wszystkich typów pochodnych w stosunku do danej klasy składa się na typ wyższego rzędu,
zwany klasowym typem referencyjnym (class-reference type) lub metaklasą (metaclass). Deklaracja metaklasy ma postać
class of typ, gdzie typ jest nazwą klasy macierzystej. Najbardziej ogólną metaklasą jest w Class of TObject
obejmująca wszystkie klasy Object Pascala; jej synonimem jest TClass. Podobnie jak inne typy, również metaklasy mogą
posiadać swoje zmienne; wartością każdej takiej zmiennej jest wskazanie na konkretną klasę (nie obiekt!), a dokładniej — na
związaną z tą klasą tablicę VMT (przyp. tłum.).
18
za pomocą nazwy typu lub zmiennej metaklasy (przyp. tłum.)
Result := X.ClassName();
if X is TEdit
then
Result := Result + '(' + TEdit(X).Text + ')';
end;
Informacja RTTI jest nowością Delphi; nie było jej w Turbo Pascalu — jeżeli nie liczyć funkcji TypeOf()
równoważnej (w pewnym sensie) metodzie ClassType(), lecz zwracającej (amorficzny) wskaźnik do tablicy
VMT.
Podsumowanie
W niniejszym rozdziale przedstawiliśmy najważniejsze cechy języka Object Pascal stanowiącego
„lingwistyczne” fundamenty Delphi. Opisaliśmy najważniejsze elementy składni i semantyki języka — zmienne,
operatory, funkcje, procedury, typy oraz instrukcje. Zajęliśmy się także podstawami realizacji programowania
obiektowego, wyjaśniając najważniejsze elementy i koncepcje związane z tą filozofią: pola, metody i
właściwości obiektów oraz enkapsulację, dziedziczenie i polimorfizm; zaprezentowaliśmy także proste
przykłady implementacji interfejsów przez obiekty. Na zakończenie omówiliśmy podstawowe zasady
generowania i obsługi wyjątków oraz najistotniejsze elementy związane z mechanizmem RTTI.