7

When working with lists of items where the lists just serve as a temporary container - which list types would you recommend me to use?

I

  • don't want to destroy the list manually
  • would like to use a built-in list type (no frameworks, libraries, ...)
  • want generics

Something which would make this possible without causing leaks:

function GetListWithItems: ISomeList;
begin
  Result := TSomeList.Create;
  // add items to list
end;

var
  Item: TSomeType;
begin
  for Item in GetListWithItems do
  begin
    // do something
  end;
end;

What options do I have? This is about Delphi 2009 but for the sake of knowledge please also mention if there is something new in this regard in 2010+.

Heinrich Ulbricht
  • 10,064
  • 4
  • 54
  • 85
  • If there was a built-in type I'd like to use it. I just wanted to check that there is nothing I don't know of ;) I found it hard to believe there is no such type because since long-time-ago I need it again and again. Now I decided to ask. If you guys don't know then I take it as proof of its non-existence. – Heinrich Ulbricht Nov 10 '11 at 09:57
  • Good question : really a lack in Delphi. I use a lot of tStringList/TList in D7, TDictionnary seems a little progress. – philnext Nov 10 '11 at 10:53
  • 1
    @philnext, what is "really lacking" in Delphi? – Cosmin Prund Nov 10 '11 at 11:50
  • @CosminPrund I guess the "don't want to destroy the list manually" part is lacking at least...I remember some record wrapper implementation for `TStringList` though from the old codegear forum. – jpfollenius Nov 11 '11 at 09:51

7 Answers7

6

An (somehow ugly) workaround for this is to create an 'autodestroy' interface along with the list. It must have the same scope so that when the interface is released, your list is destroyed too.

type
  IAutoDestroyObject = interface
  end;

  TAutoDestroyObject = class(TInterfacedObject, IAutoDestroyObject)
  strict private
    FValue: TObject;
  public
    constructor Create(obj: TObject);
    destructor  Destroy; override;
  end;

constructor TAutoDestroyObject.Create(obj: TObject);
begin
  inherited Create;
  FValue := obj;
end; 

destructor TAutoDestroyObject.Destroy;
begin
  FreeAndNil(FValue);
  inherited;
end;

function CreateAutoDestroyObject(obj: TObject): IAutoDestroyObject;
begin
  Result := TAutoDestroyObject.Create(obj);
end; 

FList := TObjectList.Create;
FListAutoDestroy := CreateAutoDestroyObject(FList);

Your usage example gets more complicated, too.

type
  TSomeListWrap = record
    List: TSomeList;
    AutoDestroy: IAutoDestroyObject;
  end;

function GetListWithItems: TSomeListWrap;
begin
  Result.List := TSomeList.Create;
  Result.AutoDestroy := CreateAutoDestroyObject(Result.List);
  // add items to list
end;

var
  Item: TSomeItem;
begin
  for Item in GetListWithItems.List do
  begin
    // do something
  end;
end;
gabr
  • 26,580
  • 9
  • 75
  • 141
  • Will it work? In your sample code, since Result.AutoDestroy is not free (it will never be out of scope), the associated IAutoDestroyObject won't be free. – Arnaud Bouchez Nov 10 '11 at 11:24
  • 2
    @ArnaudBouchez it works just fine. Result of GetListWithItems gets out of scope at the 'end' part of the begin..end surrounding the for..in loop. – gabr Nov 10 '11 at 11:45
  • Really, Gabr? Delphi will free the temporary before the end of the function? That's new to me. Usually, temporaries are freed at the end of the function, or if they're re-used in a separate block of code elsewhere in the function (such as if there were *two* function calls that returned TSomeListWrap without storing it in a variable). – Rob Kennedy Nov 10 '11 at 13:32
  • No, in reality temporary will be freed at the end of the function, but in this case there was no function, just a begin..end block so I couldn't be more specific. – gabr Nov 10 '11 at 19:45
5

Inspired by Barry Kelly's blog post here you could implement smart pointers for your purpose like this :

unit Unit80;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Generics.Collections;

