Code that looks approximately like the following leaks memory for each string instance in each TMyRecord created. I presume I must visit each record and free it somehow -- can that be done without nilling each individual string?
function TMyForm.RecordFromThing(thing): TMyRecord;
begin
result.StringVal1 = thing.SomeProperty;
result.StringVal2 = thing.SomeOtherProperty;
end;
function TMyForm.RecordsFromItems: TList<TMyRecord>;
begin
result := TList<TMyRecord>.Create;
for thing in things do
begin
result.Add(RecordFromThing(thing));
end;
end;
procedure TMyForm.Button1Click(Sender: TObject);
var Items: TList<TMyRecord;
begin
Items := RecordsFromItems;
try
//Stuff
finally
// What goes here to free those records?
Items.Clear;
Items.Free;
end
end;
As per David's request, here is actual code cut-and-pasted from my application. If I run Button1Click then FastMM reports string memory leaks.
type TPremiumPaymentInstruction = record
SupplierID: string;
FirstName: string;
LastName: string;
PolicyNo: string;
CarrierName: string;
PayAmount: string;
DueDate: string;
PayMethod: string;
Comments: string;
Payee: string;
Address: string;
Address2: string;
City: string;
State: string;
Zip: string;
InRe: string;
BankName: string;
BankABA: string;
AccountName: string;
AccountNo: string;
CreditTo: string;
end;
function TPremiumPaymentManager.RecordFromItem(Item: TListItem): TPremiumPaymentInstruction;
var PolicyID: integer;
Instructions: TDataSet;
function FormatNote(PolicyNo, First, Last: string): string;
begin
result := 'Policy# ' + PolicyNo + '; Insured Name: ' + Last + ' ' + First;
end;
begin
FillChar(result, SizeOf(result), 0);
result.SupplierID := Item.Caption;
result.FirstName := Item.SubItems[INSURED_FIRST_NAME_COLUMN];
result.LastName := Item.SubItems[INSURED_LAST_NAME_COLUMN];
result.PolicyNo := Item.SubItems[POLICY_NUMBER_COLUMN];
result.CarrierName := Item.SubItems[CARRIER_NAME_COLUMN];
result.PayAmount := Item.SubItems[ACTUAL_COLUMN];
result.DueDate := Item.SubItems[DATE_DUE_COLUMN];
result.PayMethod := Item.SubItems[PAYMENT_METHOD_COLUMN];
PolicyID := GetSingleValue('SELECT PolicyID FROM PremiumsDue WHERE PremiumID = :PremiumID;', [(Item as TAdvListItem).KeyValue], 0);
Instructions := GetDS('SELECT I.* FROM Policies P INNER JOIN CarrierPaymentInstructions I ON P.CarrierID = I.CarrierID AND P.PaymentInstruction = I.InstructionDescription WHERE P.PolicyID = ?;', [PolicyID]);
try
Instructions.Open;
Instructions.First;
if result.PayMethod = 'Check' then
begin
result.Comments := Item.SubItems[PAYMENT_NOTE_COLUMN];
result.Payee := Instructions.FieldByName('PayTo').AsString;
result.Address := Instructions.FieldByName('Address1').AsString;
result.Address2 := Instructions.FieldByName('Address2').AsString;
result.City := Instructions.FieldByName('City').AsString;
result.State := Instructions.FieldByName('State').AsString;
result.Zip := Instructions.FieldByName('ZipCode').AsString;
result.InRe := FormatNote(result.PolicyNo, result.FirstName, result.LastName);
end
else
begin
result.BankName := Instructions.FieldByName('PayTo').AsString;
result.BankABA := Instructions.FieldByName('ABANumber').AsString;
result.Address2 := Instructions.FieldByName('Address2').AsString;
result.AccountName := Instructions.FieldByName('AccountName').AsString;
result.AccountNo := Instructions.FieldByName('AccountNumber').AsString;
result.CreditTo := FormatNote(result.PolicyNo, result.FirstName, result.LastName);
end;
finally
Instructions.Free;
end;
end;
function TPremiumPaymentManager.RecordsFromItems: TList<TPremiumPaymentInstruction>;
var item: TListItem;
begin
result := TList<TPremiumPaymentInstruction>.Create;
for item in lvPremiums.Items do
begin
if (not (Item as TAdvListItem).Strikeout) and (Item.SubItems[ACTUAL_COLUMN] <> '') then
result.Add( RecordFromItem(item) );
end;
end;
procedure TPremiumPaymentManager.Button1Click(Sender: TObject);
var Items: TList<TPremiumPaymentInstruction>;
begin
Items := RecordsFromItems;
Items.Clear;
Items.Free;
end;
Strings are managed types. The compiler takes charge of their allocation and deallocation. The compiler writes code that maintains reference counts to the string data. When the reference count goes to zero, the memory is deallocated.
As such, you don't need to deallocate strings or indeed perform any explicit management. Any attempt to do so will likely clash with that performed by the compiler. For instance, if you perform raw memory access on a string variable, saw with a call to FillChar
, then the reference counting will be bypassed.
The code in your question, beyond being incomplete and in parts not compiling, is essentially fine. When you are done with your TList<TMyRecord>
simply free it. The compiler will generate code to release all references. No need even to clear it.
List := TList<TMyRecord>.Create;
try
List.Add(...);
// etc.
finally
List.Free;
end;
That's all you need.
Since the code in you question is essentially the same as this, I conclude that the cause of your leak is elsewhere.
What do you know, your edit contains that tell-tale call to FillChar
. Replace that with
Result := Default(TPremiumPaymentInstruction);
You do always need to initialize return values. Even return values that are managed types which often are initialized. But not when the call is made inside a for loop in the manner of yours. Go figure. Anyway, always initialize return values. And Default(T)
is your friend.