Easier way to work with arrays - delphi

I have the following scenario:
procedure SomeMethod(SomeList: array of string);
I have to call this method with some IDs from a DataSet, I know I can do it this way:
var
MyArray: array of string;
I: Integer;
begin
SetLength(MyArray, MyDataSet.RecordCount);
I := 0;
MyDataSet.First;
while not MyDataSet.Eof do
begin
MyArray[I] := MyDataSetID.Value;
Inc(I);
MyDataSet.Next;
end;
SomeMethod(MyArray);
end;
I'm lazy as hell and this is too much work for my liking... I want an easier way for doing this, any ideas?

That's about the only way I can think of to do it. However, why can't you just take what you already have and turn it into a function? You'd just have to call it when you need it.
If you need to use it with different datasets, simply pass the dataset and the field to use as parameters:
procedure FieldToArray(const DS: TDataSet; const FieldName: string;
const Arr: TStringArray);
var
i: Integer;
begin
SetLength(Arr, DS.RecordCount);
i := 0;
DS.First;
while not DS.Eof do
begin
Arr[i] := DS.FieldByName(FieldName).AsString;
Inc(i);
DS.Next;
end;
end;
Somewhere visible (maybe in the interface section of the unit that declares this function), declare the TStringArray type:
type
TStringArray = array of string;
Now in your code, anywhere you need that list of field values:
var
MyArray: TStringArray;
FieldToArray(MyDataSet, 'ID', MyArray);
SomeProc(MyArray);
FieldToArray(AnotherDataSet, 'LastName', MyArray);
SomeOtherProc(MyArray);
MyArray := nil;

As far as I know there is no easier way built into Delphi, but there are two things which could make your life easier:
Write your own DataSetToArray function. (As proposed by the other answers)
If you are using Delphi 2007 or newer you can add a ToArray function to the dataset using a helper class:
TDataSetHelper = class helper for TDataSet
function ToArray: TStringArray;
end;
That way all your datasets would have this function and you could just use:
SomeMethod(MyDataset.ToArray);

In addition to the answer of Ken White. Repeatedly calling FieldByName can be a serious performance drain. So you can use a variable to store the field (which is updated with the dataset).
procedure FieldToArray(const DS: TDataSet; const FieldName: string;
const Arr: TStringArray);
var
i: Integer;
field : TField;
begin
SetLength(Arr, DS.RecordCount);
i := 0;
DS.First;
field := DS.FieldByName(FieldName);
while not DS.Eof do
begin
Arr[i] := field.AsString;
DS.Next;
Inc(i);
end;
end;

I would advise to never use TDataSet.RecordCount unless you know exactly how the data may change at any given time, and what data access you will use in the future. First getting the number of records and then loop through them has the following problems:
If this is not executed in one transaction it may be possible that the number of records changes while you are executing your loop. This can lead to bad crashes - the array may actually be too short.
If a SQL server is behind the TDataSet then you may end up fetching the result set twice, once for getting the number of records, then for fetching the data.
For that reason I don't see the benefit of using an array of strings over a TStrings object. And calling TStrings.Add() in a loop is about the shortest code you can have.

I am also a lazy programmer. I used to be employed as a VB6 programmer and I sort of liked all their CInt, CStr, CSomethingOrOther functions and I set up some similar ones in my own common unit. One of the first ones I did was the CArray function and I made several versions of it using the OVERRIDE declaration. I have one for use with StringLists, one for delimited strings, and also one for datasets. So I might do something like
while not DS.Eof do
begin
MyValue := CArray(DS)[4];
DS.Next;
end;
I cheated on this a bit because I used the index of the field. So my solution sort of sucks in that regard, but it's kind of nice being able to use CArray for a lot of different scenarios and it turns things like StringLists and datasets into things you can put a watch on and check for when you get to a specific value.

Does it absolutely have to be "an array of strings"? Just about anything you can do with a dynamic array, you can use a List for and do it more easily. Here's how I'd do it
procedure SomeMethod(SomeList: TStringList);
...
var
MyList: TStringList;
begin
MyList := TStringList.Create;
MyDataSet.First;
while not MyDataSet.Eof do
begin
MyList.add(MyDataSetID.Value);
MyDataSet.Next;
end;
SomeMethod(MyList);
end;
Make sure you free your TStringList when you're done with it, of course.

Related

Delphi load name/value pair from text file

