tags:

views:

294

answers:

5

I currently use a record to pass several result parameters for a function and need to add some more data as it follows:

type
  TItemType = (itFile, itRegistry);

  TItemDetails = record
    Success: Boolean;
    ItemType: TItemType;
    TotalCount: Integer;
    TotalSize: Int64;
    List: TStringList;
  end;

function DoSomething: TItemDetails;

Is it possible/advisable to use a TStringList inside a record for this specific case?

I found on Embarcadero Developer Network a class that allows to declare StringList instead of TStringList and takes care of creating and freeing the list. Would this be an advisable solution? http://cc.embarcadero.com/Item/25670

Also, if this does indeed works, will I have to manually free the TStringList?

+3  A: 

It will work, but you'll have to free it manually. And since records clean themselves up automatically when they go out of scope, and don't have destructors, making sure you do it right can be a hassle. You're better off not using objects in records. If you need a data type that contains objects, why not make it an object too?

Mason Wheeler
There are workarounds. An interface inside a record will be managed correctly. You can than hook TStringList destruction to destruction of the object implementing this interface.
gabr
+1 I often found myself changing records to classes due to exactly this reason. That's why I tend to use records only for data structures that are a) very simple (no object members) and b) very unlikely to be extended (to avoid later refactorings)
Smasher
A: 

Why not use something like

type PStringList = ^TStringList;
type TMyFreakyRecord = record
         PointerToAStringList : PStringList;
// some more code here
end;

...
var x : TMyFreakyRecord;
    stringlist : TStringList;
begin
     stringList := TStringlist.create;
     stringList.Add('any data you wish');
     x.PointertoaStringList := @stringlist;
// some more code here
end;

and access the record's string list like

procedure ProcedureThatPasses(AFreakyRecord: TFreakyRecord);
var i : integer;
begin
     for i := 0 to AFreakyRecord.PointerToAStringList.count -1 do
       // something with AFreakyRecord.PointerToAStringList[i];
end;

in order to transparently free the memory allocated you can create a TList variable in which you add every variable of type TStringList that is used inside a record,

var frmMain : TfrmMain;
    MyJunkList : TList;

...
implementation
...
procedure clearjunk;
var i : integer;
  o : TObject;
begin
     for i := MyJunkList.count -1 downto 0 do begin
           o := MyJunkList[i];
           FreeandNil(o);
     end;
MyJunkList.clear;
end;
...
initialization
              MyJunkList := TList.Create;
finalization
            clearjunk;
            FreeAndNil(MyJunkList );
end. // end of unit

if this helps, don't hesitate to visit http://delphigeist.blogspot.com/

delphigeist
I don't know if this kind of advertisement (the link to your blog after all your answers) is helpful. Just put the link in your profile and stop adding noise to your answers.
Smasher
Although the compiler is smart enough to deal with it I prefer to correctly derefence the pointer (AFreakyRecord.PointerToAStringList^.count instead of AFreakyRecord.PointerToAStringList.count)
Remko
+4  A: 

Yes, by all means, just be aware that if the record goes out of scope, then it looses the reference to the object (unless you add code otherwise).

I've used that StringList example you are referring too, and that works great to have a record manage the lifetime of a TStringList. You can adapt that to your usage. The key is the embedded Interface which frees the object when it goes out of scope with the record.

You can also look at Allen Bauer's Nullable record example. I included the code, but you will want to read the article (and comments) too. It uses Generics in Delphi 2009 or newer, but you can adapt it to earlier versions of Delphi. Again the key is the interface, but he takes a different approach.

unit Foo;

interface

uses Generics.Defaults, SysUtils;

type
  Nullable<T> = record
  private
    FValue: T;
    FHasValue: IInterface;
    function GetValue: T;
    function GetHasValue: Boolean;
  public
    constructor Create(AValue: T);
    function GetValueOrDefault: T; overload;
    function GetValueOrDefault(Default: T): T; overload;
    property HasValue: Boolean read GetHasValue;
    property Value: T read GetValue;

    class operator NotEqual(ALeft, ARight: Nullable<T>): Boolean;
    class operator Equal(ALeft, ARight: Nullable<T>): Boolean;

    class operator Implicit(Value: Nullable<T>): T;
    class operator Implicit(Value: T): Nullable<T>;
    class operator Explicit(Value: Nullable<T>): T;
  end;

