I'm trying to add a TDateTime value into a TStringList object using Delphi 10.4 Sydney.
I managed to do it like this:
TDateTimeObj = class(TObject)
strict private
DT: TDateTime;
protected
public
constructor Create(FDateTime: TDateTime);
property DateTime: TDateTime read DT write DT;
end;
constructor TDateTimeObj.Create(FDateTime: TDateTime);
begin
Self.DT := FDateTime;
end;
Then I add it to the TStringList like this:
procedure TForm1.Button1Click(Sender: TObject);
var
b: TStringList;
begin
b := TStringList.Create;
b.AddObject('a', TDateTimeObj.Create(now));
b.AddObject('b', TDateTimeObj.Create(now));
FreeAndNil(b);
end;
It works, but when I close the program I have a memory leak as I did not free the TDateTimeObj objects.
Is there a way to free the objects automatically, or a better way to achieve the same result?
You have to make the string list own the added objects. Owned objects are destroyed when the string list is destroyed.
procedure TForm1.Button1Click(Sender: TObject);
var b: TStringList;
begin
b := TStringList.Create(TRUE); // TRUE means OwnObjects
try
b.AddObject('a', TDateTimeObj.Create(now));
b.AddObject('b', TDateTimeObj.Create(now));
finally
FreeAndNil(b); // Owned objects will be destroyed as well
end;
end;
Related
I would like to use an enumerator to populate a Combobox with Key/Value pairs. Its important that I hide the key from the user and display the value only. On selecting I would like to capture the key associated with the selected value.
The code looks something similar to this.
var
currentObj: ISuperObject;
enum: TSuperEnumerator<IJSONAncestor>;
while enum.MoveNext do
begin
currentObj := enum.Current.AsObject;
cboUserList.Items.Add(currentObj.S['key'],currentObj.S['value']);
end;
The key currentObj.S['key'] should be capture on user select of the value
currentObj.S['value'] which is visible to the user on the cboUserList dropdownlist.
Any ideas?
A simple cross-platform solution would be to use a separate TStringList to hold the keys, then display the values in the ComboBox and use its item indices to access the TStringList items.
var
currentObj: ISuperObject;
enum: TSuperEnumerator<IJSONAncestor>;
while enum.MoveNext do
begin
currentObj := enum.Current.AsObject;
userSL.Add(currentObj.S['key']);
cboUserList.Items.Add(currentObj.S['value']);
end;
var
index: Integer;
key: string;
begin
index := cboUserList.ItemIndex;
key := userSL[index];
...
end;
You can wrap your key in class, e.g.
type
TKey = class
S: string;
constructor Create(const AStr: string);
end;
constructor TKey.Create(const AStr: string);
begin
S := AStr;
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
ComboBox1.Items.AddObject('value', TKey.Create('key'));
end;
And then access it as
procedure TForm2.ComboBox1Change(Sender: TObject);
begin
Caption := (ComboBox1.Items.Objects[ComboBox1.ItemIndex] as TKey).S;
end;
just make sure to destroy these objects later
I have some code which i did not write, but there is a memory leak. The real strangeness is the memory only leaks if i zero a structure before returning it.
Reproducible minimum code
The leak is reproducible in Delphi 5 and Delphi 7.
First we have a structure:
type
TLocalFile = packed record
FileName: AnsiString;
end;
This structure is the private member of a CollectionItem object:
TEntry = class(TCollectionItem)
private
FLocalFile: TLocalFile;
end;
Then we have the owning collection, which has a function that can return a populated structure:
TEntries = class(TCollection)
protected
function GetLocalFile: TLocalFile;
public
procedure DoStuff;
end;
With the weirdness located in the GetLocalFile function:
function TEntries.GetLocalFile: TLocalFile;
var
s: AnsiString;
begin
//Only leaks if i initialize the returned structure
// FillChar(Result, SizeOf(Result), 0);
ZeroMemory(#Result, SizeOf(Result));
s := 'Testing Leak';
Result.Filename := s; //'Testing leak'; only leaks if i set the string through a variable
end;
In reality this function is passed a stream, and returns a populated structure, but that's not important now.
Next we have a method of the collection that will populate all it's entries's LocalFile structures:
procedure TEntries.DoStuff;
var
x: Integer;
begin
for X := 0 to Count-1 do
begin
(Items[X] as TEntry).FLocalFile := GetLocalFile;
end;
end;
Finally, we construction a collection, add 10 items to it, have them DoStuff, then free the list:
procedure TForm1.Button1Click(Sender: TObject);
var
list: TEntries;
i: Integer;
entry: TCollectionItem;
begin
list := TEntries.Create(TEntry);
try
for i := 1 to 10 do
entry := list.Add;
list.DoStuff;
finally
list.Free;
end;
end;
We created 10 items, we leak 9 AnsiStrings.
The horrifying confusing things
There are ways in which this code doesn't leak. It only leaks when using an intermediate string stack variable
Change:
function TEntries.GetLocalFile: TLocalFile;
var
s: AnsiString;
begin
s := 'Testing Leak';
Result.Filename := s; //'Testing leak'; only leaks if i set the string through a variable
end;
to
function TEntries.GetLocalFile: TLocalFile;
begin
Result.Filename := 'Testing leak'; //doesn't leak
end;
and it doesn't leak.
The other method is not to initialize the structure before returning it:
Remove the call to FillChar or ZeroMemory, and it doesn't leak:
function TEntries.GetLocalFile: TLocalFile;
var
s: AnsiString;
begin
//Only leaks if i initialize the returned structure
// FillChar(Result, SizeOf(Result), 0);
// ZeroMemory(#Result, SizeOf(Result));
s := 'Testing Leak';
Result.Filename := s; //'Testing leak'; only leaks if i set the string through a variable
end;
These are strange resolutions. Whether i use an intermediate stack variable, or not, whether i Zero the structure or not, should not have any effect on memory cleanup.
I doubt this is a bug in the compiler. Which mean that i (meaning the person who wrote this) is doing something fundamentally wrong. i assume it has something to do with TCollectionItemClass. But i cannot for the life of me figure out what.
Full code
unit FMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TLocalFile = packed record
FileName: AnsiString;
end;
TEntry = class(TCollectionItem)
private
FLocalFile: TLocalFile;
end;
TEntries = class(TCollection)
protected
function GetLocalFile: TLocalFile;
public
procedure DoStuff;
end;
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
contnrs;
procedure TForm1.Button1Click(Sender: TObject);
var
list: TEntries;
i: Integer;
entry: TCollectionItem;
begin
list := TEntries.Create(TEntry);
try
for i := 1 to 10 do
begin
entry := list.Add;
end;
list.DoStuff;
finally
list.Free;
end;
end;
{ TEntries }
procedure TEntries.DoStuff;
var
x: Integer;
entry: TEntry;
begin
for X := 0 to Count-1 do
begin
entry := Items[X] as TEntry;
entry.FLocalFile := GetLocalFile;
end;
end;
function TEntries.GetLocalFile: TLocalFile;
var
s: AnsiString;
begin
//Only leaks if i initialize the returned structure
// FillChar(Result, SizeOf(Result), 0);
ZeroMemory(#Result, SizeOf(Result));
s := 'Testing Leak';
Result.Filename := s; //'Testing leak'; only leaks if i set the string through a variable
end;
end.
Oh, and don't forget to add FastMM4 to your project (if you don't already have it built-in), so you can detect the leaks.
AnsiString (and its Unicode counterpart) is reference-counted by the compiler. You can't simply zero memory containing a reference to it; you need to assign '' to it so that the compiler will generate code to decrement the refcount and release the memory correctly.
You'll have similar problems trying to block-clear data structures containing references to dynamic arrays, Interfaces, or (some) variants.
If you're not using a Delphi version recent enough to support the Default compiler magic expression, (I believe it was introduced in D2009,) the best way to clear out a record safely would be to call Finalize first, and then zero the memory as you're doing.
I do not understand where are the objects below and how to clear them?
for example:
public
Alist: TStringlist;
..
procedure TForm1.FormCreate(Sender: TObject);
begin
Alist:=Tstringlist.Create;
end;
procedure TForm1. addinstringlist;
var
i: integer;
begin
for i:=0 to 100000 do
begin
Alist.add(inttostr(i), pointer(i));
end;
end;
procedure TForm1.clearlist;
begin
Alist.clear;
// inttostr(i) are cleared, right?
// Where are pointer(i)? Are they also cleared ?
// if they are not cleared, how to clear ?
end;
procedure TForm1. repeat; //newly added
var
i: integer;
begin
For i:=0 to 10000 do
begin
addinstringlist;
clearlist;
end;
end; // No problem?
I use Delphi 7. In delphi 7.0 help file, it says:
AddObject method (TStringList)
Description
Call AddObject to add a string and its associated object to the list.
AddObject returns the index of the new string and object.
Note:
The TStringList object does not own the objects you add this way.
Objects added to the TStringList object still exist
even if the TStringList instance is destroyed.
They must be explicitly destroyed by the application.
In my procedure Alist.add(inttostr(i), pointer(i)), I did not CREATE any object. Were there objects or not ?
how can I clear both inttostr(i) and pointer(i).
Thank you in advance
There is no need to clear Pointer(I) because the pointer does not reference any object. It is an Integer stored as Pointer.
Advice: if you are not sure does your code leak or not write a simple test and use
ReportMemoryLeaksOnShutDown:= True;
If your code leaks you will get a report on closing the test application.
No the code you added does not leak. If your want to check it write a test like this:
program Project2;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes;
var
List: TStringlist;
procedure addinstringlist;
var
i: integer;
begin
for i:=0 to 100 do
begin
List.addObject(inttostr(i), pointer(i));
end;
end;
procedure clearlist;
begin
List.clear;
end;
procedure repeatlist;
var
i: integer;
begin
For i:=0 to 100 do
begin
addinstringlist;
clearlist;
end;
end;
begin
ReportMemoryLeaksOnShutDown:= True;
try
List:=TStringList.Create;
repeatlist;
List.Free;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Try to comment List.Free line to create a memory leak and see what happens.
Currently I am adding object by creating it like:
type
TRecord = class
private
str: string;
num: Integer;
public
constructor Create;
end;
...
procedure TForm1.Button2Click(Sender: TObject);
var
i: Integer;
rec: TRecord;
Alist: TStringList;
begin
Alist := TStringList.create;
Alist.Clear;
for i := 0 to 9 do
begin
rec := Trecord.Create; //create instance of class
rec.str := 'rec' + IntToStr(i);
rec.num := i * 2;
Alist.AddObject(IntToStr(i), rec);
end;
end;
Is this method correct or inefficient ?
Or Can I directly add object not by creating it like using record?
type
PRec = ^TRec;
TRec = record
str: string;
num: Integer;
end;
...
var
rec: TRec;
...
for i := 0 to 9 do
begin
//how to write here to have a new record,
//can i directly Create record in delphi 7 ?
rec.str := 'rec' + IntToStr(i);
rec.num := i*2;
Alist.AddObject(IntToStr(i), ???); // how to write here?
end;
Or other fast and simple way?
I am using Delphi 7.
Thanks in advance.
The way you're doing it now is fine.
You can't do it with a record without allocating memory when you add a new record to the TStringList.Objects, and you'd have to free it afterwards. You're just as well off using a class as you are now; you have to free the objects before freeing the stringlist. (In more recent versions of Delphi, TStringList has an OwnsObjects property that will auto-free them for you when the stringlist is free'd, but it's not in Delphi 7.)
If you really want to do this with a record, you can:
type
PRec = ^TRec;
TRec = record
str: string;
num: Integer;
end;
var
rec: PRec;
begin
for i := 0 to 9 do
begin
System.New(Rec);
rec.str := 'rec' + IntToStr(i);
rec.num := i*2;
Alist.AddObject(IntToStr(i), TObject(Rec)); // how to write here?
end;
end;
You'll need to use System.Dispose(PRec(AList.Objects[i])) to release the memory before freeing the stringlist. As I said, the way you're doing it now is actually much easier; you don't have to do the typecast when adding to and deleting from the stringlist.
You don't need the AList.Clear, by the way. Since you're creating the stringlist, there can't be anything in it to remove.
I'm curious to know why Delphi treats record type properties as read only:
TRec = record
A : integer;
B : string;
end;
TForm1 = class(TForm)
private
FRec : TRec;
public
procedure DoSomething(ARec: TRec);
property Rec : TRec read FRec write FRec;
end;
If I try to assign a value to any of the members of Rec property, I'll get "Left side cannot be assigned to" error:
procedure TForm1.DoSomething(ARec: TRec);
begin
Rec.A := ARec.A;
end;
while doing the same with the underlying field is allowed:
procedure TForm1.DoSomething(ARec: TRec);
begin
FRec.A := ARec.A;
end;
Is there any explanation for that behavior?
Since "Rec" is a property, the compiler treats it a little differently because it has to first evaluate the "read" of the property decl. Consider this, which is semantically equivalent to your example:
...
property Rec: TRec read GetRec write FRec;
...
If you look at it like this, you can see that the first reference to "Rec" (before the dot '.'), has to call GetRec, which will create a temporary local copy of Rec. These temporaries are by design "read-only." This is what you're running into.
Another thing you can do here is to break out the individual fields of the record as properties on the containing class:
...
property RecField: Integer read FRec.A write FRec.A;
...
This will allow you to directly assign through the property to the field of that embedded record in the class instance.
Yes this is a problem. But the problem can be solved using record properties:
type
TRec = record
private
FA : integer;
FB : string;
procedure SetA(const Value: Integer);
procedure SetB(const Value: string);
public
property A: Integer read FA write SetA;
property B: string read FB write SetB;
end;
procedure TRec.SetA(const Value: Integer);
begin
FA := Value;
end;
procedure TRec.SetB(const Value: string);
begin
FB := Value;
end;
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
FRec : TRec;
public
property Rec : TRec read FRec write FRec;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Rec.A := 21;
Rec.B := 'Hi';
end;
This compiles and workes without problem.
A solution I frequently use is to declare the property as a pointer to the record.
type
PRec = ^TRec;
TRec = record
A : integer;
B : string;
end;
TForm1 = class(TForm)
private
FRec : TRec;
function GetRec: PRec;
procedure SetRec(Value: PRec);
public
property Rec : PRec read GetRec write SetRec;
end;
implementation
function TForm1.GetRec: PRec;
begin
Result := #FRec;
end;
procedure TForm1.SetRec(Value: PRec);
begin
FRec := Value^;
end;
With this, directly assigning Form1.Rec.A := MyInteger will work, but also Form1.Rec := MyRec will work by copying all the values in MyRec to the FRec field as expected.
The only pitfall here is that when you wish to actually retrieve a copy of the record to work with, you will have to something like MyRec := Form1.Rec^
The compiler is stopping you from assigning to a temporary. The equivalent in C# is permitted, but it has no effect; the return value of the Rec property is a copy of the underlying field, and assigning to the field on the copy is a nop.
Because you have implicit getter and setter functions and you cannot modify the Result of a function as it is a const parameter.
(Note: In case you transform the record in an object, the result would actually be a pointer, thus equivalent to a var parameter).
If you want to stay with a Record, you have to use an intermediate variable (or the Field variable) or use a WITH statement.
See the different behaviors in the following code with the explicit getter and setter functions:
type
TRec = record
A: Integer;
B: string;
end;
TForm2 = class(TForm)
private
FRec : TRec;
FRec2: TRec;
procedure SetRec2(const Value: TRec);
function GetRec2: TRec;
public
procedure DoSomething(ARec: TRec);
property Rec: TRec read FRec write FRec;
property Rec2: TRec read GetRec2 write SetRec2;
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
{ TForm2 }
procedure TForm2.DoSomething(ARec: TRec);
var
LocalRec: TRec;
begin
// copy in a local variable
LocalRec := Rec2;
LocalRec.A := Arec.A; // works
// try to modify the Result of a function (a const) => NOT ALLOWED
Rec2.A := Arec.A; // compiler refused!
with Rec do
A := ARec.A; // works with original property and with!
end;
function TForm2.GetRec2: TRec;
begin
Result:=FRec2;
end;
procedure TForm2.SetRec2(const Value: TRec);
begin
FRec2 := Value;
end;
The simplest approach is:
procedure TForm1.DoSomething(ARec: TRec);
begin
with Rec do
A := ARec.A;
end;
This is because property are actually complied as a function. Properties only return or set a value. It is not a reference or a pointer to the record
so :
Testing.TestRecord.I := 10; // error
is same as calling a function like this:
Testing.getTestRecord().I := 10; //error (i think)
what you can do is:
r := Testing.TestRecord; // read
r.I := 10;
Testing.TestRecord := r; //write
It is a bit messy but inherent in this type of architecture.
Like others have said - the read property will return a copy of the record, so the assignment of fields isn't acting on the copy owned by TForm1.
Another option is something like:
TRec = record
A : integer;
B : string;
end;
PRec = ^TRec;
TForm1 = class(TForm)
private
FRec : PRec;
public
constructor Create;
destructor Destroy; override;
procedure DoSomething(ARec: TRec);
property Rec : PRec read FRec;
end;
constructor TForm1.Create;
begin
inherited;
FRec := AllocMem(sizeof(TRec));
end;
destructor TForm1.Destroy;
begin
FreeMem(FRec);
inherited;
end;
Delphi will dereference the PRec pointer for you, so things like this will still work:
Form1.Rec.A := 1234;
There's no need for a write part of the property, unless you want to swap the PRec buffer that FRec points at. I really wouldn't suggest to do such swapping via a property anyway.