Want to load name value pairs into a TStringList.
have a text file with data:
a=ant
a=animal
b=bob
d=darn
Function to load it:
LoadData(argPath: string): TStringList;
var
data: TStringList;
begin
data := TStringList.Create(true);
data.Delimiter := '=';
try
data.LoadFromFile(argPath);
except on E : Exception do
begin
Application.MessageBox(PWideChar(E.Message),
'Unable to Load Data', MB_OK or MB_ICONERROR);
Application.Terminate;
end;
end;
Result := data;
end;
Result is currently:
a=ant
a=animal
b=bob
d=darn
want the Result's strings to be:
ant
animal
bob
darn
and the Result's TObjects to be
a
a
b
d
After this I want to be able to display this in a TComboBox.
You'll have to use the built-in functions in the TStringList. I've broken this down into variables just for ease of reading / understanding, but could be compacted:
var
X: Integer;
Name, Value: String;
begin
for X:= 0 to MyList.Count - 1 do begin
Name:= MyList.Names[X];
Value:= MyList.ValueFromIndex[X];
MyCombo.Items.Add(Value);
end;
end;
With that, I'm sure you can figure out the rest (since I'm not sure exactly what you mean by result strings and objects).
It's preferable to pass the string list as a parameter, to ease lifetime management. It's also better to let the function raise exceptions and deal with them later. That allows more flexibility and the possibility of reuse. Like this:
procedure LoadValues(const FileName: string: Values: TStringList);
var
i: Integer;
begin
Values.LoadFromFile(FileName);
for i := 0 to Values.Count - 1 do
if Pos(Values.NameValueSeparator, Values[i]) <> 0 then
Values[i] := Values.ValueFromIndex[i];
end;
You cannot store strings directly in the Objects[] property of a TStrings because strings are managed types. You should probably store the names in a separate string list, or another more capable data structure. It's easy to extend the above code to populate two lists instead of one using the Names[] property.
It sounds like you are trying to stuff all of your data into a visual control. Don't do that. Treat visual controls as things to display data and not to hold and manage it.
Gave up on trying to load the combo box with LoadFromFile. Ended up creating two separate lists.
In FormCreate handler:
LoadData(...)
myComboBox.Items := displayList;
LoadData
procedure MyForm.LoadData(
const path: string; data: TStringList);
var index : Integer;
begin
data.LoadFromFile(path);
for index := 0 to data.Count - 1 do
begin
displayList.Add(data.ValueFromIndex[index]);
valueList.Add(data.Names[index]);
end;
end;
Whenever you need to get the selected value:
valueList[myComboBox.ItemIndex]

Is it possible to sort a TListBox using a custom sort comparator?