type
  TMyList =class( TList<Integer>)
  public
    destructor Destroy; override;
  end;

  TLifetimeWatcher = class(TInterfacedObject)
  private
    FWhenDone: TProc;
  public
    constructor Create(const AWhenDone: TProc);
    destructor Destroy; override;
  end;

  TSmartPointer<T: class> = record
  strict private
    FValue: T;
    FLifetime: IInterface;
  public
    constructor Create(const AValue: T); overload;
    class operator Implicit(const AValue: T): TSmartPointer<T>;
    property Value: T read FValue;
  end;

  TForm80 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    function getList : TSmartPointer<TMyList>;
    { Public declarations }
  end;


var
  Form80: TForm80;

implementation


{$R *.dfm}



{ TLifetimeWatcher }

constructor TLifetimeWatcher.Create(const AWhenDone: TProc);
begin
  FWhenDone := AWhenDone;
end;

destructor TLifetimeWatcher.Destroy;
begin
  if Assigned(FWhenDone) then
    FWhenDone;
  inherited;
end;

{ TSmartPointer<T> }

constructor TSmartPointer<T>.Create(const AValue: T);
begin
  FValue := AValue;
  FLifetime := TLifetimeWatcher.Create(procedure
  begin
    AValue.Free;
  end);
end;

class operator TSmartPointer<T>.Implicit(const AValue: T): TSmartPointer<T>;
begin
  Result := TSmartPointer<T>.Create(AValue);
end;

procedure TForm80.Button1Click(Sender: TObject);
 var i: Integer;
begin
  for I in getList.Value do
   Memo1.Lines.Add(IntToStr(i));

end;

{ TMyList }

destructor TMyList.Destroy;
begin
  ShowMessage('Kaputt');
  inherited;
end;

function TForm80.getList: TSmartPointer<TMyList>;
var
  x: TSmartPointer<TMyList>;
begin
  x := TMyList.Create;
  Result := x;
  with Result.Value do
  begin
    Add(1);
    Add(2);
    Add(3);
  end;
end;

end.

Look at getList and Button1click to see its usage.

Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
iamjoosy
  • 3,299
  • 20
  • 30
  • @TOndrej, thanks should actually go to Barry Kelly. I just adapted his example a bit to better fit the needs of the OP. – iamjoosy Nov 10 '11 at 14:42
  • Understood, and thanks for the link and your code. I remember briefly seeing the article, later I completely forgot about it. (I fixed the link to point directly to the article.) – Ondrej Kelle Nov 10 '11 at 14:49
4

To fully support what you're after the language would need to support 2 things:

  • Garbage collector. That's the only thing that gives you the freedom to USE something without bothering with freeing it. I'd welcome a change in Delphi that gave us even partial support for this.
  • The possibility to define local, initialized variables. Again, I'd really love to see something along those lines.

