08 March 2014

Been casting value types to stuff into TStrings.Objects[]?

On numerous occasions I've had the need to store simple, record and interface types in variable or fields of type TObject and had to use some horrible casting to do it. For example, you may resorted to doing stuff like this:

var
  SL: TStringList;
  Value: Integer;
begin
  SL := TStringList.Create;
  Value := 42;
  SL.AddObject('some text', TObject(5));
  ...
end;

Not nice!

Things get worse when the size of the data type is greater than SizeOf(TObject) and this kind of horror is needed:

var
  SL: TStringList;
  R1, R2: TRect;
  PR: PRect;
  Idx: Integer;
  MyRect: TRect;
begin
  ...
  // add some strings and objects
  SL := TStringList.Create;
  R1 := Rect(0, 0, 42, 56);
  GetMem(PR, SizeOf(TRect));
  PR^ := R1;
  SL.AddObject('some text', TObject(PR));
  R2 := Rect(1, 2, 3, 4);
  GetMem(PR, SizeOf(TRect));
  PR^ := R2;
  SL.AddObject('some more text', TObject(PR));
  ...
  // do something with SL e.g.
  MyRect := PRect(SL.Objects[0])^;
  ...
  // free the allocated memory
  for Idx := 0 to Pred(SL.Count) do
  begin
    PR := PRect(SL.Objects[Idx]);
    FreeMem(PR, SizeOf(TRect));
  end;
  SL.Free;
  ...
end;

And I'm not even going into the reference counting hell that is involved when doing all this with interfaces.

It's common to see this kind of stuff going on when someone wants to associate a value with an item in a list box or a list view for example. Now I know we shouldn't be storing data in UI controls, and I do try to avoid it (honest), but who hasn't done it?!

It many cases the problem can be solved by creating a little class that "wraps" the non-object type:

type
  TBox<T> = class(TObject)
  strict private
    var
      fValue: T;
  public
    constructor Create(Value: T);
    property Value: T read fValue;
  end;
 
...
 
constructor TBox<T>.Create(Value: T);
begin
  fValue := Value;
end;

Use TBox<> as follows: here we're wrapping an integer:

var
  IntObj: TBox<Integer>;
begin
  IntObj := TBox<Integer>.Create(42);
  try
    ...
    // use IntObj e.g.
    ShowMessageFmt('Value = %d', [IntObj.Value]);
    ...
  finally
    IntObj.Free;
  end;
end;

Now, the example above using rectangles changes to the somewhat less horrid:

var
  SL: TStringList;
  Idx: Integer;
  MyRect: TRect;
begin
  ...
  // add some strings and objects
  SL := TStringList.Create;
  SL.AddObject('some text', TBox<TRect>.Create(Rect(0, 0, 42, 56)));
  SL.AddObject('some more text', TBox<TRect>.Create(Rect(1, 2, 3, 4)));
  ...
  // do something with SL e.g.
  MyRect := (SL.Objects[0] as TBox<TRect>).Value;
  ...
  // free the allocated memory
  for Idx := 0 to Pred(SL.Count) do
    SL.Objects[Idx].Free;
  SL.Free;
  ...
end;

You still have to do a little casting to get the value and you still need to remember to free the objects, but it's a lot clearer what's happening, and you are actually storing data of the correct type in the TStrings.Objects[] property.

For convenience I've created a Gist containing the TBox<> definition.