0

I'm wondering how I can perform serialization of a generic TObjectList<T> container. Basically, I want to store different objects in that list, but all objects will descend from TSerializable, which is defined as follows:

  TSerializable = class abstract(TObject)
  public
    { Public declarations }
    procedure LoadFromStream(const S: TStream); virtual; abstract;
    procedure SaveToStream(const S: TStream); virtual; abstract;
  end;

Now, let's say I have these classes defined somewhere in my app:

type
    TExampleClass = class(TSerializable)
    private
        { Private declarations }
        FIntProp: Integer;
    public
        { Public declarations }
        constructor Create();

        procedure LoadFromStream(const S: TStream); override;
        procedure SaveToStream(const S: TStream); override;

        property IntProp: Integer read FIntProp write FIntProp;
    end;

    TAnotherExample = class(TSerializable)
    private
        { Private declarations }
        FStringProp: String;
    public
        { Public declarations }
        constructor Create();

        procedure LoadFromStream(const S: TStream); override;
        procedure SaveToStream(const S: TStream); override;

        procedure ReverseStringProp();

        property StringProp: String read FStringProp write FStringProp;
    end;

I'm planning to store such objects in a list:

var
    MS: TMemoryStream;
    SomeList: TObjectList<TSerializable>;
begin
    MS := TMemoryStream.Create();
    SomeList := TObjectList<TSerializable>.Create(True);
    try
        SomeList.Add(TExampleClass.Create());
        SomeList.Add(TAnotherClass.Create());

        TExampleClass(SomeList[0]).IntProp := 1992;
        TAnotherClass(SomeList[1]).StringProp := 'Some value';

        //  Here, a method to serialize the list...
        SerializeList(SomeList, MS);

        //  Clear the list and reset position in the stream.
        SomeList.Clear();
        MS.Seek(0, soFromBeginning);

        //  Unserialize the list.
        UnserializeList(SomeList, MS);

        //  Should display "Some value".
        Writeln(TAnotherClass(SomeList[1]).StringProp);
    finally
        SomeList.Free();
        MS.Free();
    end;
end;

Now, how could I possibly serialize the whole list to stream and then re-create the list from that stream?

What I was thinking about was:

  1. Iterate through the list.
  2. Write each object's class name to the stream first.
  3. Call SaveToStream() on that object.

But for that approach to work, I would need to create some kind of a class register, which would be some kind of a dictionary to store known classes. It sounds like a good idea, but then I would need to call some RegisterClass() method to add every new class to the dictionary, and I don't like that way too much.

Is there any other way, or should I just do it the way I proposed?

Thanks a bunch.

Pateman
  • 2,516
  • 3
  • 25
  • 35
  • What format are you using for your serialized objects? JSON? XML? Or something home brewed? I hope for your sake that it's not the latter. – David Heffernan Dec 19 '12 at 17:03
  • Actually it would be something home brewed. Does that complicate things? – Pateman Dec 19 '12 at 17:05
  • 5
    Of course it does. You have to solve (again) all the problems that have been solved so many times before. – David Heffernan Dec 19 '12 at 17:08
  • What else would you suggest then, David? – Pateman Dec 19 '12 at 17:09
  • 6
    I've got nothing more to suggest. Use either JSON or XML. And use one of the many serialization libraries. Don't re-invent the wheel. Usually what happens when you do that is that your wheel comes out with corners. – David Heffernan Dec 19 '12 at 17:13
  • 2
    I'm on the same page with @DavidHeffernan, if you want something already made, omnixml is a very good option, using it for years now http://code.google.com/p/omnixml/ –  Dec 19 '12 at 17:25
  • 1
    You can serialize any object using extended RTTI. No need to descend from a TSerializable class.. – whosrdaddy Dec 19 '12 at 17:27
  • Have a look to the **JCL library** and `TJvAppXMLFileStorage` class: http://stackoverflow.com/a/12526037/148690 – TridenT Dec 19 '12 at 19:03
  • or look at mORMot library and their blog like http://blog.synopse.info/post/2011/03/12/TDynArray-and-Record-compare/load/save-using-fast-RTTI – Arioch 'The Dec 20 '12 at 07:55
  • 1
    @whosrdaddy that may result in faster load/save or ordering properties in proper sequence. There are people who think that RTTI was re-inventing the wheel. – Arioch 'The Dec 20 '12 at 07:57

