14

Can you use a record as a Key value in TDictionary? I want to find objects based on combination of string, integer and integer.

TUserParKey=record
  App:string;
  ID:integer;
  Nr:integer;
end;

...

var
  tmpKey:TUserParKey;
  tmpObject:TObject;
begin
  tmpObject:= TTObject.Create(1); 
  tmpKey.App:='1';
  tmpKey.ID :=1;
  tmpKey.Nr :=1;

  DTUserPars.Add(tmpKey,tmpObject)

...

var
  tmpKey:TUserParKey;
begin
  tmpKey.App:='1';
  tmpKey.ID :=1;
  tmpKey.Nr :=1;

  if not DTUserPars.TryGetValue(tmpKey,Result) then begin
    result := TTObject.Create(2); 
  end;

This returns object 2.

Stefan Glienke
  • 20,860
  • 2
  • 48
  • 102
r_j
  • 1,348
  • 15
  • 35

3 Answers3

21

Yes, you can use records as keys in a TDictionary but you should provide your own IEqualityComparer when creating the dictionary because the default one for records just does a dumb binary compare of the record. This fails for a record containing a string because it just compares the pointer of that string which may be different even if the string contains the same value.

Such a comparer would look like this:

type
  TUserParKeyComparer = class(TEqualityComparer<TUserParKey>)
    function Equals(const Left, Right: TUserParKey): Boolean; override;
    function GetHashCode(const Value: TUserParKey): Integer; override;
  end;

function TUserParKeyComparer.Equals(const Left, Right: TUserParKey): Boolean;
begin
  Result := (Left.App = Right.App) and (Left.ID = Right.ID) and (Left.Nr = Right.Nr);
end;

function TUserParKeyComparer.GetHashCode(const Value: TUserParKey): Integer;
begin
  Result := BobJenkinsHash(PChar(Value.App)^, Length(Value.App) * SizeOf(Char), 0);
  Result := BobJenkinsHash(Value.ID, SizeOf(Integer), Result);
  Result := BobJenkinsHash(Value.Nr, SizeOf(Integer), Result);
end;
Stefan Glienke
  • 20,860
  • 2
  • 48
  • 102
  • thnx for the code. What is the Bobjenkinshash? and why is it needed? My hash looks like this. tmpStr:=''; for I := 1 to Value.App.length do tmpStr:=tmpStr + inttostr(ord(Value.app[i])); tmpStr:= tmpStr+inttostr(Value.ID)+inttostr(Value.Nr); result:= StrToIntDef(tmpStr,-1); – r_j Jan 07 '15 at 13:40
  • 1
    It's from Generics.Defaults and used for all the GetHashCode functions in the comparers. Your code will easily fail for various reasons - please don't do it like that. – Stefan Glienke Jan 07 '15 at 13:44
  • This will lead to range check error for empty strings with range checking enabled. It would be nice to do thois without having to call BobJenkinsHash which feels quite low level. It would be more concise to use anon methods and Construct rather than declaring a class. Sometimes that feels better. +1 – David Heffernan Jan 07 '15 at 21:38
  • Thanks for the post but this solution fails for records that contain an empty string, because `Value.App[1]` throws a RangeError. – DBedrenko Feb 22 '16 at 15:22
  • @NewWorld I leave that as an exercise to the reader to add any validation code. – Stefan Glienke Feb 22 '16 at 15:29
  • @StefanGlienke I don't know how exactly hashes work and this is quite a low-level solution, but the question did ask specifically about strings so I wonder if you can help. Should (and can) an empty string be part of a hash calculation? Or should the validation skip the string field of a record if the string is empty? – DBedrenko Feb 22 '16 at 16:50
  • 1
    @newworld: It depends on what an empty sting means in your application. Either you cancel the search because you don't have all searching information or you assume empty string=empty string and omit it from the comparison. – MichaSchumann Mar 03 '16 at 07:59
  • 3
    Instead of `BobJenkinsHash(Value.App[1], Length(Value.App) * SizeOf(Char), 0)` you can use `BobJenkinsHash(PChar(Value.App)^, Length(Value.App) * SizeOf(Char), 0)`. It works also in case of empty string. – Grzegorz Skoczylas Mar 24 '16 at 11:22
  • 1
    BobJenkinsHash is deprecated. You can now use THashBobJenkins.GetHashValue from System.Hash. – Toon Krijthe Jul 28 '20 at 12:47
0

Instead of using the record as a key, you could use a string consisting of the serialized record. You could use something like https://github.com/hgourvest/superobject to do the serialization.

Since strings have built-in comparison semantics and hashcodes, you don't need to write comparison and hashcode functions.

JoelFan
  • 37,465
  • 35
  • 132
  • 205
-1

My best approach should be to joint the default hash code of the base types.

For instance:

Value.App.GetHashCode + Value.ID.GetHashCode + Value.Nr.GetHashCode;
Pingolin
  • 3,161
  • 6
  • 25
  • 40