I found that Delphi generics TObjectList with an attached TComparer behaves very strange. I struggle into that problem while switching my code from normal TObjectList to TObjectList.

I found that Delphi generics TObjectList with an attached TComparer behaves very strange. I struggle into that problem while switching my code from normal TObjectList to TObjectList.

The heavy to understand background is the fact that the Remove(const Value: T) suddenly uses "undocumented" the TComparer and not the pointer. That works fine as long as there are not "key doubles" in the list. I know this sounds rather technical and I have prepared a small sample to make the problem visible.

RunTest(False) works RunTest(True) fails

Perhaps I should stop using TObjectList.Create(const AComparer: IComparer; AOwnsObjects: Boolean)


---
unit uTest;
interface
uses classes;

type

TPerson = class(TObject)
public
Name: string;
RecID: integer;
constructor Create(AName: string; ARecID: integer);
end;

TMyTest = class(TObject)
procedure RunTest(AAddComparer: boolean);
end;

implementation

uses system.Generics.Collections, system.Generics.Defaults, sysUtils;

procedure TMyTest.RunTest(AAddComparer: boolean);
var
oList: TObjectList;
oPers: TPerson;
iRemovedIdx, iLast: integer;
comparison: TComparison;
begin

if AAddComparer then
begin
comparison:=
function(const left, right: TPerson): Integer
begin
Result:= CompareText(left.Name,right.Name);
end;
oList:= TObjectList.Create(TComparer.Construct(comparison),True);
end
else begin
oList:= TObjectList.Create;
end;

try
oList.Add(TPerson.Create('Tom',1));
oList.Add(TPerson.Create('Tom',2));
iLast:= oList.Count-1;
oPers:= oList[iLast];
Assert(oPers.RecID = 2); // was the last record select?
iRemovedIdx:= oList.Remove(oPers);
Assert(iRemovedIdx=iLast) // was the last record deleted?
finally
oList.Free;
end;
end;

constructor TPerson.Create(AName: string; ARecID: integer);
begin
inherited Create;
Name:= AName;
RecID:= ARecID;
end;
end.

Comments

  1. I'm not sure I'd call this undocumented.

    The help for Remove() says "Remove first occurrence of value" while the help for the constructor says "AComparer is an equality comparator function. If not provided, the default comparator for the type is used".

    For me it's pretty clear the comparer is used to find the element in Remove(), and the default comparer for an object just compares the pointers.

    ReplyDelete
  2. Asbjørn Heid Yes and no. I see your point but could argue that method using a compare should always have an overload to specify a specific one. As

    procedure Sort(const AComparer: IComparer); overload;
    function BinarySearch(...IComparer): Boolean; overload;

    But adding all this methods would bloat the generics again to much.
    I think I have to live with this trap.

    thank you for the feedback

    ReplyDelete
  3. Günther Schoch Yeah, that's fair. IndexOf() for example should absolutely have an overloaded version taking an IComparer, and so on.

    ReplyDelete

Post a Comment