Tablice asocjacyjne w Delphi

bidok

Wstęp

"Tablica asocjacyjna (mapa, słownik) (ang. associative array, map, dictionary) to pojemnik zawierający wartości indeksowane przez klucze. [...] Wiele złożonych danych jest naturalnie reprezentowanych przez tego typu tablice - np. drzewa plików, nagłówki poczty, a nawet wszystkie atrybuty obiektu czy przestrzeń nazw zmiennych."[#]_

Jak wszyscy wiemy Delphi nie oferuje w standardzie takiego typu tablicy, który co niektórym jest dobrze znany na przykład z php. Zastosowanie tablicy asocjacyjnej w bardzo wielu sytuacjach ułatwia prace programiście i właśnie dlatego napisałem odpowienią klasę którą chciałbym się podzielić.

Zastosowanie

<font color="red">Nowe!</span> Ustawienia:
Od wersji 1.1 jest zaimplementowany przełącznik łatwo pozwalający zmienić niektóre zasady działania klasy. Teraz zamiast wskaźników można stosować wygodniejsze varianty. To w jaki sposób chce się korzystać zależy tylko od własnych upodobań. Aby "Przełączyć" wystarczy odkomentować jedna linie w AssocArray.pas tak jak niżej:

{ varianty wyłączone, pointery włączone }
// {$DEFINE USE_VARIANTS}
{ varianty włączone, pointery wyłączone }
{$DEFINE USE_VARIANTS}

Tworzenie i usuwanie:
Listę tworzy się jak każdą inna klasę i tak samo się usuwa (należy jednak pamiętać by nie zostawiać po sobie w pamięci tego co utworzliśmy za pomocą New lub GetMem).

var
  Lista: TAssocArray;
begin 
  Lista := TAssocArray.Create;
  // [...]
  Lista.Destroy;
end;

Zapis danych:
Zapis (lub modyfikowanie) listy odbywa się w taki sposób:

Lista[Klucz] := Wskaznik;
// lub (w zależności czy mamy ustawione USE_VARIANTS )
Lista[Klucz] := Zmienna;

Nie musimy się martwić czy dany klucz istnieje czy też nie, tak samo nie musimy martwić się czy nasza lista będzie miałą wystarczającą wielkość. W razie potrzeby lista sama zarezerwuje odpowiednią ilość pamięci.

Odczyt danych:
Analogicznie do zapisu

Wskaznik := Lista[Klucz];
// lub (w zależności czy mamy ustawione USE_VARIANTS )
Zmienna := Lista[Klucz];

W tym miejscu należy jednak pamiętać, że Lista[Klucz] może zwrócić wartość Nil gdy nie istnieje żadna wartość pod danym kluczem.

Usuwanie elementu listy:
Aby usunąć element listy gdy nie jest nam już potrzebny wykonujemy prostą operację przypisania wartości Nil lub Null dla danego klucza:

Lista[Klucz] := nil;
// lub (w zależności czy mamy ustawione USE_VARIANTS )
Lista[Klucz] := Null;
// Null można stosować również gdy use_variants jest wyłączone (const null = nil;) jednak gdy jest włączone null jest funkcją która zwraca Variant będący nil.. pokręcone ale musi tak być

Spowoduje to usunięcie i zwolnienie pamięci zajmowanej przez element. Lista będzie mniejsza, bardziej czysta i szybsza.

Gdy chcemy usunąć wszystkie elementy wystarczy użyć procedury

Lista.Clear;

Pętla
By odczytać wszystkie wartości listy najwygodniej użyć pętli While i funkcji Lista.GetNextItem, tak jak w kodzie niżej:

var
  Temp: PAssocItem;
begin
  Temp := nil;
  while Lista.GetNextItem(Temp) do
  begin
    // dane które przypisaliśmy do Lista[Klucz] znajdują się w Temp^.Value
    // w Temp^.Name jest Klucz pod którym dane występują na liście
    // Temp^.Next jest wskaźnikiem do następnego elementu listy
    // dla zapewnienia stabilności lepiej tego ostatniego nie modyfikowac ^_^
  end;
end;

Możliwe klucze:
Kluczem dla listy mogą być liczby całkowite, liczby rzeczywiste oraz tekst na przykład kod:

// Gdy USE_VARIANTS jest wyłączony
const
  s1 = 'string numer 1';
  s2 = 'string numer 2';
  s3 = 123456789;
begin
  Lista['Kowalski'] := @s1;
  Lista[123456] := @s2;
  Lista[3.14] := @s3;
end;
// Gdy USE_VARIANTS jest włączony
begin
  Lista['Nazwisko'] := 'Jan Marian';
  Lista[123] := 'Przykładowy tekst';
  Lista[3.14] := Integer(@s2);
end;

jest jak najbardziej poprawny.

Przykładowy kod

Zdaję sobie sprawę, że czasami o wiele lepiej człowiek uczy się z przykładu, więc takowy zamieszczam.

type
  PDane = ^TDane;
  TDane = record
    Imie: string;
    Nazwisko: string;
    NrButa: integer;
  end;
 
var
  Lista: TAssocArray; // nie zapomnieć utworzyć!
 
// zwróci false jesli user juz istnieje
function AddUser(UID, Imie, Nazwisko: string; NrButa: Integer): Boolean;
var
  Nowy: PDane;
begin
  Result := False;
  if Lista[UID] <> nil then Exit;
 
  New(Nowy);
 
  Nowy^.Imie := Imie;
  Nowy^.Nazwisko := Nazwisko;
  Nowy^.NrButa := NrButa;
 
  Lista[UID] := Nowy;
 
  Result := True;
end;
 
// zmienia dane usera.. jesli nie istnieje to go dodaje i zwraca false
function ModifyUser(UID, Imie, Nazwisko: string; NrButa: Integer): Boolean;
var
  Temp: PDane;
begin
  Temp := PDane(Lista[UID]);
  if Temp = nil then
  begin
    AddUser(UID, Imie, Nazwisko, NrButa);
    Result := False;
  end;
 
  Temp^.Imie := Imie;
  Temp^.Nazwisko := Nazwisko;
  Temp^.NrButa := NrButa;
  Result := True;
end;
 
// zmienia tylko nr buta, pod warunkiem ze user istnieje
procedure ZmienNrButa(UID: string; NrButa: Integer);
begin
  if Lista[UID] <> nil then
     PDane(Lista[UID])^.NrButa := NrButa;
end;
 
// usuwa usera
function DeleteUser(UID: string): Boolean;
var
  Temp: PDane;
begin
  Temp := Lista[UID];
  if Temp <> nil then
  begin
    Dispose(Temp);
    Lista[UID] := nil;
    Result := True;
  end else
    Result := False;
end;
 
procedure ListUsers(var L: TStrings);
var
  Temp: PAssocItem;
  Dane: PDane;
begin
  L.Clear;
 
  Temp := nil;
  while Lista.GetNextItem(Temp) do
  begin
    Dane := PDane(Temp^.Value);
    L.Add(Format('[%s]  Imie: %s, Nazwisko: %s, Numer buta: %d', [Temp^.Name, Dane^.Imie, Dane^.Nazwisko, Dane^.NrButa]));
  end;
end;

Załącznik

AssocArray.zip
AssocArray1.1.zip


.. [#] Źródło: <wiki href="Tablica_asocjacyjna">Wikipedia</wiki>

4 komentarzy

Dzięki wielkie, naprawdę mi się przydało :)

Jak tylko dojde do tego jak podmienic załącznik to umieszcze nowsza wersje..
edit: zrobione

nie zawsze jest to wygodne w używaniu - te pointery, no ale ogólnie to mechanizm chyba nawet sprawny (GetItem itp)

Mim zdaniem to bardziej art niż gotowiec :/ Ale nie będę przenosił, bo jeszcze jakiś mod mnie okrzyczy albo coś :) :)