procedure SetFlagInterface(var Intf: IInterface);

implementation

function NopAddref(inst: Pointer): Integer; stdcall;
begin
  Result := -1;
end;

function NopRelease(inst: Pointer): Integer; stdcall;
begin
  Result := -1;
end;

function NopQueryInterface(inst: Pointer; const IID: TGUID; out Obj): HResult; stdcall;
begin
  Result := E_NOINTERFACE;
end;

const
  FlagInterfaceVTable: array[0..2] of Pointer =
  (
    @NopQueryInterface,
    @NopAddref,
    @NopRelease
  );

  FlagInterfaceInstance: Pointer = @FlagInterfaceVTable;

procedure SetFlatInterface(var Intf: IInterface);
begin
  Intf := IInterface(@FlagInterfaceInstance);
end;

{ Nullable<T> }

constructor Nullable<T>.Create(AValue: T);
begin
  FValue := AValue;
  SetFlagInterface(FHasValue);
end;

class operator Nullable<T>.Equal(ALeft, ARight: Nullable<T>): Boolean;
var
  Comparer: IEqualityComparer<T>;
begin
  if ALeft.HasValue and ARight.HasValue then
  begin
    Comparer := TEqualityComparer<T>.Default;
    Result := Comparer.Equals(ALeft.Value, ARight.Value);
  end else
    Result := ALeft.HasValue = ARight.HasValue;
end;

class operator Nullable<T>.Explicit(Value: Nullable<T>): T;
begin
  Result := Value.Value;
end;

function Nullable<T>.GetHasValue: Boolean;
begin
  Result := FHasValue <> nil;
end;

function Nullable<T>.GetValue: T;
begin
  if not HasValue then
    raise Exception.Create('Invalid operation, Nullable type has no value');
  Result := FValue;
end;

function Nullable<T>.GetValueOrDefault: T;
begin
  if HasValue then
    Result := FValue
  else
    Result := Default(T);
end;

function Nullable<T>.GetValueOrDefault(Default: T): T;
begin
  if not HasValue then
    Result := Default
  else
    Result := FValue;
end;

class operator Nullable<T>.Implicit(Value: Nullable<T>): T;
begin
  Result := Value.Value;
end;

class operator Nullable<T>.Implicit(Value: T): Nullable<T>;
begin
  Result := Nullable<T>.Create(Value);
end;

class operator Nullable<T>.NotEqual(const ALeft, ARight: Nullable<T>): Boolean;
var
  Comparer: IEqualityComparer<T>;
begin
  if ALeft.HasValue and ARight.HasValue then
  begin
    Comparer := TEqualityComparer<T>.Default;
    Result := not Comparer.Equals(ALeft.Value, ARight.Value);
  end else
    Result := ALeft.HasValue <> ARight.HasValue;
end;

end.
Jim McKeeth
+1  A: 

Any solution for a record correctly lifetime-managing a string list object will involve an interface in one way or another. So why not return an interface from your function in the first place? Add properties to the interface, and for the consuming code it will look like record fields. It will allow you to easily add more "record fields" later on, and you can put arbitrarily complex code in the getters that return the values.

mghie
A: 

Another issue to be aware of, if you use sizeof to determine the memory footprint of the record, it will only include the size of a pointer for the TStringList. If you attempt to stream it out, the pointer which is stored will NOT be available to later instances, so you would have to ignore the pointer on the load and have another method to load the Tstringlist.

For example:

Procedure SaveRecToStream(Rec: TItemDetails ; Stream:tStream);
var
  i : integer;
begin
  Stream.Write(Rec,SizeOf(Rec)-SizeOf(tSTringList));
  Rec.List.saveToStream(Stream);
end;

Procedure LoadRecFromStream(Rec: TItemDetails ; Stream:tStream);
var
  i : integer;
begin
  FillMemory(@Rec,SizeOf(Rec),0);
  i := Stream.Read(rec,SizeOf(Rec)-SizeOf(tStringList));
  if i <> SizeOf(Rec)-SizeOf(tStringList) then
    Raise Exception.create('Unable to load record');
  Rec.List := tStringlist.create;
  Rec.List.LoadFromStream(Stream);
end;

This assumes that each stream contains exactly one record, and that the record variable passed to LoadRecFromStream does not contain a live tStringlist (if it was previously used it must be freed prior to the call or a leak occurs).

skamradt