1 Answers1

1

Thank you guys for tips. I have decided to use my own approach, which is probably not the best one, but suits the needs of my small project.

I thought that someone might be interested in such approach, so I posted it here.

Basically, what I decided on is to have a base class TSerializable:

type
  TSerializable = class abstract(TObject)
  public
    { Public declarations }
    procedure LoadFromStream(const S: TStream); virtual; abstract;
    procedure SaveToStream(const S: TStream); virtual; abstract;
  end;

Every descendant class needs to implement LoadFromStream() and SaveToStream() and handle saving to stream separately. It would be probably good to write some generic methods, which would load/save all class properties automatically.

Then, I have this small class:

type
  TSerializableList = class(TObjectList<TSerializable>)
  public
    procedure Serialize(const S: TStream);
    procedure UnSerialize(const S: TStream);
  end;

The code is:

{ TSerializableList }

procedure TSerializableList.Serialize(const S: TStream);
var
  CurrentObj: TSerializable;
  StrLen, StrSize: Integer;
  ClsName: String;
begin
  S.Write(Self.Count, SizeOf(Integer));
  for CurrentObj in Self do
  begin
    ClsName := CurrentObj.QualifiedClassName();
    StrLen := Length(ClsName);
    StrSize := SizeOf(Char) * StrLen;

    S.Write(StrLen, SizeOf(Integer));
    S.Write(StrSize, SizeOf(Integer));
    S.Write(ClsName[1], StrSize);

    CurrentObj.SaveToStream(S);
  end;
end;

procedure TSerializableList.UnSerialize(const S: TStream);
var
  I, NewIdx, TotalCount, Tmp, Tmp2: Integer;
  ClsName: String;
  Context: TRttiContext;
  RttiType: TRttiInstanceType;
begin
  Context := TRttiContext.Create();
  try
    S.Read(TotalCount, SizeOf(Integer));
    for I := 0 to TotalCount -1 do
    begin
      S.Read(Tmp, SizeOf(Integer));
      S.Read(Tmp2, SizeOf(Integer));

      SetLength(ClsName, Tmp);
      S.Read(ClsName[1], Tmp2);

      RttiType := (Context.FindType(ClsName) as TRttiInstanceType);
      if (RttiType <> nil) then
      begin
        NewIdx := Self.Add(TSerializable(RttiType.MetaclassType.Create()));
        Self[NewIdx].LoadFromStream(S);
      end;
    end;
  finally
    Context.Free();
  end;
end;

Quick and dirty, but works for what I need.

NOTE Since the code uses extended RTTI, it won't compile in older Delphi versions. Also, you might need to add {$STRONGLINKTYPES ON} in your DPR file or invent some other mechanism, so that linker doesn't skip your classes (David Heffernan suggest one way here)

Community
  • 1
  • 1
Pateman
  • 2,516
  • 3
  • 25
  • 35
  • I'd highly recommend replacing SizeOf(Integer|Char) with constants and also add a warning or comment somewhere that "Integer" is 32bit, if you compile this in 64bit, you'll have surprises. –  Dec 20 '12 at 06:03
  • @Computer in Delphi 64 bits, the Integer type is still 32 bits, so I think there's no surprise. – jachguate Dec 20 '12 at 07:13
  • @jachguate I don't have delphi that can target 64bit, so, then NativeInt is the 64bit? –  Dec 20 '12 at 09:03
  • 1
    According to the official documentation, `Integer` is always 32-bit on both x86 and x64, while `NativeInt` is 32-bit on x86, and 64-bit on x64. :) – Pateman Dec 20 '12 at 09:28