Search code examples
delphigenericstlistdelphi-xe8

Delphi XE8 bug in TList<T>, need workaround


After upgrading to XE8 some of our projects start to break data. Looks like a bug in TList realization.

program XE8Bug1;
{$APPTYPE CONSOLE}

uses
  System.SysUtils, Generics.Collections;

type
  TRecord = record
    A: Integer;
    B: Int64;
  end;

var
  FRecord: TRecord;
  FList: TList<TRecord>;

begin
  FList := TList<TRecord>.Create;
  FRecord.A := 1;
  FList.Insert(0, FRecord);
  FRecord.A := 3;
  FList.Insert(1, FRecord);
  FRecord.A := 2;
  FList.Insert(1, FRecord);
  Writeln(IntToStr(FList[0].A) + IntToStr(FList[1].A) + IntToStr(FList[2].A));

end.

This code prints "123" in XE7 and before (as it should be), but in XE8 it prints "120". Maybe someone know a quickfix for this?

Update: unofficial fix is here


Solution

  • I found that now the TList<T>.Insert method call TListHelper.InternalInsertX depends on the data size, in my case:

    procedure TListHelper.InternalInsertN(AIndex: Integer; const Value);
    var
      ElemSize: Integer;
    begin
      CheckInsertRange(AIndex);
    
      InternalGrowCheck(FCount + 1);
      ElemSize := ElSize;
      if AIndex <> FCount then
        Move(PByte(FItems^)[AIndex * ElemSize], PByte(FItems^)[(AIndex * ElemSize) + 1], (FCount - AIndex) * ElemSize);
      Move(Value, PByte(FItems^)[AIndex * ElemSize], ElemSize);
      Inc(FCount);
      FNotify(Value, cnAdded);
    end;
    

    I see the problem in the first Move call. Destination should be:

    PByte(FItems^)[(AIndex + 1) * ElemSize]
    

    not

    PByte(FItems^)[(AIndex * ElemSize) + 1]
    

    Aaargh!

    Finally, I've used the System.Generics.Defaults.pas and System.Generics.Collections.pas units from Delphi XE7 in my projects, and now all works as expected.

    Update: as I see, RTL not affected, as it isn't use TList<T>.Insert for T with SizeOf > 8 (or maybe I miss something?)