Meanwhile, the closest you can get is to use Interfaces in place of garbage collection (because interfaces are reference-counted, once they go out of scope they'll be released). As for initialized local variables, you could use a trick similar to what I'm describing here: Declaring block level variables for branches in delphi

And for the sake of fun, here's a Console application that demonstrates the use of "fake" local variables and Interfaces to obtain temporary lists that are readily initialized will be automatically freed:

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

type

  ITemporaryLocalVar<T:constructor> = interface
    function GetL:T;
    property L:T read GetL;
  end;

  TTemporaryLocalVar<T:constructor> = class(TInterfacedObject, ITemporaryLocalVar<T>)
  public
    FL: T;

    constructor Create;
    destructor Destroy;override;

    function GetL:T;
  end;

  TTempUse = class
  public
    class function L<T:constructor>: ITemporaryLocalVar<T>;
  end;

{ TTemporaryLocalVar<T> }

constructor TTemporaryLocalVar<T>.Create;
begin
  FL := T.Create;
end;

destructor TTemporaryLocalVar<T>.Destroy;
begin
  TObject(FL).Free;
  inherited;
end;

function TTemporaryLocalVar<T>.GetL: T;
begin
  Result := FL;
end;

{ TTempUse }

class function TTempUse.L<T>: ITemporaryLocalVar<T>;
begin
  Result := TTemporaryLocalVar<T>.Create;
end;

var i:Integer;
begin
  try
    with TTempUse.L<TList<Integer>> do
    begin
      L.Add(1);
      L.Add(2);
      L.Add(3);
      for i in L do
        WriteLn(i);
    end;
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Community
  • 1
  • 1
Cosmin Prund
  • 25,498
  • 2
  • 60
  • 104
  • @gabr, It also works for anything that has a `Create` constructor, not only lists. – Cosmin Prund Nov 10 '11 at 12:06
  • This could be it. But unfortunately it doesn't compile in D2009 :( `TObject(FL).Free;`: "[DCC Error] E2089 Invalid typecast" I'm experimenting with some pointer magic right now trying to make it work... – Heinrich Ulbricht Nov 10 '11 at 12:11
  • Apparently Delphi 2010 got smarter; Since `T` is constrained to be of `constructor` type, it's obviously a `TObject` descendent. Glad you found a workaround that also works on D2009! – Cosmin Prund Nov 10 '11 at 12:33
3

The standard list classes, like TList, TObjectList, TInterfaceList, etc, do not implement automated lifecycles, so you have to free them manually when you are done using them. If you want a list class that is accessible via an interface, you have to implement that yourself, eg:

type
  IListIntf = interface
    ...
  end;

  TListImpl = class(TInterfacedObject, IListIntf)
  private
    FList: TList;
    ...
  public
    constructor Create; override;
    destructor Destroy; override;
    ...
  end;

constructor TListImpl.Create;
begin
  inherited;
  FList := TList.Create;
end;

destructor TListImpl.Destroy;
begin
  FList.Free;
  inherited;
end;

function GetListWithItems: IListIntf;
begin
  Result := TListImpl.Create;
  // add items to list
end;   
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 2
    "Implement yourself" unfortunately is the ultimate solution to every problem. I hoped buried in the RTL somewhere might wait something for me... Nevertheless thanks for the answer! – Heinrich Ulbricht Nov 10 '11 at 09:42
  • I thought that a Generic TObjectList did always own its contained objects. Are you talking about the LIST freeing ITSELF, or freeing its owned objects alone? – Warren P Nov 10 '11 at 16:14
2

Another option is to implement a generic IEnumerable adapter (as one of the ways to satisfy the for .. in compiler requirement) and rely on reference counting of the interface. I don't know if the following works in Delphi 2009, it seems to work in Delphi XE:

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes,
  Generics.Collections;

type
  // IEnumerator adapter for TEnumerator
  TInterfacedEnumerator<T> = class(TInterfacedObject, IEnumerator<T>)
  private
    FEnumerator: TEnumerator<T>;
  public
    constructor Create(AEnumerator: TEnumerator<T>);
    destructor Destroy; override;
    function IEnumerator<T>.GetCurrent = GetCurrent2;
    { IEnumerator }
    function GetCurrent: TObject;
    function MoveNext: Boolean;
    procedure Reset;
    { IEnumerator<T> }
    function GetCurrent2: T;
  end;

  // procedure used to fill the list    
  TListInitProc<T> = reference to procedure(List: TList<T>);

  // IEnumerable adapter for TEnumerable
  TInterfacedEnumerable<T> = class(TInterfacedObject, IEnumerable<T>)
  private
    FEnumerable: TEnumerable<T>;
  public
    constructor Create(AEnumerable: TEnumerable<T>);
    destructor Destroy; override;
    class function Construct(InitProc: TListInitProc<T>): IEnumerable<T>;
    function IEnumerable<T>.GetEnumerator = GetEnumerator2;
    { IEnumerable }
    function GetEnumerator: IEnumerator; overload;
    { IEnumerable<T> }
    function GetEnumerator2: IEnumerator<T>; overload;
  end;

{ TInterfacedEnumerator<T> }

constructor TInterfacedEnumerator<T>.Create(AEnumerator: TEnumerator<T>);
begin
  inherited Create;
  FEnumerator := AEnumerator;
end;

destructor TInterfacedEnumerator<T>.Destroy;
begin
  FEnumerator.Free;
  inherited Destroy;
end;

function TInterfacedEnumerator<T>.GetCurrent: TObject;
begin
  Result := TObject(GetCurrent2);
end;

function TInterfacedEnumerator<T>.GetCurrent2: T;
begin
  Result := FEnumerator.Current;
end;

function TInterfacedEnumerator<T>.MoveNext: Boolean;
begin
  Result := FEnumerator.MoveNext;
end;

procedure TInterfacedEnumerator<T>.Reset;
begin
  // ?
end;

{ TInterfacedEnumerable<T> }

class function TInterfacedEnumerable<T>.Construct(InitProc: TListInitProc<T>): IEnumerable<T>;
var
  List: TList<T>;
begin
  List := TList<T>.Create;
  try
    if Assigned(InitProc) then
      InitProc(List);
    Result := Create(List);
  except
    List.Free;
    raise;
  end;
end;

constructor TInterfacedEnumerable<T>.Create(AEnumerable: TEnumerable<T>);
begin
  inherited Create;
  FEnumerable := AEnumerable;
end;

destructor TInterfacedEnumerable<T>.Destroy;
begin
  FEnumerable.Free;
  inherited Destroy;
end;

function TInterfacedEnumerable<T>.GetEnumerator: IEnumerator;
begin
  Result := GetEnumerator2;
end;

function TInterfacedEnumerable<T>.GetEnumerator2: IEnumerator<T>;
begin
  Result := TInterfacedEnumerator<T>.Create(FEnumerable.GetEnumerator);
end;

type
  TSomeType = record
    X, Y: Integer;
  end;

function GetList(InitProc: TListInitProc<TSomeType>): IEnumerable<TSomeType>;
begin
  Result := TInterfacedEnumerable<TSomeType>.Construct(InitProc);
end;

procedure MyInitList(List: TList<TSomeType>);
var
  NewItem: TSomeType;
  I: Integer;
begin
  for I := 0 to 9 do
  begin
    NewItem.X := I;
    NewItem.Y := 9 - I;
    List.Add(NewItem);
  end;
end;

procedure Main;
var
  Item: TSomeType;
begin
  for Item in GetList(MyInitList) do // you could also use an anonymous procedure here
    Writeln(Format('X = %d, Y = %d', [Item.X, Item.Y]));
  Readln;
end;

begin
  try
    ReportMemoryLeaksOnShutdown := True;
    Main;
  except
    on E: Exception do
    begin
      ExitCode := 1;
      Writeln(Format('[%s] %s', [E.ClassName, E.Message]));
    end;
  end;
end.
Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
  • +1 Interesting...even if I'm not sure if that's really worth the effort. I think it is much less readable with the list initialization in another function or in an anonymous method. – jpfollenius Nov 11 '11 at 09:57
  • @Smasher Thanks! A separate initialization method is not required; you could rewrite `GetList` to create *and* fill the list, too. – Ondrej Kelle Nov 11 '11 at 10:14
1

No, not 'out of the box' in Delphi. I know that you don't need a library but you may be interessed by the principle of TDynArray.

philnext
  • 3,242
  • 5
  • 39
  • 62
  • By design, Dynamic Arrays will be auto-destroyed, and the TDynArray / TDynArrayHashed wrappers will provide all TList-like methods (and much more, like hashing and serialization) to your code. But this is not a list of objects, but records (so it won't handle inheritance and such). But works from Delphi 5 up to XE2. This can be used for your purpose, but it is not `generics`. – Arnaud Bouchez Nov 10 '11 at 11:26
  • See http://stackoverflow.com/questions/5536022/delphi-hashmap#5536647 for how you may use this. – Arnaud Bouchez Nov 10 '11 at 11:32
1

In Jedi Code Library, exist the Guard function that already implements what Gabr's code does.

Fabricio Araujo
  • 3,810
  • 3
  • 28
  • 43
  • I am frequently astonished about JCL/JVCL and what is in it! Unfortunately the documantation is so bad, that it is not easy to find out. – iamjoosy Nov 10 '11 at 22:41
  • In truth, I've implemented that myself (since here on job we use very few external libraries) before knowing - on another question here on SO. – Fabricio Araujo Nov 17 '11 at 17:32