如何从TObjectList<T>继承,而不是从TObjectList继承

11

为什么这个程序会报告内存泄漏?

{$APPTYPE CONSOLE}

uses
  System.Generics.Collections;

type
  TDerivedGenericObjectList = class(TObjectList<TObject>)
  public
    constructor Create;
  end;

constructor TDerivedGenericObjectList.Create;
begin
  inherited;
end;

var
  List: TDerivedGenericObjectList;

begin
  ReportMemoryLeaksOnShutdown := True;
  List := TDerivedGenericObjectList.Create;
  List.Add(TObject.Create);
  List.Free;
end.

1
重构可以帮助解决问题。 - smooty86
1
你的泄漏是因为你的代码有缺陷。我们看不到你的代码。无论如何,为什么要子类化呢?你可以直接使用 TObjectList<TPerson>。除非你想要添加方法。 - David Heffernan
@DavidHeffernan 我已经编辑了问题并添加了构造函数和用法。使用TObjectList的代码不会泄漏,但是如果我将TMembers声明更改为TMembers = class(TObjectList<TPerson>),那么上面的代码就会显示内存泄漏。是的,我想要添加方法。我在这里展示的只是一个简单的虚构示例,希望能得到清晰的答案。 - alondono
1
我重新改写了你的问题,使其只询问我认为你想要问的内容,根据你的评论。此外,有了这个问题列表,回答变得更容易了!!;-) - David Heffernan
1
在我的经验中,像这样一个简单完整的程序可以使回答问题变得更容易,同时也有助于您,因为它的重点清晰明了,没有障碍。 - David Heffernan
显示剩余7条评论
2个回答

14
您正在调用无参构造函数TObjectList<T>,实际上这是TObjectList<T>的父类TList<T>的构造函数。 TObjectList<T>中声明的所有构造函数都接受名为AOwnsObjects的参数,该参数用于初始化OwnsObjects属性。因为您绕过了该构造函数,OwnsObjects默认为False,因此列表成员不会被销毁。
您应该确保调用初始化OwnsObjectsTObjectList<T>构造函数。例如:
{$APPTYPE CONSOLE}

uses
  System.Generics.Collections;

type
  TDerivedGenericObjectList = class(TObjectList<TObject>)
  public
    constructor Create;
  end;

constructor TDerivedGenericObjectList.Create;
begin
  inherited Create(True);
end;

var
  List: TDerivedGenericObjectList;

begin
  ReportMemoryLeaksOnShutdown := True;
  List := TDerivedGenericObjectList.Create;
  List.Add(TObject.Create);
  List.Free;
end.

也许更好的变体是使您的构造函数还提供AOwnsObjects参数:

type
  TDerivedGenericObjectList = class(TObjectList<TObject>)
  public
    constructor Create(AOwnsObjects: Boolean = True);
  end;

constructor TDerivedGenericObjectList.Create(AOwnsObjects: Boolean);
begin
  inherited Create(AOwnsObjects);
end;

或者:

type
  TDerivedGenericObjectList = class(TObjectList<TObject>)
  public
    constructor Create(AOwnsObjects: Boolean = True);
  end;

constructor TDerivedGenericObjectList.Create(AOwnsObjects: Boolean);
begin
  inherited;
end;

那么,您可能会想知道为什么原始版本选择了 TList<T> 构造函数而不是 TObjectList<T> 中的构造函数。好的,让我们更详细地看一下这个问题。以下是您的代码:

type
  TDerivedGenericObjectList = class(TObjectList<TObject>)
  public
    constructor Create;
  end;

constructor TDerivedGenericObjectList.Create;
begin
  inherited;
end;

当以这种方式使用 inherited 时,编译器会寻找与当前构造函数精确相同签名的构造函数。由于 TObjectList<T> 中的所有构造函数都有参数,因此它无法在其中找到合适的构造函数,但是它可以在 TList<T> 中找到一个,并使用它。

正如您在评论中提到的,以下变体不会泄漏:

constructor TDerivedGenericObjectList.Create;
begin
  inherited Create;
end;

与裸的inherited相比,这个语法将在默认参数被替换时找到匹配的方法。因此,TObjectList<T>的单参数构造函数会被调用。

