Using TDictionary for Hash Tables in Delphi

TDictionary example in Delphi
TDictionary example in Delphi

Introduced in Delphi 2009, the TDictionary class, defined in the Generics.Collections unit, represents a generic hash table type collection of key-value pairs.

Generic types, also introduced in Delphi 2009, allow you to define classes that don't specifically define the type of data members.

A dictionary is, in a way, similar to an array. In an array you work with a series (collection) of values indexed by an integer value, which can be any ordinal type value.

This index has a lower and an upper bound.

In a dictionary you can store keys and values where either can be of any type.

The TDictionary Constructor

Hence the declaration of the TDictionary constructor:

  TDictionary<TKey, TValue>.Create;

In Delphi, the TDictionary is defined as a hash table. Hash tables represent a collection of key-and-value pairs that are organized based on the hash code of the key. Hash tables are optimized for lookups (speed). When a key-value pair is added to a hash table, the hash of the key is computed and stored along with the added pair.

The TKey and TValue, because they're generics, can be of any type. For example, if the information you are to store in the dictionary is coming from some database, your Key can be a GUID (or some other value presenting the unique index) value while the Value can be an object mapped to a row of data in your database tables.

Using TDictionary

For the sake of simplicity the example below uses integers for TKeys and chars for TValues.

 

// // "log" is a TMemo control placed on a form //
var
  dict : TDictionary<integer, char>;
  sortedDictKeys : TList<integer>;
  i, rnd : integer;
  c : char;
begin
  log.Clear;
  log.Text := 'TDictionary usage samples';

  Randomize;

  dict := TDictionary<integer, char>.Create;
  try
    //add some key/value pairs (random integers, random characters from A in ASCII)
    for i := 1 to 20 do
    begin
      rnd := Random(30);

      if NOT dict.ContainsKey(rnd) then dict.Add(rnd, Char(65 + rnd));
    end;

    //remove some key/value pairs (random integers, random characters from A in ASCII)
    for i := 1 to 20 do
    begin
      rnd := Random(30);

      dict.Remove(rnd);
    end;

    //loop elements - go through keys
    log.Lines.Add('ELEMENTS:');
    for i in dict.Keys do
      log.Lines.Add(Format('%d, %s', [i, dict.Items[i]]));

    //do we have a "special" key value
    if dict.TryGetValue(80, c) then
      log.Lines.Add(Format('Found "special", value: %s', [c]))
    else
      log.Lines.Add(Format('"Special" key not found', []));

    //sort by keys ascending
    log.Lines.Add('KEYS SORTED ASCENDING:');
    sortedDictKeys := TList.Create(dict.Keys);
    try
      sortedDictKeys.Sort; //default ascending
      for i in sortedDictKeys do
        log.Lines.Add(Format('%d, %s', [i, dict.Items[i]]));
    finally
      sortedDictKeys.Free;
    end;

    //sort by keys descending
    log.Lines.Add('KEYS SORTED DESCENDING:');
    sortedDictKeys := TList.Create(dict.Keys);
    try
      sortedDictKeys.Sort(TComparer.Construct(
       function (const L, R: integer): integer
       begin
         result := R - L;
       end
       )) ;
      for i in sortedDictKeys do
        log.Lines.Add(Format('%d, %s', [i, dict.Items[i]]));
    finally
      sortedDictKeys.Free;
    end;

  finally
    dict.Free;
  end;
end;

First, we declare our dictionary by specifying what the types of the TKey and TValue will be:

dict : TDictionary;

Then the dictionary is filled using the Add method. Becuase a dictionary cannot have two pairs with the same Key value, you can use the ContainsKey method to check if some key-valued pair is already inside the dictionary.

To remove a pair from the dictionary, use the Remove method. This method will not cause problems if a pair with a specified key is not a part of the dictionary.

To go through all the pairs by looping through keys you can do a for in loop.

Use the TryGetValue method to check if some key-value pair is included in the dictionary.

Sorting The Dictionary

Because a dictionary is a hash table it does not store items in a defined sort order. To iterate through the keys that are sorted to meet your specific need, take advantage of the TList -- a generic collection type that supports sorting.

The code above sorts keys ascending and descending and grabs values as if they were stored in the sorted order in the dictionary. The descending sorting of integer-type Key values uses TComparer and an anonymous method.

When Keys And Values Are Of TObject Type

The example listed above is a simple one because both the key and the value are simple types.

You can have complex dictionaries where both the key and the value are "complex" types like records or objects.

Here's another example:

type
  TMyRecord = record
    Name, Surname : string
  end;

  TMyObject = class(TObject)
    Year, Value : integer;
  end;

procedure TForm2.logDblClick(Sender: TObject);
var
  dict : TObjectDictionary<TMyRecord, TMyObject>;
  myR : TmyRecord;
  myO : TMyObject;
begin
  dict := TObjectDictionary<TMyRecord, TMyObject>.Create([doOwnsValues]);

  try
    myR.Name := 'Zarko';
    myR.Surname := 'Gajic';

    myO := TMyObject.Create;
    myO.Year := 2012;
    myO.Value := 39;

    dict.Add(myR, myO);

    myR.Name := 'Zarko';
    myR.Surname := '?????';
		
    if NOT dict.ContainsKey(myR) then log.Lines.Add('not found');
  finally
    dict.Free;
  end;
end;

Here a custom record is used for the Key and a custom object/class is used for the value.

Note the usage of a specialized TObjectDictionary class here. TObjectDictionary can handle objects' lifetime automatically.

The Key value cannot be nil, while the Value value can.

When a TObjectDictionary is instantiated, an Ownerships parameter specifies whether the dictionary owns the keys, values or both -- and therefore helps you not have memory leaks.