I need to sort my TListBox but I realized it is a lot of work to modify my code if I were to say make a TStringList, sort it and then copy those items into the Listbox. The main reason it's a lot of work is that I have many places in the code where the listbox contents are modified and I guess I would have to edit them all to force a sort at the time they are added, deleted or whatever.
I would much prefer something that let me just attach a method to a listbox somehow to sort it using my custom sort logic.
Is it somehow possible?
This is no Problem! Look at this Code:
function CompareDates(List: TStringList; Index1, Index2: Integer): Integer;
var
d1, d2: TDateTime;
begin
d1 := StrToDate(List[Index1]);
d2 := StrToDate(List[Index2]);
if d1 < d2 then
Result := -1
else if d1 > d2 then
Result := 1
else
Result := 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
sl: TStringList;
begin
sl := TStringList.Create;
try
sl.Assign(ListBox1.Items);
sl.CustomSort(CompareDates);
ListBox1.Items.Assign(sl);
finally
sl.Free
end;
end;
If you are using Delphi XE or later, I have a possibility for you.
Note that I say "possibility" and not "solution" as it is more of a hack than anything else and I wouldn't really approve this in production code unless it was a last resort.
From what I understand, what you are essentially trying to achieve is override the behavior of the Add function (which is virtual) to make it insert at the right position based on a custom order. (If you need to also override insert, this works too). If it was possible to override the TStrings descendant TListbox uses, that would be simple, but we are not that lucky.
Delphi XE introduced a new class called TVirtualMethodInterceptor (Rtti unit) that allows to intercept virtual method to do whatever we want to do with it. We can inspect and modify the parameters, call other functions, or litterally cancel the call and do nothing at all.
Here's how the proof of concept I made looked like:
//type
// TCompareFunc<T1> = reference to function (const Arg1, Arg2 : T1) : Integer;
procedure TForm4.FormCreate(Sender: TObject);
var vCompareFunc : TCompareFunc<string>;
RttiContext : TRttiContext;
vAddMethod : TRttiMethod;
vRttiType : TRttiType;
begin
RttiContext := TRttiContext.Create;
vRttiType := RttiContext.GetType(TStrings);
vAddMethod := vRttiType.GetMethod('Add');
vCompareFunc := MyCompareFunc;
Fvmi := TVirtualMethodInterceptor.Create(Listbox1.Items.ClassType);
Fvmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
var
idx : Integer;
begin
if Method = vAddMethod then
begin //if it's the Add method, map it to Insert at the right position
DoInvoke := False;
BinarySearch(TStrings(Instance), Args[0].AsString, vCompareFunc,idx);
TStrings(Instance).Insert(idx, Args[0].AsString);
end;
end;
Fvmi.Proxify(Listbox1.Items);
end;
This proof of concept intercept the call to TStrings.add and map it to binarysearch/Insert so that the items of the list are always in the right order. This does not override the Insert or Assign function, or any other function modifying the list. If you want to use this approach, you need to override all the "offending" functions.
Disclaimer : Since I have never really used this technique, don't consider this example as the golden rule for TVirtualMethodInterceptor's usage. It does work, but it might have performance implications or others that I'm unaware of.
One important point to mention (from Barry Kelly's blog, see below)
One thing the TVirtualMethodInterceptor class doesn't have, however,
is a way to unhook (unproxify) the object. If the object is never
unhooked, it's important that the object doesn't outlive the
interceptor, because the interceptor needs to allocate executable
memory in order to create the little stubs with which it redirects
method calls to the events.
If you want to dig deeper, here's a pretty good article on the subject:
http://blog.barrkel.com/2010/09/virtual-method-interception.html

Iterate over variables in Delphi

Given the declarations :
Unit MyUnit;
interface
type
TMyFileStream= class(TFileStream);
...
end;
var
a1,a2,a3,a4,a5: integer;
b1,b2,b3: boolean;
c1: char;
d1,d2,d3,d4: TDateTime;
f1,f2,f3,f4,f5,f6,f7,f8: TMyFileStream // LineX
...
procedure MyProc;
implementation
procedure MyProc
begin
// I wanna iterate over all integer (or any other type) variables here with a loop regardless of their count and identifier name
end;
Some specific type variables' count regularly changes in code - mostly increases as I add new functions. How can I reference them in a loop to take the same action on all of them ? I want to preserve the fact that when I add a new one, the code needs to be modified at only one place.
I've already thought of putting them in an (either static or dynamic) array, but this involves the modifocation of code at every location where they are referenced, which is much-much-much work that I wanna spare if it's possible by any means.
There's currently 38 variables I want to take an acton upon, the references' count is a multiple of it far above 100.
Hope I was clear enough.
Thanks for any idea.
Peter
Although the design smells, this is what pointers are made for:
type
PMyFileStream = ^TMyFileStream;
TMyFileStream= class(TFileStream)
end;
var
a1,a2,a3,a4,a5: integer;
b1,b2,b3: boolean;
c1: char;
d1,d2,d3,d4: TDateTime;
f1,f2,f3,f4,f5,f6,f7,f8: TMyFileStream; // LineX
function GetVarsInt: TArray<PInteger>;
begin
result := TArray<PInteger>.Create(#a1, #a2, #a3, #a4, #a5);
end;
function GetVarsBool: TArray<PBoolean>;
begin
result := TArray<PBoolean>.Create(#b1, #b2, #b3);
end;
function GetVarsChar: TArray<PChar>;
begin
result := TArray<PChar>.Create(#c1);
end;
function GetVarsDateTime: TArray<PDateTime>;
begin
result := TArray<PDateTime>.Create(#d1, #d2, #d3, #d4);
end;
function GetVarsMyFileStream: TArray<PMyFileStream>;
begin
result := TArray<PMyFileStream>.Create(#f1, #f2, #f3, #f4, #f5, #f6, #f7, #f8);
end;
procedure HandleInt(var Value: Integer);
begin
end;
procedure HandleBool(var Value: Boolean);
begin
end;
procedure HandleChar(var Value: Char);
begin
end;
procedure HandleDateTime(var Value: TDateTime);
begin
end;
procedure HandleMyFileStream(var Value: TMyFileStream);
begin
end;
procedure MyProc;
var
vInt: PInteger;
vBool: PBoolean;
vChar: PChar;
vDateTime: PDateTime;
vMyFileStream: PMyFileStream;
begin
for vInt in GetVarsInt do
HandleInt(vInt^);
for vBool in GetVarsBool do
HandleBool(vBool^);
for vChar in GetVarsChar do
HandleChar(vChar^);
for vDateTime in GetVarsDateTime do
HandleDateTime(vDateTime^);
for vMyFileStream in GetVarsMyFileStream do
HandleMyFileStream(vMyFileStream^);
end;
In case of the TMyFileStream variables, you might get away with no pointers when you only want to manipulate the existing object instances.
If you put these variables in a class you can use RTTI to loop over the properties of that class. There is no method that I know of to loop over variables that do not belong to a class.
I've already thought of putting them in an (either static or dynamic) array, but this involves the modifocation of code at every location where they are referenced, which is much-much-much work that I wanna spare if it's possible by any means.
So what! Do it!
The longer you put off fixing horrible code, the worse it will get. Also, it's not nearly as bad as you think.
E.g. Change the following:
var
a1,a2,a3,a4,a5: integer;
to:
var
A: array[1..5] of Integer;
Now everything that was referring to a? will fail to compile (unless you had scope conflicts, which would be a simmering pot of disaster in any case). These compilation errors can easily be fixed by changing a? to a[?].
If you simply go through a cycle of: compile --> fix --> compile --> fix --> ... You'll find you can clean up a lot faster than you think.

Implementing TObjectList's sort without copy/paste code

I have a procedure for sorting nodes in a node tree (VirtualTreeView)
All memory leaks, extracted from FMM4 report, are stored in objects of a class TMemoryLeakList(these are the list I want to sort), which are stored in a list of lists called TGroupedMemoryLeakList, and both TMLL and TGMLL extend TObjectList. If I want to keep the functionality of being able to chose between ascending and descending sort order and choosing between sorting by one of four different data types, I 'have to' implement EIGHT different comparison methods (4 sort types * 2 sort directions) which I pass on to the main sorting method, because my TMLL list extends TObjectList. The main sorting method look like this.
The values for the fields fSortType and fSortDirection are acquired from the GUI comboboxes.
One of those eight generic comparison functions looks like this.
The seven remaining are copy/pasted variations of this one.
Is there any rational way to refactor this huge amount of copy paste code and still keep the functionality of choosing a specific sort type and direction?
Nice question about refactoring, but I dislike the answer you presumably are looking for. There is nothing wrong with a few extra lines of code, or a few extra routines. Especially the latter in which case naming actively assist in more readability.
My advice would be: leave the design as you have, but shorten the code:
function CompareSizeAsc(Item1, Item2: Pointer): Integer;
begin
Result := TMemoryLeak(Item2).Size - TMemoryLeak(Item1).Size;
end;
function CompareSizeDesc(Item1, Item2: Pointer): Integer;
begin
Result := TMemoryLeak(Item1).Size - TMemoryLeak(Item2).Size;
end;
function CompareClassNameAsc(Item1, Item2: Pointer): Integer;
begin
Result := CompareStr(TMemoryLeak(Item1).ClassName,
TMemoryLeak(Item2).ClassName);
end;
procedure TMemoryLeakList.Sort;
begin
case FSortDirection of
sdAsc:
case FSortType of
stSize: inherited Sort(CompareSizeAsc);
stClassName: inherited Sort(CompareClassNameAsc);
stCallStackSize: inherited Sort(CompareCallStackSizeAsc);
stId: inherited Sort(CompareIdAsc);
end;
sdDesc:
case FSortType of
stSize: inherited Sort(CompareSizeDesc);
stClassName: inherited Sort(CompareClassNameDesc);
stCallStackSize: inherited Sort(CompareCallStackSizeDesc);
stId: inherited Sort(CompareIdDesc);
end;
end;
end;
You can't get it much smaller then this ánd preserve the same level of readability.
Of course, you could rewrite the Sort routine as suggested by Arioch 'The:
procedure TMemoryLeakList.Sort;
const
Compares: array[TSortDirection, TSortType] of TListSortCompare =
((CompareSizeAsc, CompareClassNameAsc, CompareCallStackSizeAsc,
CompareIdAsc), (CompareSizeDesc, CompareClassNameDesc,
CompareCallStackSizeDesc, CompareIdDesc));
begin
inherited Sort(Compares[FSortDirection, FSortType]);
end;
But then: why not rewrite the QuickSort routine to eliminate the need for separate compare routines?
Alternatively, you could add ownership to TMemoryLeak in which case you have a reference to the owning list and its sort direction and sort type, for use within óne single compare routine.
Use function pointers.
var comparator1, comparator2: function (Item1, Item2: Pointer): Integer;
function sortComplex (Item1, Item2: Pointer): Integer;
begin
Result := comparator1(Item1, Item2);
if 0 = Result then Result := comparator2(Item1, Item2);
end;
Then you GUI elements should behave like
case ListSortType.ItemIndex of
itemBySzie : comparator1 := sortBySizeProcAsc;
....
end;
DoNewSort;
PS: make sure that you correctly specify those pointers even before user 1st click any GUI element;
PPS: you can rearrange this even further like
type t_criteria = (bySize, byName,...);
t_comparators = array[t_criteria] of array [boolean {Descending?}]
of function (Item1, Item2: Pointer): Integer;
const comparator1table: t_comparators =
( {bySize} ( {false} sortBySizeProcAsc, {true} sortBySizeProcDesc),
{byName} ( {false} sortByNameProcAsc, ...
Then you would fill working pointers from that array constants
This is my solution. Apart from completely rewriting the two procedures I also added two 'static' variables to my TMemoryLeakList class, and removed the former instance variables of the same name. This way, they are globally accessible to the Sort function.
TMemoryLeakList=class(TObjectList)
class var fSortType :TMlSortType;
class var fSortDirection :TMLSortDirection;
...
end
procedure TMemoryLeakList.Sort;
begin
inherited sort(sortBySomethingSomething);
end;
function sortBySomethingSomething(Item1, Item2: Pointer): Integer;
var
a, b : string;
ret : Integer;
begin
ret := 1;
if(TMemoryLeakList.fSortDirection = sdAsc) then
ret := -1;
case TMemoryLeakList.fSortType of stSize:
begin
a := IntToStr(TMemoryLeak(Item1).Size);
b := IntToStr(TmemoryLeak(Item2).Size);
end;
end;
case TMemoryLeakList.fSortType of stClassName:
begin
a := TMemoryLeak(Item1).ClassName;
b := TMemoryLeak(Item2).ClassName;
end;
end;
case TMemoryLeakList.fSortType of stID:
begin
a := IntToStr(TMemoryLeak(Item1).ID);
b := IntToStr(TMemoryLeak(Item2).ID);
end;
end;
case TMemoryLeakList.fSortType of stCallStackSize:
begin
a := IntToStr(TMemoryLeak(Item1).CallStack.Count);
b := IntToStr(TMemoryLeak(Item2).CallStack.Count);
end;
end;
//...jos tu
if a=b then
Result:=0
else if a>b then
Result:=-1*ret
else if a<b then
Result:=1*ret;
end;
I would like to rewrite this solution so as to use instance bounded variables fSortType,fSortDirection in TMemoryLeakList, but it seems impossible to pass a member function to an inherited sort function (from TObjectList), or is it?

Delphi 2010: How to save a whole record to a file?

I have defined a record which has lots of fields with different types (integer, real , string, ... plus dynamic arrays in terms of "array of ...").
I want to save it as a whole to a file and then be able to load it back to my program. I don't want to go through saving each field's value individually.
The file type (binary or ascii or ...) is not important as long Delphi could read it back to a record.
Do you have any suggestions?
You can load and save the memory of a record directly to and from a stream, as long as you don't use dynamic arrays. So if you use strings, you need to make them fixed:
type TTestRecord = record
FMyString : string[20];
end;
var
rTestRecord: TTestRecord;
strm : TMemoryStream;
strm.Write(rTestRecord, Sizeof(TTestRecord) );
You can even load or save an array of record at once!
type TRecordArray = array of TTestRecord;
var ra : TRecordArray;
strm.Write(ra[0], SizeOf(TTestRecord) * Length(ra));
In case you want to write dynamic content:
iCount := Length(aArray);
strm.Write(iCount, Sizeof(iCount) ); //first write our length
strm.Write(aArray[0], SizeOf * iCount); //then write content
After that, you can read it back:
strm.Read(iCount, Sizeof(iCount) ); //first read the length
SetLength(aArray, iCount); //then alloc mem
strm.Read(aArray[0], SizeOf * iCount); //then read content
As promised here it is: https://github.com/KrystianBigaj/kblib
When you defined for example record as:
TTestRecord = record
I: Integer;
D: Double;
U: UnicodeString;
W: WideString;
A: AnsiString;
Options: TKBDynamicOptions;
IA: array[0..2] of Integer;
AI: TIntegerDynArray;
AD: TDoubleDynArray;
AU: array of UnicodeString;
AW: TWideStringDynArray;
AA: array of AnsiString;
R: array of TTestRecord; // record contain dynamic array of itself (D2009+)
end;
You can save whole dynamic record to stream (as binary data) by :
TKBDynamic.WriteTo(lStream, lTestRecord, TypeInfo(TTestRecord));
To load it back:
TKBDynamic.ReadFrom(lStream, lTestRecord, TypeInfo(TTestRecord));
It not need to be a record, you can do same for any dynamic type like:
TKBDynamic.WriteTo(lStream, lStr, TypeInfo(UnicodeString));
TKBDynamic.WriteTo(lStream, lInts, TypeInfo(TIntegerDynArray));
TKBDynamic.WriteTo(lStream, lArrayOfTestRecord, TypeInfo(TArrayOfTestRecord)); // TArrayOfTestRecord = array of TTestRecord;
Tested on Delphi 2006/2009/XE. License: MPL 1.1/GPL 2.0/LGPL 3.0
See readme for information.
Another option which works very well for records (Delphi 2010+) is to use the SuperObject library. For example:
type
TData = record
str: string;
int: Integer;
bool: Boolean;
flt: Double;
end;
var
ctx: TSuperRttiContext;
data: TData;
obj: ISuperObject;
sValue : string;
begin
ctx := TSuperRttiContext.Create;
try
sValue := '{str: "foo", int: 123, bool: true, flt: 1.23}';
data := ctx.AsType<TData>(SO(sValue));
obj := ctx.AsJson<TData>(data);
sValue := Obj.AsJson;
finally
ctx.Free;
end;
end;
I also tested this briefly with a simple TArray<Integer> dynamic array and it did not have a problem storing and loading the array elements.
In addition to the answers that indicate how you do this, please also be aware of these:
You must be aware that writing records out to a file will be Delphi version specific (usually: specific to a series of Delphi versions that share the same memory layout for the underlying data types).
You can only do that if your record does not contain fields of a managed type. Which means that fields cannot be of these managed types: strings, dynamic arrays, variants, and reference types (like pointers, procedural types, method references, interfaces or classes) and file types, or types that contain those manages types. Which basically limits to to these unmanaged types:
A: Simple types (including bytes, integers, floats, enumerations, chars and such)
B: Short strings
C: Sets
D: Static arrays of A, B, C, D and E
E: Records of A, B, C, D and E
In stead of writing out records to/from a file, it might be better to go with class instances and convert them to/from JSON, and them write the JSON string equivalent to a file and read it back in.
You can use this unit to do the JSON conversion for you (should work with Delphi 2010 and up; works for sure with Delphi XE and up) from this location this location.
unit BaseObject;
interface
uses DBXJSON, DBXJSONReflect;
type
TBaseObject = class
public
{ public declarations }
class function ObjectToJSON<T : class>(myObject: T): TJSONValue;
class function JSONToObject<T : class>(json: TJSONValue): T;
end;
implementation
{ TBaseObject }
class function TBaseObject.JSONToObject<T>(json: TJSONValue): T;
var
unm: TJSONUnMarshal;
begin
if json is TJSONNull then
exit(nil);
unm := TJSONUnMarshal.Create;
try
exit(T(unm.Unmarshal(json)))
finally
unm.Free;
end;
end;
class function TBaseObject.ObjectToJSON<T>(myObject: T): TJSONValue;
var
m: TJSONMarshal;
begin
if Assigned(myObject) then
begin
m := TJSONMarshal.Create(TJSONConverter.Create);
try
exit(m.Marshal(myObject));
finally
m.Free;
end;
end
else
exit(TJSONNull.Create);
end;
end.
I hope this helps you getting an overview of things.
--jeroen
You could also define an object instead of a record, so you can use RTTI to save your object to XML or whatever. If you have D2010 or XE, you can use DeHL to serialize it:
Delphi 2010 DeHL Serialization XML and custom attribute : how it work?
But if you "google" you can find other libs with RTTI and serialization (with D2007 etc)
Another solution, working from Delphi 5 up to XE, is available as an OpenSource unit.
In fact, it implements:
some low-level RTTI functions for handling record types: RecordEquals, RecordSave, RecordSaveLength, RecordLoad;
a dedicated TDynArray object, which is a wrapper around any dynamic array, able to expose TList-like methods around any dynamic array, even containing records, strings, or other dynamic arrays. It's able to serialize any dynamic array.
Serialization uses an optimized binary format, and is able to save and load any record or dynamic array as RawByteString.
We use this in our ORM, to store high-level types like dynamic array properties into a database back-end. First step to a DB-Sharding architecture.
If you have dynamic strings or array you can't write the record "as a whole". Instead of using old style-25 characters max strings, I would add methods to the record to be able to "stream" itself to a stream, or better using a TFiler descendant:
TMyRec = record
A: string;
B: Integer;
procedure Read(AReader: TReader);
procedure Writer(AWriter: TWriter);
end;
procedure TMyrec.Read(AReader: TReader);
begin
A := AReader.ReadString;
B := AReader.ReadInteger;
end;
Codes from delphibasics :
type
TCustomer = Record
name : string[20];
age : Integer;
male : Boolean;
end;
var
myFile : File of TCustomer; // A file of customer records
customer : TCustomer; // A customer record variable
begin
// Try to open the Test.cus binary file for writing to
AssignFile(myFile, 'Test.cus');
ReWrite(myFile);
// Write a couple of customer records to the file
customer.name := 'Fred Bloggs';
customer.age := 21;
customer.male := true;
Write(myFile, customer);
customer.name := 'Jane Turner';
customer.age := 45;
customer.male := false;
Write(myFile, customer);
// Close the file
CloseFile(myFile);
// Reopen the file in read only mode
FileMode := fmOpenRead;
Reset(myFile);
// Display the file contents
while not Eof(myFile) do
begin
Read(myFile, customer);
if customer.male
then ShowMessage('Man with name '+customer.name+
' is '+IntToStr(customer.age))
else ShowMessage('Lady with name '+customer.name+
' is '+IntToStr(customer.age));
end;
// Close the file for the last time
CloseFile(myFile);
end;
The problem with saving a record containing dynamic array or real strings (or other "managed" types for that matter) is, it's not an big blob of memory containing everything, it's more like a tree. Someone or something needs to go over everything and save it to storage, somehow. Other languages (Python for example) include all sorts of facilities to transform most objects to text (serialize it), save it to disk and reload it (deserialize it).
Even though a Embarcadero-provided solution doesn't exist for Delphi, one can be implemented using the extended RTTI available in Delphi 2010. A ready-made implementation is available in the DeHL library (here's a blog post about it) - but I can't say much about the implementation, I never used DeHL.
An other option is the one you want to avoid: manually serialize the record to an TStream; It's actually not half difficult. Here's the kind of code I usually use to read/write objects to a file stream:
procedure SaveToFile(FileName:string);
var F:TFileStream;
W:TWriter;
i:Integer;
begin
F := TFileStream.Create(FileName, fmCreate);
try
W := TWriter.Create(F, 128);
try
// For every field that needs saving:
W.WriteString(SomeStr);
W.WriteInteger(TheNumber);
// Dynamic arrays? Save the length first, then save
// every item. The length is needed when reading.
W.WriteInteger(Length(DArray));
for i:=0 to High(DArray) do
W.WriteString(DArray[i]);
finally W.Free;
end;
finally F.Free;
end;
end;
procedure ReadFromFile(FileName:string);
var F:TFileStream;
R:TReader;
i,n:Integer;
begin
F := TFileStream.Create(FileName, fmOpenRead);
try
R := TReader.Create(F, 128);
try
SomeStr := R.ReadString;
TheNumber := R.ReadInteger;
// Reading the dynamic-array. We first get the length:
n := R.ReadInteger;
SetLength(DArray, n);
// And item-by-item
for i:=0 to n-1 do
DArray[i] := R.ReadString;
finally R.Free;
end;
finally F.Free;
end;
end;

Resources