Tablice asocjacyjne
bidok
Ktoś pracuje nad tą stroną, jej zawartość może się wkrótce zmienić. Prosimy o cierpliwość!
1 Słowo od autora
1.1 Zastosowanie
1.2 Wymagania
1.3 Możliwości
2 Jak się do tego zabrać
2.4 Tworzenie i niszczenie
2.5 Standardowe klasy
2.5.1 Przypisywanie i odczytywanie wartości
2.5.2 Wiele wymiarów
2.5.3 Pobieranie wartości bez znajomości klucza
2.5.3.1 ForEach
2.5.3.2 ForEachLevel
2.6 Własne klasy dziedziczące z TCustomAssocArray
2.6.4 Dodawanie większej ilości pól oraz funkcji pomocniczych
2.6.5 Zmiana typu klucza
2.6.5.3 Tablica ignorująca wielkość znaków
2.6.5.4 Tablica o indeksie liczbowym (Integer, Real)
3 Dodatki
3.7 Odnośniki
3.8 Starsza wersja
4 TO-DO
5 Download
Słowo od autora
Witaj, powodów dla których znalazłeś się właśnie na tej stronie może być wiele, począwszy od przypadkowego kliknięcia w wyszukiwarce, poprzez chęć zobaczenia “co nowego” w serwisie (tudzież w tym artykule), a skończywszy na potrzebie uzupełnienia języka jakim jest Object Pascal w dodatkową funkcje, funkcje którą oferuje właśnie moja klasa. Jeśli nie wiesz do końca, na czym działanie tablic asocjacyjnych polega, już teraz odsyłam Cię do części Dodatki w której znajdziesz interesujące odnośniki. W zakamarkach internetu można znaleźć wiele przykładów implementacji takich tablic. Ja w tym właśnie artykule, postaram się przybliżyć, czemu właśnie moja klasa a nie inna. Zapraszam!Zastosowanie
Osób które miały styczność z PHP, C++ czy też Javą nie muszę przekonywać o możliwościach które daje nam właśnie taki typ tablic. Natomiast osób zaznajomionych z naszym ulubionym językiem, również nie muszę informować o tym, iż w Object Pascalu takich tablic po prostu brak w standardzie. Tablicę taką można użyć do obsługi języków w naszej aplikacji, przechowywania ustawień programu czy też prostej bazy danych.. tak naprawdę możliwości są nieograniczone. Wszystko zależy od Twojej wyobraźni oraz problemu jaki musisz rozwiązać.Wymagania
To, co różni moją klasę od innych dostępnych w internecie, to między innymi właśnie wymagania. By użyć TAssocArray nie musisz dodawać żadnych wielkich, dodatkowych unitów do swojej listy uses. Nie musisz używać żadnych obejść używanych przy TstringListach, jedyne czego potrzebujesz, to kompilator. TAssocArray działa z Delphi, Free Pascal Compiler (FPC) i bardzo możliwe (choć nie sprawdzone) z innymi dostępnymi kompilatorami języka Object Pascal. Kod jest niezależny od platformy na której pracujemy. Dla przykładu, wszystkie przykłady będę kompilował na systemie Ubuntu 6.06.<b>Lista potrzebnych rzeczy:</b>
- Kompilator (Delphi, FPC, inne (nie sprawdzone))
- IDE lub edytor tekstu (Delphi, Kylix, Lazarus, FpcIde, vi, gedit, kate, notatnik – co kto woli)
- I najważniejsze: chęć ;)
Możliwości
Podobnie jak zastosowanie, możliwości są praktycznie nieograniczone, oczywiście pomijając ograniczenia, jakie narzuca nam używany przez nas kompilator. Do głównych zalet klasy, którą opisuję należą: * szybkość, * wielkość, * wielowymiarowość (spróbujcie to osiągnąć kiedyś z wykorzystaniem TStringListy...) * elastyczność – można napisać wiele własnych klas pochodnych, bez martwienia się “jak to wszystko działa” * automatyczne tworzenie i zwalnianie kluczy z pamięci – my martwimy się tylko o .Create i .Free głównego obiektu. * jeden klucz nie musi odpowiadać jednej wartości – każdy klucz, może mieć tyle pól ile jest nam potrzeba.. oprócz tego, może zawierać również kolejne wymiaryJak się do tego zabrać
TAssocArray jest niczym więcej jak zwykłą klasą napisaną z wykorzystaniem najprostszych struktur języka. Używamy jej tak samo jak każdej innej klasy. Przed rozpoczęciem należy klasę stworzyć za pomocą .Create, po skończeniu zwolnić za pomocą .Free.Tworzenie i niszczenie
```delphi var Tab: TAssocStr; begin Tab := TAssocStr.Create; // działania na tablicy Tab.Free; end; ``` Jak widać, do tego miejsca pojawiło się niewiele różnic.Standardowe klasy
W poprzednim przykładzie wykorzystałem klasę TAssocStr, jest to jedna ze standardowych klas napisanych przeze mnie i dostarczona razem z TAssocArray. Wszystkie różnią się typem wartości jakie mogą przechowywać (w polu Value). Tak więc: * TAssocStr – w polu .Value można zapisywać łańcuchy tekstowe ([[Delphi/string]]) * TAssocInt – pole .Value jest typu liczbowego ([[Delphi/Integer]]) * TAssocPtr – pole .Value jest typu wskaźnikowego ([[Delphi/Pointer]]) * TAssocVar – w tym przypadku, pole .Value jest typu [[Delphi/Variant]] dzięki czemu możemy pod dany klucz zapisać praktycznie każdy typ.Przypisywanie i odczytywanie wartości
Przy tworzeniu opisywanej klasy postawiłem sobie parę warunków – jednym z nich było możliwe jak najbliższe odwzorowanie zwykłych tablic dostępnych w Object Pascalu. Myślę, że nawet początkujący znajdzie tutaj parę podobieństw.Na pierwszy ogień przykład:
var
Tab: TAssocStr; // wartości będą typu string
begin
Tab := TAssocStr.Create; // tworzenie tablicy
// przypisywanie
Tab['klucz 1'].Value := 'wartosc 1';
Tab['klucz 2'].Value := 'wartosc 2';
// odczytywanie
Zmienna1 := Tab['klucz 1'].Value;
Zmienna2 := Tab['klucz 2'].Value;
Tab.Free; // zwalnianie tablicy jak i wszystkich jej elementów
end;
Jak widzimy, odczyt i zapis wartości odbywa się dosyć intuicyjnie. Sądzę, że ze zrozumieniem tego kodu nie będzie większych problemów, dlatego przejdę dalej.
Wiele wymiarów
To chyba jedna z większych zalet TAssocArray, jak do tej pory nie spotkałem się z implementacją tablic asocjacyjnych w Object Pascalu pozwalających na wykorzystanie wielu wymiarów.. choć może słabo szukałem? Obsługa jest o wiele prostsza niż można by przypuszczać. W żadnym miejscu nie musimy określać ile nasza tablica takich wymiarów będzie posiadać, wszystko dzieje się auto-magicznie a rozmiar tablicy jest ograniczony jedynie wielkością pamięci dostępnej na danym komputerze.Przykład:
var
Tab: TAssocInt; // Tym razem, Value będzie typu Integer
begin
Tab := TAssocInt.Create;
Tab['klucz 1']['pod klucz 1'].Value := 1;
Tab['klucz 1']['pod klucz 2'].Value := 2;
Tab['klucz 2']['pod klucz 1'].Value := 3;
Tab['klucz 2']['pod klucz 2'].Value := 4;
Tab['klucz 1'].Value := 5;
Tab['klucz 2']['pod klucz 3']['...']['pod klucz (n-1)']['pod klucz (n)'].Value := MaxInt;
Tab.Free;
end;
Jak widać, nigdzie nie podałem docelowej ilości wymiarów. W żadnym miejscu nie musiałem również tworzyć kolejnych wymiarów – wszystko odbywa się automatycznie przy pierwszym użyciu.
Pobieranie wartości bez znajomości klucza
Brzmi to dziwnie ale osoby mające styczność juz wcześniej z takim typem tablic na pewno nie raz używały odpowiednich pętli dzięki którym można było wykonać odpowiednie czynności na wszystkich komórkach tabeli. W Object Pascalu nie ma odpowiedniej pętli 'foreach' która mogła by w tym miejscu pomóc, jednak z pomocą przychodzą odpowiednie funkcje dostępne w klasie.ForEach
Dzięki tej funkcji przy każdym obrocie pętli otrzymujemy kolejny klucz razem z jego wartością (jednak tylko i wyłącznie w danym wymiarze)var
Tab: TAssocInt;
Klucz: TAssocInt; // zmienna potrzebna do funkcji
begin
Tab := TAssocInt.Create;
// przypisanie paru wartosci
Tab['klucz 1'].Value := 1;
Tab['klucz 2'].Value := 2;
Tab['klucz 3'].Value := 3;
// dodatkowy wymiar...
Tab['klucz 1']['pod klucz 1'].Value := 10;
Tab['klucz 1']['pod klucz 2'].Value := 20;
Tab['klucz 1']['pod klucz 3'].Value := 30;
Tab['klucz 1']['pod klucz 2']['pod pod klucz'].Value := 123;
// część właściwa przykładu:
Klucz := nil; // konieczne jest wyzerowanie lub przypisanie odpowiedniej wartości przed wejściem do pętli
while Tab.ForEach(Klucz) do
begin
WriteLn(Klucz.Name, ' = ', Klucz.Value);
// w Klucz.Name jak idzie się domyślić znajduje się nazwa danego klucza
// a w Klucz.Value jego wartość
end;
WriteLn; // mały odstęp dla czytelności
// wariant drugi, rozpoczęcie od pewnego elementu
Klucz := Tab['klucz 1']; // przy podaniu takiej wartości pętla rozpocznie się od NASTĘPNEGO elementu...
while Tab.ForEach(Klucz) do
begin
WriteLn(Klucz.Name, ' = ', Klucz.Value);
end;
// aby temu zapobiec można użyć w tym miejscu pętli repeat-until z dodatkowym sprawdzeniem czy Klucz jest różny od nil
Tab.Free;
end;
Po kompilacji takiego kodu, powinniśmy w konsoli uzyskać taki oto wynik:
ForEachLevel
Ta funkcja jest bardzo podobna do poprzedniej, z tą różnicą, że zwraca każdy klucz i wartość w każdym wymiarze. Aby to zilustrować, zmodyfikujmy trochę poprzedni przykład dodając przed końcem dodatkową pętlę:[...]
Klucz := nil;
while Tab.ForEachLevel(Klucz) do
begin
Write(DoIndent(Klucz.Level)); // funkcja DoIndent zmienia liczbę na odpowiednią ilość spacji.. dzięki temu uzyskamy efekt 'wcięcia'
// w Klucz.Level znajduje się aktualny stopień zagłębienia danego klucza, czym większy tym większy wymiar
WriteLn(Klucz.Name, ' = ', Klucz.Value);
end;
Tab.Free;
end;
Tym razem po kompilacji, oprócz wyniku który pojawił się w poprzednim przykładzie powinniśmy otrzymać dodatkowy, wyglądający tak:
Własne klasy dziedziczące z TCustomAssocArray
Tak, TAssocArray został specjalnie zbudowany w taki sposób by z małym wysiłkiem można było budować własne tablice, pozwalające przechowywać nie tylko jedną czy dwie wartości ale również funkcje oraz procedury. Klasa TCustomAssocArray znajduje się w pliku “assocabase.pas” i ten plik właśnie musimy dodać do naszego bloku uses. Drugą czynnością będzie napisanie odpowiedniej klasy, w poniższych przykładach postaram się przybliżyć mechanizm działania. Na początek, stworzymy szablon który będzie nam służył w dalszych przykładach. Każda klasa pochodna od TCustomAssocArray musi napisać dwie funkcje: Create() oraz GetItem() dzięki czemu będzie możliwe działanie tablicy. Oto kod: ```delphi type TMojAssoc = class(TCustomAssocArray) private function GetItem(const AName: string): TMojAssoc; reintroduce; public constructor Create; property Items[const AName: string]: TMojAssoc read GetItem; default; end; ``` Tak powinna wyglądać podstawowa definicja nowej klasy opartej na TCustomAssocArray. W dalszej części kodu powinniśmy również umieścić następujący kod: ```delphi constructor TMojAssoc.Create; begin inherited Create; // wywołujemy funkcje .Create klasy z której dziedziczymy... MagicClass := TMojAssoc; // To jedna z najważniejszych czynności end;function TMojAssoc.GetItem(const AName: string): TMojAssoc;
begin
Result := TMojAssoc(inherited GetItem(AName)); // odpowiednie rzutowanie...
end;
Najważniejsze, co musimy zrobić to ustawić MagicClass, jest to typ jakiego będą odpowiednie “dzieci” tablicy (czyli kolejne wymiary). Jeśli tego nie zrobimy zapewne dostaniemy ładny Access Violation.
Kolejną rzeczą jaką potrzebujemy w standardzie to odpowiednie rzutowania w GetItem – nie jest to może mocno potrzebne ale na pewno sprawia, że kod będzie o wiele ładniejszy przy codziennym używaniu naszej klasy. W końcu, lepiej napisać:
```delphi
Tab['klucz']['klucz'].Pole := Wartosc;
niż:
TMojAssoc(Tab['klucz']['klucz']).Pole := Wartosc;
Dodawanie większej ilości pól oraz funkcji pomocniczych
Mając już gotowy szablon możemy już przejść do bardziej interesujących nas rzeczy – mianowicie jak przy pomocy jednego klucza zapisywać wiele wartości oraz funkcji. By nie zaśmiecać artykułu zbyt wielką ilością kodu, umieszczę tylko zmiany w definicji klasy: ```delphi type TMojAssoc = class(TCustomAssocArray) private FWartosc1: Integer; FWartosc2: string; // + funkcje z szablonu public // + funkcje z szablonu function GetWartosci: string;property Wartosc1: Integer read FWartosc1 write FWartosc1;
property Wartosc2: string read FWartosc2 write FWartosc2;
end;
[...]
function TMojAssoc.GetWartosci: string;
begin
Result := IntToStr(Wartosc1) + ' (' + Wartosc2 + ')';
end;
Jak widać, nic trudnego. Postępujemy tak, jakbyśmy pisali własną klasę. Dodajemy tyle pól i funkcji ile potrzebujemy. Aby użyć tego, co dodaliśmy wystarczy zastosować taki kod:
```delphi
var
Tab: TMojAssoc;
begin
Tab := TMojAssoc.Create;
// przypisanie wartosci
Tab['klucz 1'].Wartosc1 := 123;
Tab['klucz 1'].Wartosc2 := 'raz dwa trzy';
Tab['klucz 2'].Wartosc1 := 456;
Tab['klucz 2'].Wartosc2 := 'cztery pięć sześć';
// odczytanie
WriteLn(Tab['klucz 1'].Wartosc1); // lub, używając naszej funkcji:
WriteLn(Tab['klucz 2'].GetWartosci);
Tab.Free;
end;
Co zrobić, aby funkcje ForEachLevel itp. działały pod Delphi? W tej chwili po wywołaniu np. Length jest zwis całego programu.
Dlaczego nie można pobrać pliku?
pomogło! zmieniałem to nie tam gdzie trzeba, thx
Napisalem, ze kod moze ale nie musi sie kompilowac pod Delphi.. jego wczesniejsza wersja dzialala dobrze...
Sproboj zmienic deklaracje GetItem(AName: string) na GetItem(const AName: string) powinno przejsc ta linijke...
Co dziwne/ciekawe, FPC o takie "coś" spokojnie obchodzi ;)
Co do połączenia.. jest to możliwe, jednak wersja wcześniejsza jest wg mnie zupełnie inna.. od implementacji aż do używania.. pomyśli się nad tym.
Po nowym roku powinienem znalesc troche wiecej wolnego czasu i dopracowac zarowno dzialanie kodu pod Delphi jak i ten artykuł.
Coś, czego mi zawsze brakuje.. niedługo użyję, mam nadzieję, że ładnie śmiga
jaka licencja? bo używam w większym projekcie [nie czerpię korzyści, darmowy]
[edited]
ok, info w pliku .pas
"This code is free only for non-commercial usage :)"
Nazwa metody "HaveChilds" niezbyt fortunna, gdyż forma mnoga od "child" to.. "children" a nie "childs" ;-)
[edited2]
property Items[const AName: string]: TCustomAssocArray read GetItem; default;
ta linijka zwraca błąd:
[Error] assocabase.pas(63): Incompatible types
może dodaj do archiwum skompilowanego unita?
Zmieniam deklarację tak jak mówisz, muszę też jeszcze zmienić to w wywołaniu funkcji
function TCustomAssocArray.GetItem(const AName: string): TCustomAssocArray;
to wyskakuje mi błąd tu:
property Items[const AName: string]: TAssocInt read GetItem; default;