Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-pascal / 2022-01

Strange behavior in generics.collections TDictionary

2 messages · 2 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Thomas Kurz

Consider the following code:

***
PROGRAM project1;
{$mode objfpc}
{$longstrings on} // see output below
{$modeswitch advancedrecords}
USES Variants, Generics.Collections, SysUtils;
TYPE TRecord = PACKED RECORD
  FID: NativeUInt;
  FKey: String;
  CONSTRUCTOR Create (AID: NativeUInt; AKey: String);
END;
CONSTRUCTOR TRecord.Create (AID: NativeUInt; AKey: String);
BEGIN
  FID := AID;
  FKey := UpperCase (AKey);
END;
VAR
  Dict: SPECIALIZE TDictionary<TRecord,Variant>;
  i: SPECIALIZE TPair<TRecord,Variant>;
BEGIN
  Dict := SPECIALIZE TDictionary<TRecord,Variant>.Create;
  Dict.Add (TRecord.Create (1, 'test'), 1);
  FOR i IN Dict DO Writeln (i.Key.FID, #9, i.Key.FKey, #9, i.Value);
  // ^^^ 1 TEST 1
  // -> so the entry is ok!
  Writeln (Dict.ContainsKey (TRecord.Create (1, 'test')));
  // ^^^ with longstrings on -> FALSE
  //     with longstrings off -> TRUE
  Writeln (Dict.ContainsKey (TRecord.Create (1, 'TEST')));
  // ^^^ always FALSE
  Dict.Free;
END.
***

I'm very confused... I have no idea if I'm overseeing something or this is a bug in generics.collections or in the compiler.

My system is:
Lazarus 2.2.0 (64 bit), FPC 3.2.2 (64 bit) on Windows 8.1 (64 bit)

Reply by email

Sven Barth
to Thomas Kurz
Thomas Kurz wrote 43 lines
Consider the following code: *** PROGRAM project1; {$mode objfpc} {$longstrings on} // see output below {$modeswitch advancedrecords} USES Variants, Generics.Collections, SysUtils; TYPE TRecord = PACKED RECORD FID: NativeUInt; FKey: String; CONSTRUCTOR Create (AID: NativeUInt; AKey: String); END; CONSTRUCTOR TRecord.Create (AID: NativeUInt; AKey: String); BEGIN FID := AID; FKey := UpperCase (AKey); END; VAR Dict: SPECIALIZE TDictionary<TRecord,Variant>; i: SPECIALIZE TPair<TRecord,Variant>; BEGIN Dict := SPECIALIZE TDictionary<TRecord,Variant>.Create; Dict.Add (TRecord.Create (1, 'test'), 1); FOR i IN Dict DO Writeln (i.Key.FID, #9, i.Key.FKey, #9, i.Value); // ^^^ 1 TEST 1 // -> so the entry is ok! Writeln (Dict.ContainsKey (TRecord.Create (1, 'test'))); // ^^^ with longstrings on -> FALSE // with longstrings off -> TRUE Writeln (Dict.ContainsKey (TRecord.Create (1, 'TEST'))); // ^^^ always FALSE Dict.Free; END. *** I'm very confused... I have no idea if I'm overseeing something or this is a bug in generics.collections or in the compiler.

This is by design, Delphi behaves the same here. The point is that the default comparer used for records uses binary comparison and in case of an AnsiString that will be a pointer while with a ShortString this will be the string contents.

The solution is to create a comparer for your custom key:

=== code begin ===

type
   TRecordComparer = class(TEqualityComparer<TRecord>)
     function Equals(constref Left, Right: TRecord): Boolean; override;
     function GetHashCode(constref Value: TRecord): UInt32; override;
   end;
function TRecordComparer.Equals(constref Left, Right: TRecord): Boolean;
begin
   Result := (Left.FID = Right.FID) and (Left.FKey = Right.FKey);
end;
function TRecordComparer.GetHashCode(constref Value: TRecord): UInt32;
begin
   Result := BobJenkinsHash(PChar(Value.FKey)^, Length(Value.FKey) *
SizeOf(Char), 0);
   Result := BobJenkinsHash(Value.FID, SizeOf(NativeUInt), Result);
end;
begin
   // and then:
   Dict := specialize
TDictionary<TRecord,Variant>.Create(TRecordComparer.Create);
   // and now the remaining code works
end.

=== code end ===

Regards,
Sven

Reply by email