delphirecordtlist

TList of Record types leaking memory


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;

Solution

  • 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.