Typy danych - systematyka

Dryobates

Object Pascal ma bardzo wiele typów i dosyć ostro przestrzega zasad zgodności typów. To umożliwia kompilatorowi wyszukiwanie błędów popełnionych przez użytkownika (np. nie przekaże do funkcji, która wymaga łańcucha znaków, rekordu czy liczby, co powodowałoby błędy).

W OP istnieje wiele predefiniowanych typów, które rozpoznaje kompilator. Typy dzielą się na ogólne (zależne od platformy) i podstawowe (niezależne od platformy). Operacje na typach ogólnych są szybsze, ale mogą się one zmieniać w zależności od procesora i systemu operacyjnego (np. Integer w systemach 16-bitowych ma wartość maksymalną 32767, w systemach 32-bitowych 2147483647).

Aby sprawdzić ilość miejsca zajmowanego przez dany typ w pamięci należy użyć funkcji SizeOf

Podział typów:

  1. Proste
    1. Porządkowe
      1. Całkowite
      2. Znakowe
      3. Boolowskie
      4. Wyliczeniowe
      5. Okrojone
    2. Rzeczywiste
  2. Łańcuchowe
  3. Strukturalne
    1. Zbiory
    2. Tablice
    3. Rekordy
    4. Pliki
    5. Klasy
    6. Referencje klas
    7. Interfejsy
    8. Wskaźniki
  4. Proceduralne
  5. Variant


I. Typy proste, w skład, których wchodzą typy porządkowe i rzeczywiste stanowią uporządkowane zbiory

1. Typy porządkowe to takie uporządkowane zbiory, w których każdy element oprócz pierwszego ma swojego poprzednika i każdy element oprócz ostatniego ma swojego następcę.

Każdy element ma swój numer porządkowy:

  • dla liczb typu całkowitego jest to wartość tej liczby
  • dla typów okrojonych są to wartości odpowiadających im typów bazowych
  • dla typów wyliczeniowych jest to numer elementu zbioru zaczynając od 0

Funkcje operujące na typach porządkowych:

  • Ord - zwraca wartość porządkową (nie działa na typ Int64)
  • Pred - zwraca wartość poprzednią (nie używać na właściwościach, które mają procedurę write)
  • Succ - zwraca wartość następną (nie używać na właściwościach, które mają procedurę write)
  • High - zwraca wartość największą
  • Low - zwraca wartość najmniejszą

Dokładne opisy tych funkcji znajdują się w Encyklopedii Delphi

A. Typ całkowity odpowiada liczbom całkowitym z odpowiednich przedziałów. Są dwa typy ogólne:

  • Integer (w systemach 32-bitowych przyjmuje wartość -2147483648..2147483647),
  • Cardinal (w systemach 32-bitowych przyjmuje wartość 0..4294967295),

i siedem podstawowych:

  • Shortint (-128..127)
  • Smallint (-32768..32767)
  • Longint (-2147483648..2147483647)
  • Int64 (-263..263-1)
  • Byte (0..255)
  • Word (0..65535)
  • Longword (0..4294967295)

    B. Typ znakowy gromadzi znaki w kilku standardach (ASCII, ANSI i Unicode)
    Podstawowymi typami są:
  • ANSIChar (zapisywany za pomocą 8 bitów. Zawiera 256 znaków w danym kodowaniu)
  • WideChar (zapisywany za pomocą 16 bitów. Zawiera 65536 znaków. Jest tylko jedno kodowanie. Pierwsze 256 znaków odpowiada znakom ANSIChar)

Ogólnym typem jest:

  • Char (odpowiada ANSIChar w kompilatorze Object Pascal z Delphi 6)
    Mając kod znaku można uzyskać znak używając funkcji Chr (patrz Encyklopedia Delphi)

    C. Typy boolowskie reprezentują wartości "prawda" (True) i "fałsz" (False)
  • Boolean (1 bajt)
  • ByteBool (1 bajt)
  • WordBool (2 bajty)
  • LongBool (4 bajty)

Podstawowym typem boolowskim w Delphi jest Boolean. Pozostałe typy: ByteBool, WordBool i LongBool są wprowadzone w celu zgodności z innymi językami. Typy boolowskie można traktować jako typy wyliczeniowe

