Wednesday, November 09, 2011

TOmniValue handles arrays, hashes and records

Arrays

For quite some time, TOmniValue record has the ability to store arrays. You would use

TOmniValue.Create([1, 2, OTL, TButton.Create(nil)]) 
and some magic would store this array inside TOmniValue.
The problem, though, was that this magic was quite lame. The array was converted to a ‘variant array’ and you could only access its elements as Variant type. This, besides other things, meant that it was not simple to store pointers, interfaces and objects in TOmniValue array. Or, better said, it was simple to store them but not simple to retrieve them. You needed some ugly casting like

o := TButton(NativeUInt(ov[1]));
Since today, TOmniValue supports native arrays, that is, each item in the array is again of the TOmniValue type.

You use the same syntax to create an array but retrieving is much simpler – as the default array property returns TOmniValue values, you can use built-in TOmniValue properties to access specific types. For example, the second example above can now be written as

o := TButton(ov[1].AsObject);
You can also access the complete array instead of individual parameters by calling TOmniValue.AsArray. This can be helpful if you want to know how many elements are in the array (TOmniValue.AsArray.Count; first element has index 0).

Hashes

As the internal array is implemented using TOmniValueContainer, a class that can address individual elements either by the numeric index or by the string name, it was trivially for me to add support for associative arrays, or hashes as this type of data is called in Perl. In Delphi, the appropriate equivalent is TDictionary<string, TOmniValue>, that is, a dictionary addressed by strings that stores TOmniValue elements.
To create this kind of TOmniValue container, you must call the CreateNamed constructor, passing it an open array where every second parameter is a string.

TOmniValue.CreateNamed([
  'Person', 'Primoz',
  'Location', 'at home'])
To access an individual item in the array, use default array property with string as an index selector.

Format('%s is %s', [msgData['Person'].AsString, msgData['Location'].AsString])
This option is quite useful for sending structured data to the task (and back) without creating custom class.

Records

If you are using at least Delphi 2009 (actually, the code was only tested with XE but I have high hopes that it will work with 2009), you can store a record inside the TOmniValue. For example, if you have a record named TTestRecord, you can use TOmniValue.FromRecord to initialize TOmniValue instance.

ov := TOmniValue.FromRecord<TTestRecord>(rec);
To get record contents back from the TOmniValue, use

rec := msgData.AsRecord<TTestRecord>;
Internally, your record is wrapped into TOmniRecordWrapper<T> class (which is public so you can use it independently of TOmniValue).

TOmniRecordWrapper<T: record> = class
strict private
  FValue: T;
public
  constructor Create(const value: T);
  function  GetRecord: T;
  procedure SetRecord(const value: T);
  property Value: T read GetRecord write SetRecord;
end;
The instance of this class is in turn stored in the interface which destroys owned object automatically when its reference count falls to zero.

IOmniAutoDestroyObject = interface
  function GetValue: TObject;
  property Value: TObject read GetValue;
end; 
By using this trick, everything is correctly destroyed and cleaned up when TOmniValue variable is no longer used.

Demo

All that is demoed in new test application in folder 50_OmniValueArray.

2 comments:

  1. Hi Gabr,

    Is it possible to add IsRecord to TOmniValue?


    Kind Regards,

    Keith

    ReplyDelete
    Replies
    1. Sure. Implemented in revision 1189.

      Delete