Search code examples
delphiserializationdeserialization

Delphi how to Serialize and Deserialize TObject


I've the following code that deals with TStream writing and reading, but my problem (issue) is reading/writing TILObjects using the TListHelper class. Please read the comments inside these methods:

procedure TListHelper<T>.Serialize(const Buffer: IBuffer; const List: IList<T>);
function TListHelper<T>.Deserialize(const Buffer: IBuffer): IList<T>;
type
  IBuffer = interface
    procedure WriteInteger(Value: Integer);
    function ReadInteger:Integer
  end;

  TBuffer = class(TInterfacedObject, IBuffer)
  private
    FStream: TStream;
  public
    constructor Create(AStream: TStream);
    procedure WriteInteger(Value: Integer);
    function ReadInteger: Integer;
  end;
  
  type
  ILObject = interface
    ['{D4E3A925-58F2-4E8C-93E7-B49C1C3B453D}']
    procedure Serialize(Stream: IBuffer);
    function Deserialize(Stream: IBuffer): ILObject;
  end;


  TILObject = class(TInterfacedObject, ILObject)
  public
    procedure Serialize(Stream: IBuffer); virtual; abstract;
    function Deserialize(Stream: IBuffer): ILObject; virtual; abstract;
  end;

type
  IListHelper<T> = interface
    procedure Serialize(const Buffer: IBuffer; const List: IList<T>);
    function Deserialize(const Buffer: IBuffer): IList<T>;
  end;

  TListHelper<T> = class(TInterfacedObject, IListHelper<T>)
  public
    procedure Serialize(const Buffer: IBuffer; const List: IList<T>);
    function Deserialize(const Buffer: IBuffer): IList<T>;
  end;
// test classes
TChild = class(TILObject)
  private
  public
    constructor Create; overload; override;
    destructor Destroy; override;
    procedure Serialize(Stream: IBuffer); override;
    class function Deserialize(const Stream: IBuffer): TChild; reintroduce; overload;
  end;
  
  TFather = class(TILObject)
  private
    FChildList: IList<TChild>;
   procedure ReadChild(const Stream: IBuffer);
  public
    constructor Create; overload; override;
    destructor Destroy; override;
    procedure Serialize(Stream: IBuffer); override;
    class function Deserialize(const Stream: IBuffer): ILObject; reintroduce; overload;
    property ChildList: IList<TChild> read FChildList write FChildList;
  end;

// 
implementation

constructor TBuffer.Create(AStream: TStream);
begin
  inherited Create;
  FStream := AStream;
end;

procedure TBuffer.WriteInteger(Value: Integer);
begin
  FStream.WriteBuffer(Value, SizeOf(Value));
end;

function TBuffer.ReadInteger: Integer;
begin
  FStream.ReadBuffer(Result, SizeOf(Result));
end;

procedure TListHelper<T>.Serialize(const Buffer: IBuffer; const List: IList<T>);
var
  Count, i: Integer;
begin
  Count := List.Count;
  Buffer.WriteInteger(Count);

  for i := 0 to Count - 1 do
  begin
    if TypeInfo(T) = TypeInfo(Integer) then
      Buffer.WriteInteger(Integer(TValue.From<T>(List[I]).AsType<Integer>))
// here how deal with the TILObjects.Serialize
// I want to have TILObject.Serialize(Buffer)
  // else if TILObject
    else
      raise Exception.Create('Unsupported type for List serialization');
  end;
end;

function TListHelper<T>.Deserialize(const Buffer: IBuffer): IList<T>;
var
  Count, i: Integer;
  Item: TValue;
  LObject: TILObject;
begin
  Result := TList<T>.Create;
  Count := Buffer.ReadInt32;

  for i := 0 to Count - 1 do
  begin
    if TypeInfo(T) = TypeInfo(Integer) then
      Item := TValue.From<Integer>(Buffer.ReadInteger)
    // here how to deal with TILObject.Deserialize
   // else if  TILObject.Deserialize(Buffer)
    else
      raise Exception.Create('Unsupported type for List deserialization');

    Result.Add(Item.AsType<T>);
  end;
end;

// test classes
constructor TChild.Create;
begin
  inherited Create;
end;

destructor TChild.Destroy;
begin
  inherited Destroy;
end;

procedure TChild.Serialize(Stream: IBuffer);
begin
  inherited Serialize(Stream);
end;

constructor TFather.Create;
begin
  inherited Create;
  FChildList := TList<TChild>.Create;
end;

class function TChild.Deserialize(const Stream: IBuffer): ILObject;
begin
  Result := TChild.Create;
end;

How to use TIlObject.Serialize() inside TListHelper<T>.Serialize() and TILObject.Deserialize() inside TListHelper<T>.Deserialize()?