type Boolean = (False, True); Ord(False) = 0; Ord(True) = 1; False < True; Pred(True) = False;
D. Typy wyliczeniowe są zbiorem dowolnych identyfikatorów. Jeżeli elementów jest mniej niż 256 to typ wyliczeniowy zajmuje 1 bajt. Jeżeli więcej to tym zajmuje 2 bajty (może być maksymalnie 65536 elementów). Np.:
type DniTygodnia = (Poniedzialek, Wtorek, Sroda, Czwartek, Piatek, Sobota, Niedziela);
Identyfikatorom tym przyporządkowywane są po kolei wartości od 0: Ord(Poniedziałek) = 0; Ord(Środa) = 2; Ord(Sobota) = 6); Można jednak zmienić domyślne przyporządkowanie wartości:
type DniTygodnia = (Poniedzialek = 1, Wtorek = 2, Sroda = Poniedzialek + Wtorek, Czwartek, Piatek = 4, Sobota, Niedziela = 0);
Ord(Poniedzialek) = 1; Ord(Sroda) = 3; Ord(Czwartek) = 4; Ord(Piatek) = 4; Zarówno wartość Czwartek jak i Piatek mają wartość 4, ponieważ Czwartek nie miał przyporządkowanej wartości, więc otrzymał taką wartość jak odpowiadałaby mu ze względu na kolejność. Piatek ma już przez nas przyporządkowaną wartość. Po znaku = musi pojawić się dowolne wyrażenie stałe.

Jeżeli zdefiniujmy typ:

type WartosciLogiczneSystemuKleenego = (Prawda = 5, Falsz = 10, Nieokreślone = 7);

a następnie zadeklarujemy

var x: WartościLogiczneSystemuKleenego;

To otrzymamy typ okrojony 5..10, którego wartości 5, 7 i 10 mają określone nazwy, ale istnieje też dostęp do pozostałych wartości (6, 8, 9).

E. Typy okrojone to typy postaci:

type TypOkrojony = NajmniejszaWartosc..NajwiekszaWartosc;

Typ okrojony zajmuje tyle samo miejsca w pamięci co typ bazowy, z którego powstał. Np. type Cyfry = 0..9; zajmuje 1 bajt.
Typem bazowym może być dowolny typ porządkowy.

Typy okrojone są wygodne przy określaniu zakresu danych, które może przyjąć funkcja/procedura.

2. Typy rzeczywiste reprezentują liczby zmiennoprzecinkowe (lub liczby zapisywane za pomocą cechy i mantysy).
Typy podstawowe:

Typ
Zakres
Real482.9 x 10-39 .. 1.7 x 1038
Single1.5 x 10-45 .. 3.4 x 1038
Double5.0 x 10-324 .. 1.7 x 10308
Extended3.6 x 10-4951 .. 1.1 x 104932
Comp-263+1 .. 263 -1
Currency-922337203685477.5808.. 922337203685477.5807

Typ ogólny (w kompilatorze Delphi 6)
Real = Double (5.0 x 10-324 .. 1.7 x 10308)

Comp jest specyficznym typem całkowitym. Należy do typów rzeczywistych tylko dlatego, że jest zapisywany w postaci cechy i mantysy i nie zachowuje się jak typ prządkowy ( nie można na nim zastosować Pred i Succ).
Currency jest typem stałoprzecinkowym w prowadzonym w celu dokładnych obliczeń na pieniądzach. Przyporządkowując wartości tego typu innym typom rzeczywistym wartość jest automatycznie mnożona lub dzielona przez 10000.

II. Typy łańcuchowe reprezentują zbiory typów znakowych.
Predefiniowane typy:

  • ShortString - maksymalnie 255 znaków ANSIChar(zajmuje od 2 do 256 bajtów)
  • AnsiString - maksymalnie ~2^31 znaków ANSIChar (zajmuje od 4 bajtów do 2GB)
  • WideString - maksymalnie ~2^30 znaków WideChar (zajmuje od 4 bajtów do 2GB)

    III. Elementami typów strukturalnych mogą być dowolne inne typy (wyjątek stanowią zbiory, których elementami mogą być jedynie typy porządkowe). W celu zaoszczędzenia miejsca (ale też spowolnienia dostępu do danych) przy deklaracji typu można użyć słowa kluczowego packed np.:
type Tablica = packed array [0..50] of Byte;
1. Zbiory posiadają nie więcej niż 256 elementów typu bazowego. Żaden element się nie powtarza. Typ bazowy musi być typem porządkowym o liczbie elementów nie większej niż 256 np.:
type Zbior = set of ‘a’..’z’; {zbiór małych liter alfabetu}
2. Tablice to uporządkowane zbiory elementów tego samego typu bazowego. Każdy element ma swój indeks, więc elementy mogą się powtarzać.

Są dwa rodzaje tablic:

  • statyczne:
type TStatyczna = array [TypPorzadkowy] of TypBazowy (TypPorzadkowy nie może być większy niż 2GB)
- dynamiczne:
type Dynamiczna = array of TypBazowy

Rozmiar tablicy dynamicznej ustawia się przez SetLength w trakcie działania programu.
Można również definiować tablice tablic, czyli tablice wielowymiarowe np.:

type Tablica3D = array [Boolean] of array [Byte] of array [0..10] of Char;
lub inaczej
type Tablica3D = array [Boolean, Byte, 0..10] of Char;

Ustawianie rozmiarów tablicy wielowymiarowej jest podobne:
SetLength(Talbica, LiczbaWierszy, LiczbaKolumn);

Co jednak zrobić, jeżeli potrzebujemy tablicy trójkątnej? Można zadeklarować tablicę kwadratową, ale zużywamy wówczas prawie dwa razy więcej pamięci. Może więc zadeklarujemy taką tablicę:

type TTab = array [0..10] of array of Byte;

Czyli mam hybrydę: tablicę statyczną, której elementami są tablice dynamiczne. Ponieważ są to tablice dynamiczne, to bez problemu każdej z nich możemy nadać rozmiar:

var Tab: TTab; i : Byte; begin for i := 0 to 10 do SetLength(Tab[i], i + 1); end;

Otrzymaliśmy więc taką tablicę:
K
KK
KKK
KKKK
.
.
.
KKKKKKKKKKK

K - komórka tablicy typu Byte;

No tak, ale jak nie wiemy jak wiele wierszy tablicy będziemy potrzebować? Może zadeklarujmy tablicę dynamiczną, której elementami będą tablice dynamiczne:

type TTab = array of array of Byte;

var
Tab: TTab;
i : Byte;
begin
{Możemy ustalić liczbę wierszy:}
SetLength(Tab, 20);
{Jak też liczbę kolumn w każdym wierszu:}
for i := 0 to 10 do
SetLength(Tab[i], Random(10));
end;

W ten sposób możemy tworzyć tablice w kształcie trapezu krzywoliniowego.

Specyficznym rodzajem tablic są tablice otwarte. Są przekazywane wyłącznie w procedurach i funkcjach. Nie mogą być definiowane przy użyciu type.
np:

function TabOtw(const A: array of Byte);
Żeby wywołać funkcję należy przekazać jej tablicę statyczną lub dynamiczną np: TabOtw([1,4,5]); lub
var T: array [0..3] </b>of Byte; begin TabOtw[T]; end;

Niestety tablice do tak zdefiniowanych funkcji mogą być przekazywane jedynie jako const. Co zrobić, jeżeli mamy za dużą tablicę statyczną?
Z pomocą przychodzi funkcja Slice, która przycina naszą tablicę do odpowiednich rozmiarów.

3. Rekordy to zbiory różnych pól.

type TMojRekord = record Pole1: Byte; Pole2: string[10]; JakiesInnePole: JakiegosTypu; end;

Co zrobić, jeżeli nie wszystkie dane w rekordach wykorzystujemy? Po co zużywać cenne miejsce? Możemy wówczas użyć rekordów z częścią wariantową:

type TRekordWariantowy = record Pole1: Typ1; case Pole0: TypPorzadkowy of Element1TypuPorz: (Pole2: Byte; Pole3: string[10]); Element2TypuPorz: (Pole4, Pole5, Pole6: Real); ... ElementNtyTypuPorz:(Pole7: comp); end;

W takim rekordzie mamy zawsze dostęp do Pole1, ale już z pozostałymi polami jest różnie. Możemy mieć albo dostęp do Pole2 i Pole3 albo do Pole4, Pole5, Pole6 albo do Pole7. Nigdy naraz. Pole2 i Pole3 zajmują te same miejsce w pamięci co Pole4, Pole5 i Pole6, a także Pole7. Obszar rezerwowanej pamięci jest równy większej grupie. Możemy wykorzystać to, że pola zajmują to samo miejsce w pamięci, aby mieć wygodny dostęp do niektórych danych:

type TimeStamp = record case Byte of 1: (Whole: comp); 2: (Lo, Hi: Longint); end;

Jak widać pominęliśmy Pole0. Jeżeli nie ma potrzeby nie musiymy go wykorzystywać (jest to pole zwykłe - zawsze dostępne). Nie wykorzystalićmy także wszystkich elementów typu Byte - nie musimy. Za to otrzymaliśmy bardzo wygodny dostęp do młodszej i starszej cześci zmiennej typu comp.
Jednym z większych ograniczeń w stosowaniu części wariantowych jest to, że można je umieszczać jedynie na końcu rekordu. Jednak i na to są sposoby - rekordy w rekordach:

type TTab2 = record case Byte of 1:(x2, y2: Byte); 2:(p2: Word); end; type TTab1 = record case Byte of 1:(x1, y1: Byte); 2:(p1: Word); end; type TTab = record T1: TTab1; T2: TTab2; end;


4. Pliki to uporządkowane zbiory elementów dowolnego typu, za wyjątkiem typów wskaźnikowych, dynamicznych tablic, długich łańcuchów (ANSIString i WideString), klas, variantów i rekordów zawierających takie typy.

type Plik = file of JakisTyp;
Istnieją także pliki o nieokreślonym typie:
type Plik = file;
5. Klasy - złożone obiekty zawierające właściwości, metody i zdarzenia i umożliwiające dziedziczenie
type MojaKlasa = class (KlasaPoKtorejDziedziczymy);
6. Referencje klas - typ zastępujący potomków klasy:
type Klasa = class of TObject;
Klasa może wskazywać na dowolny typ potomny klasy TObject; 7. Interfejs - złożony obiekt posiadający wiele funkcji. IV. Wskaźniki są typami, które podają adres zmiennej w pamięci. Wskaźniki mogą mieć typ odpowiadający zmiennej, na którą wskazują:
type PByte = ^TByte; {wskaźnik do zmiennej w pamięci typu Byte}
Predefiniowany typ wskaźnikowy, który nie rozróżnia rodzaju wskazywanych danych to Pointer. V. Typy proceduralne umożliwiają deklarowanie zmiennych wskazujących na procedury lub funkcje:
type TMojaFunkcja = function (const S: string): Integer;
Typy proceduralne umożliwiają traktowanie funkcji jak zwykłych typów i przekazywanie ich jako parametrów. W rzeczywistości są to wskaźniki do funkcji. Jeżeli jednak chcemy użyć typów proceduralnych w klasach to musimy je zdefiniować dodając wyrażenie of object na końcu.
type TMojaFunkcja = function (const S: string): Integer of object;
VI. Typu Variant używa się, jeżeli nie wiadomo, jakiego rodzaju dane zostaną podstawione w czasie wykonywania programu pod zmienną. Typ Variant zapewnia także wygodną konwersję i definiowanie operacji.

8 komentarzy

Niepoprawne linki do "SetLength" i "Slice".

świetny artykuł. bardzo użyteczny.

//dop.
chociaż przydałoby się napisać trochę więcej o zbiorach.

Co zrobić, jeżeli nie wszystkie dane w rekordach wykorzystujemy? Po co zużywać cenne miejsce? Możemy wówczas użyć rekordów z częścią wariantową

Dodać należy, że rekord zajmuje tyle miejsca w pamięci, ile jego największy (pod względem zajętości pamięci) wariant + rozmiar części niewariantowej.

dry... artykuł cienki jak barszcz :PPPP ;)

O tablicach otwartych jest troszke niescisle, bo do procedury z (otwartym) parametrem tablicowym mozna rowniez przekazac tablice dynamiczna. Vide http://www.4programmers.net/forum/viewtopic.php?id=30914 .

No tak! Postarales sie!
Ode mnie też 6 :]

No, to sie nazywa artykul. Ode mnie 6!

Świetny, systematyczny artykuł. Ale paru drobiazgów zabrakło:
tablice otwarte
nieprostokątne tablice dynamiczne
packed record i jego znaczenie przy operacjach plikowych
rekordy wariantowe

może jak będę miał czas do dopiszę, ale i tak jest rewelacyjnie!!!!