Programowanie w języku Delphi » Artykuły

Tablice asocjacyjne

  • 2010-10-31 18:27
  • 6 komentarzy
  • 1203 odsłony
  • Oceń ten tekst jako pierwszy

Strona w budowie
Ktoś pracuje nad tą stroną, jej zawartość może się wkrótce zmienić. Prosimy o cierpliwość!




Spis treści

     1 Słowo od autora
          1.1 Zastosowanie
          1.2 Wymagania
          1.3 Możliwości
     2 Jak się do tego zabrać
          2.1 Tworzenie i niszczenie
          2.2 Standardowe klasy
               2.2.1 Przypisywanie i odczytywanie wartości
               2.2.2 Wiele wymiarów
               2.2.3 Pobieranie wartości bez znajomości klucza
                    2.2.3.1 ForEach
                    2.2.3.2 ForEachLevel
          2.3 Własne klasy dziedziczące z TCustomAssocArray
               2.3.1 Dodawanie większej ilości pól oraz funkcji pomocniczych
               2.3.2 Zmiana typu klucza
                    2.3.2.1 Tablica ignorująca wielkość znaków
                    2.3.2.2 Tablica o indeksie liczbowym (Integer, Real)
     3 Dodatki
          3.1 Odnośniki
          3.2 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.

        Lista potrzebnych rzeczy:
  • 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 wymiary

Jak 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


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 (string)
  • TAssocInt – pole .Value jest typu liczbowego (Integer)
  • TAssocPtr – pole .Value jest typu wskaźnikowego (Pointer)
  • TAssocVar – w tym przypadku, pole .Value jest typu 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:
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:
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ć:
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:
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:
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;


Zmiana typu klucza


        Taka zmiana polega tylko na odpowiedniej zmianie funkcji GetItem() oraz, w niektórych przypadkach właściwości Items[]. Więcej w poniższych przykładach.

Tablica ignorująca wielkość znaków


        To bardzo prosta modyfikacja, jedyne co musimy zmienić to wnętrze funkcji GetItem() z naszego szablonu:
function TMojAssoc.GetItem(const AName: string): TMojAssoc;
begin
  Result := TMojAssoc(inherited GetItem(AName));
end;

        na odpowiedni, zmieniający wartość klucza AName na małe (lub duże) litery:
function TMojAssoc.GetItem(const AName: string): TMojAssoc;
begin
  Result := TMojAssoc(inherited GetItem(AnsiLowerCase(AName)));
  // z użyciem funkcji AnsiLowerCase która zmieni wszystkie litery na ich 'małe' odpowiedniki
end;

        Prawda, że nic trudnego? :)

Tablica o indeksie liczbowym (Integer, Real)


        Dla przykładu, wybrałem typ Integer, jednak można w taki sposób wybrać dowolny typ który będzie nam służył jako klucz.
        W wypadku zmiany typu klucza, należy zmienić trochę więcej rzeczy, zaczynając oczywiście od definicji klasy (naszego szablonu):
type
  TMojAssoc = class(TCustomAssocArray)
  private
    function GetItem(const AName: Integer): TMojAssoc; reintroduce;
  public
// [...]
    property Items[const AName: Integer]: TMojAssoc read GetItem; default;
  end;

        Zmieniliśmy tutaj tylko i wyłącznie typ AName w funkcji GetItem() oraz w właściwości Items[], dzięki temu nie będzie potrzebne jakieś brzydkie rzutowanie w czasie codziennego używania.
        Teraz czas na wnętrze funkcji GetItem() które również musimy zmienić:
function TMojAssoc.GetItem(const AName: Integer): TMojAssoc;
begin
  Result := TMojAssoc(inherited GetItem(IntToStr(AName))); 
  // można tutaj zastosować coś innego.. 
  // ja wybrałem najprostszy sposób i zmieniam typ liczbowy na tekstowy
end;

        Dzięki takiej zmianie, możliwe będzie teraz odwoływanie się do tablicy za pomocą indeksu liczbowego, np:
Tab[123].Wartosc1 := 123;

        Jednak, tak na prawdę, nadal będziemy się posługiwać nazwą typu string. Powyższy przykład będzie widziany w ten sposób:
Tab['123'].Wartosc1 := 123;

Przy takich zmianach, używając pętli ForEach lub ForEachLevel, Klucz.Name nadal będzie typu string!

Dodatki


Odnośniki


Starsza wersja


TO-DO


  • powstawiać linki w tekście
  • zaktualizować kod, aktualnie żeby działało w Delphi potrzebna jest zmiana podana w komantarzach
  • brakujące działy

Download


Wersja 1.4: assoca.zip (4,84 KB)

6 komentarzy

markac 2007-07-29 19:37

Co zrobić, aby funkcje ForEachLevel itp. działały pod Delphi? W tej chwili po wywołaniu np. Length jest zwis całego programu.

markac 2007-07-19 21:15

Dlaczego nie można pobrać pliku?

Marooned 2006-12-28 20:05

pomogło! zmieniałem to nie tam gdzie trzeba, thx

bidok 2006-12-28 19:49

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ł.

Marooned 2006-12-27 22:31

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?

Blek Medżik 2009-12-13 14:48

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;