文档中有以下信息:

保留字inherited在实现多态行为时扮演着特殊角色。它可以出现在方法定义中,可以带或不带标识符。

如果inherited后面跟着成员的名称,则表示对属性、字段或普通方法的调用或引用,但是查找所引用的成员始于封闭方法类的直接祖先。例如,在以下情况下:

inherited Create(...);

在方法的定义中出现时,它会调用继承的Create。

当inherited后面没有标识符时,它指的是与封闭方法同名的继承方法,或者如果封闭方法是消息处理器,则指的是相同消息的继承消息处理器。在这种情况下,inherited不需要显式参数,但会将封闭方法调用时使用的相同参数传递给继承方法。例如:

inherited;

在构造函数的实现中经常出现。它使用传递给子类的相同参数调用继承的构造函数。


我刚刚尝试了最佳解决方案,实际上删除 True 参数也可以工作。所以,你只需要 inherited Create; - alondono
2
但最好是明确的。我版本的 TObjectList<T> 强制调用者传递 OwnsObjects。 - David Heffernan

1
您可以使用泛型。它可以在不需要类型转换和内存泄漏的情况下正常工作(例如 TObjectList<T>TObjectDictionary<T> 列表在释放命令时自动销毁内部对象)。
一些提示:
  • TObjectList<TPerson> -- 像 membersList.Free 一样,在释放时自动销毁人员列表;

  • TList<TPerson> -- 不要销毁人员列表。您必须创建析构函数并手动释放列表中的每个人员;

这是您的代码示例(使用新构造函数、无内存泄漏,并具有向后兼容性,参见 GetPerson):
    type
      TPerson = class
      public
        Name: string;
        Age: Integer;

        function Copy: TPerson;
      end;

      TMembers = class(TObjectList<TPerson>)
      private
        function GetPerson(i: Integer): TPerson;
      public
        property Person[i: Integer]: TPerson read GetPerson;

        constructor Create(SourceList: TMembers); overload;
      end;


    { TPerson }

    function TPerson.Copy: TPerson;
    var
      person: TPerson;
    begin
      person := TPerson.Create;
      person.Name := Self.Name;
      person.Age := Self.Age;
      Result := person;
    end;

    { TMembers }

    constructor TMembers.Create(SourceList: TMembers);
    var
      person: TPerson;
    begin
      inherited Create;

      for person in SourceList do
      begin
        Self.Add(person.Copy);
      end;
    end;

    function TMembers.GetPerson(i: Integer): TPerson;
    begin
      Result := Self[i];
    end;

    procedure TForm21.Button1Click(Sender: TObject);
    var
      person: TPerson;
      memsList1: TMembers;
      memsList2: TMembers;
    begin
      // test code

      memsList1 := TMembers.Create;

      person := TPerson.Create;
      person.Name := 'name 1';
      person.Age := 25;
      memsList1.Add(person);

      person := TPerson.Create;
      person.Name := 'name 2';
      person.Age := 27;
      memsList1.Add(person);

      memsList2 := TMembers.Create(memsList1);

      ShowMessageFmt('mems 1 count = %d; mems 2 count = %d', [memsList1.Count, memsList2.Count]);

      FreeAndNil(memsList1);
      FreeAndNil(memsList2);
    end;

你应该描述为什么没有内存泄漏,仅有代码是不足以理解的。 - Sir Rufo
因为通用类TObjectList<T>在执行free命令后会自动销毁列表中的所有内部对象。 - JayDi
1
好的,OP从TObjectList<T>继承了他的类,并且他有内存泄漏问题。这就是需要回答的问题。 - Sir Rufo
正如我在问题中所说,这是一个使用TObjectList的旧代码,一直没有内存泄漏问题,但当我尝试改用TObjectList<T>时,现在出现了内存泄漏。我已经尝试了问题中提到的各种方法,但都没有成功。 - alondono
因为您的原始代码调用了一个将OwnsObjects初始化为true的TObjectList构造函数,而您的TObjectList<T>代码调用了一个将OwnsObjects初始化为false的构造函数。因此发生了泄漏。David的答案详细解释了为什么会在您的新代码中发生这种情况。 - Remy Lebeau
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接