Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"descending" records in delphi?

Tags:

delphi

records

I know you can't actually descend anything from a record, but I'm not sure how to summarize my problem in one sentence. Edit the title if you do.

What I want to do here is make an array of some generic type, which can be one of X number of types, the array would be filled with those custom types (they have different fields and that's what is important). The easy way is to make just an array of variant records, each variant has it's own type, but obviously can't redeclare identifiers like so:

GenericRec = Record
  case SubTypeName: TSubTypeName of
    type1name: (SubRec: Type1);
    type2name: (SubRec: Type2);
    ...
    typeNname: (SubRec: TypeN);
  end;

Changing SubRec to SubRec1, SubRec2... SubRecN makes referencing painful, but not impossible. And since I started looking for alternative solutions to the above problem, classes came to mind.

The obvious example to demonstrate what I am trying to achieve is TObject, an array of those can be assigned to many different things. That's what I want, but with records (and that's impossible to do), because I want to be able to save the records to file as well as read them back (also because it's something I'm already familiar with). Making my own simple class is not a problem, making a descendant class from that to represent my subtype - I can do that. But what about writing that to file and reading it back? This boils down to serialization, which I have no idea how to do. From what I gather it's not as easy and the class must be descended from TComponent.

TMyClass = Class

Does it make any difference if I make the class like above? It's nothing fancy and has at most 10 fields, including a few custom types.

Setting serialization aside (just because I have a lot of reading to do on that topic), use of classes here also might be out of the question.

At this point, what are my options? Should I abandon records and try this with classes? Or would it be a lot less complicated just to stick to records and deal with the variant "limitation"? I'm all about learning and if exploding the class approach might make me smarter, I'll do it. I've also just looked into TList too (never used it), but it seems that it doesn't mix too well with records, well maybe it can be done, but that might be out of my league at the moment. I'm open to any kind of suggestions. What do i do?

like image 441
Raith Avatar asked Jun 28 '12 21:06

Raith


2 Answers

You're conflating serialization with "writing everything to disk with a single BlockWrite call." You can serialize anything you want, regardless of whether it descends from TComponent or TPersistent.

Although writing everything with a single BlockWrite call looks convenient at first, you'll quickly find it's not really what you want if your desired record types are going to store anything particularly interesting (like strings, dynamic arrays, interfaces, objects, or other reference- or pointer-based types).

You'll probably also find variant records unsatisfying since you'll be coding to the lowest common denominator. You won't be able to access anything in the record without checking the actual contained type, and the size of even the smallest amount of data will occupy the same amount of space as the largest data type.

The question seems to describe polymorphism, so you may as well embrace what the language already provides for that. Use an array (or list, or any other container) of objects. Then you can use virtual methods to treat them all uniformly. You can implement dynamic dispatch for records if you want (e.g., give each record a function pointer that refers to a function that knows how to deal with that record's contained data type), but in the end you'll probably just find yourself reinventing classes.

like image 152
Rob Kennedy Avatar answered Sep 24 '22 02:09

Rob Kennedy


The "natural" way of handling such data is to use a class, and not a record. It will be much easier to work with, both at definition time and when dealing with implementation: in particular, virtual methods are very powerful to customize a process for a particular kind of class. Then use a TList/TObjectList or a TCollection, or a generic-based array in newer versions of Delphi to store the list.

About serialization, there are several ways to do it. See Delphi: Store data in somekind of structure

In your particular case, the difficulty comes from the "variant" kind of record you are using. IMHO the main drawback is that the compiler will refuse to set any reference-counted kind of variable (e.g. a string) within the "variant" part. So you'll be able to write only "plain" variables (like integer) within this "variant" part. A big limitation IMHO, which reduces the interest of this solution.

Another possibility could be to store the kind of record at the beginning of its definition, e.g. with a RecType: integer or even better with a RecType: TEnumerationType which will be more explicit than a number. But you'll have to write a lot of code by hand, and works with pointers, which is a bit error-prone if you are not very fluent with pointer coding.

So you can also store the type information of the record, accessible via TypeInfo(aRecordVariable). Then you can use FillChar to initialize the record content to zero, just after allocation, then use the following function to finalize the record content, just after disallocation (this is what Dispose() does internally, and you shall call it, otherwise you'll leak memory):

procedure RecordClear(var Dest; TypeInfo: pointer);
asm
  jmp System.@FinalizeRecord
end;

But such an implementation pattern will just reinvent the wheel! It is in fact how class is implemented: the first element of any TObject instance is a pointer to its ClassType:

function TObject.ClassType: TClass;
begin
  Pointer(Result) := PPointer(Self)^;
end;

There is also another structure in Delphi, which is called object. It is some kind of record, but it supports inheritance - see this article. It is the old style of OOP programming in Turbo Pascal 5.5 days, deprecated, but still available. Note that I discovered a weird compilation issue on newer versions of Delphi: sometimes, an object allocated on the stack is not always initialized.

Take a look at our TDynArray wrapper and its associated functions, who is able to serialize any record content, into binary or JSON. See Delphi (win32) serialization libraries question. It will work with variant records, even if they include a string in their unvariant part, whereas a plain "Write/BlockWrite" won't work with reference counted fields.

like image 20
Arnaud Bouchez Avatar answered Sep 24 '22 02:09

Arnaud Bouchez