Solution

  • Testing the return value of TypeInfo(T) alone won't help you in this situation, as you would have to test for specific types. Instead, you can use GetTypeKind(T) (XE7+), checking for tkInteger, tkInterface, and tkClass types, and handle their data accordingly.

    For example:

    procedure TListHelper<T>.Serialize(const Buffer: IBuffer; const List: IList<T>);
    var
      Count, i: Integer;
      LIntf: ILObject;
      LObject: TObject;
    begin
      Count := List.Count;
      Buffer.WriteInteger(Count);
    
      for i := 0 to Count - 1 do
      begin
        case GetTypeKind(T) of 
          tkInteger: begin
            Buffer.WriteInteger(TValue.From<T>(List[I]).AsType<Integer>);
          end;
          tkInterface: begin
            if Supports(TValue.From<T>(List[I]).AsInterface, ILObject, LIntf) then
              LIntf.Serialize(Buffer)
            else
              raise Exception.Create('Unsupported interface type for List serialization');
          end;
          tkClass: begin
            LObject := TValue.From<T>(List[I]).AsObject;
            if LObject.InheritsFrom(TILObject) then
              TILObject(LObject).Serialize(Buffer)
            else
              raise Exception.Create('Unsupported object type for List serialization');
          end;
        else
          raise Exception.Create('Unsupported type for List serialization');
        end;
      end;
    end;
    
    function TListHelper<T>.Deserialize(const Buffer: IBuffer): IList<T>;
    var
      Count, i: Integer;
      Item: TValue;
      LIntf: ILObjct;
      LObject: TILObject;
    begin
      Result := TList<T>.Create;
      Count := Buffer.ReadInt32;
    
      for i := 0 to Count - 1 do
      begin
        case GetTypeKind(T) of
          tkInteger: begin
            Item := TValue.From<T>(Buffer.ReadInteger);
          end;
          tkInterface: begin
            LIntf := ???.Create as ILObject;
            LIntf.Deserialize(Buffer);
            Item := TValue.From<T>(LIntf);
          end;
          tkClass: begin
            LObject := ???.Create as TILObject;
            LObject.Deserialize(Buffer);
            Item := TValue.From<T>(LObject);
          end;
        else
          raise Exception.Create('Unsupported type for List deserialization');
        end;
    
        Result.Add(Item.AsType<T>);
      end;
    end;
    

    However, as you can see above, there is a problem with TListHelper.Deserialize(). You have to create an object instance first in order to call (T)ILObject.Deserialize() on it, so you need to know which class type to create. But you can't call T.Create since T doesn't have the class constraint. So you will have to write the class name into your Buffer, read it back, and use a separate factory to create and return a new (T)ILObject instance for the specified class name, eg:

    procedure TListHelper<T>.Serialize(const Buffer: IBuffer; const List: IList<T>);
    var
      Count, i: Integer;
      LIntf: ILObject;
      LObject: TObject;
    begin
      Count := List.Count;
      Buffer.WriteInteger(Count);
    
      for i := 0 to Count - 1 do
      begin
        case GetTypeKind(T) of 
          tkInteger: begin
            Buffer.WriteInteger(TValue.From<T>(List[I]).AsType<Integer>);
          end;
          tkInterface: begin
            if Supports(TValue.From<T>(List[I]).AsInterface, ILObject, LIntf) then
            begin
              Buffer.WriteString((LIntf as TObject).ClassName);
              LIntf.Serialize(Buffer);
            end else
              raise Exception.Create('Unsupported interface type for List serialization');
          end;
          tkClass: begin
            LObject := TValue.From<T>(List[I]).AsObject;
            if LObject.InheritsFrom(TILObject) then
            begin
              Buffer.WriteString(LObject.ClassName);
              TILObject(LObject).Serialize(Buffer)
            end else
              raise Exception.Create('Unsupported object type for List serialization');
          end;
        else
          raise Exception.Create('Unsupported type for List serialization');
        end;
      end;
    end;
    
    function TListHelper<T>.Deserialize(const Buffer: IBuffer): IList<T>;
    var
      Count, i: Integer;
      Item: TValue;
      LClassName: string;
      LIntf: ILObjct;
      LObject: TILObject;
    begin
      Result := TList<T>.Create;
      Count := Buffer.ReadInt32;
    
      for i := 0 to Count - 1 do
      begin
        case GetTypeKind(T) of
          tkInteger: begin
            Item := TValue.From<T>(Buffer.ReadInteger);
          end;
          tkInterface: begin
            LClassName := Buffer.ReadString;
            LIntf := CreateILObject(LClassName) as ILObject; // <--
            LIntf.Deserialize(Buffer);
            Item := TValue.From<T>(LIntf);
          end;
          tkClass: begin
            LClassName := Buffer.ReadString;
            LObject := CreateILObject(LClassName); // <--
            LObject.Deserialize(Buffer);
            Item := TValue.From<T>(LObject);
          end;
        else
          raise Exception.Create('Unsupported type for List deserialization');
        end;
    
        Result.Add(Item.AsType<T>);
      end;
    end;
    

    You will have to implement CreateILObject() yourself, eg:

    function CreateILObject(AClassName: string): TILObject;
    begin
      if AClassName = 'TChild' then
        Result := TChild.Create
      ...
      else
        Result := nil; // or raise
    end;
    

    There are ways to generalize such a factory, but that is outside the scope of this question. There are many other questions on StackOverflow (and other forums) related to (de)serializing objects.