从类引用构造对象

时间:2009-01-21 14:23:30

标签: delphi design-patterns oop

我有一个构造对象,调用Execute方法并释放对象的方法。对象的类型由传递给方法的TClass后代确定。 请注意,这是我正在谈论的Delphi for Win32,而不是.NET。

编辑:我应该指出这是Delphi 2006,因为在下面的答案中已经注意到,在未来版本中可能不需要NewInstance调用。但就我而言,这是必需的。因此,我会想象我的问题的答案(它是否安全?并且CreateForm()是否有潜在的泄漏)需要在此基础上回答这是Delphi 2006

编辑#2:似乎为D2007& D2009实际上为D2006工作。我必须从早期版本的Delphi中获得“NewInstance”习惯......

function TPageClassFactory.TryExecute(ScrnClass: TCustomPageClass): boolean;
//TCustomPageClass = class of TCustomPage
var
  ScrnObj: TCustomPage; //TCustomPage defines an abstract Execute() method
begin
  Result := FALSE; //default
  ScrnObj := TCustomPage(ScrnClass.NewInstance); //instantiate
  try
    ScrnObj.Create(Self);  //NB: Create() and Execute() are *virtual* methods
    ScrnObj.Execute;       
  finally
    FreeAndNil(ScrnObj);
  end;
  Result := TRUE;
end;

我想知道的是这是否安全 - 如果Create()引发异常,会发生什么?

从Forms.pas.TApplication.CreateForm()看一个类似的例子,对异常处理采取了不同的方法(我已经删除了下面不相关的位):

procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference);
var
  Instance: TComponent;
begin
  Instance := TComponent(InstanceClass.NewInstance);
  TComponent(Reference) := Instance;
  try
    Instance.Create(Self);
  except
    TComponent(Reference) := nil;
    raise;
  end;
end;

在Forms.pas方法中,这是否意味着在Create()方法中发生异常时内存泄漏?我的理解是InstanceClass.NewInstance分配了内存,因此在这种情况下内存没有被释放/释放/释放?

4 个答案:

答案 0 :(得分:12)

你应该把try放在try finally块之外。

但更好的解决方案是:

type 
  TMyClass = class ()
  public
    constructor Create(...); virtual;
    function Execute: Boolean; virtual;
  end;
  TMyClassClass = class of TMyClass;


procedure CreateExecute(const AClass: TMyClassClass): Boolean;
var
  theclass : TMyClass;
begin
  theclass := AClass.Create;
  try
    Result := theclass.Execute;
  finally
    theclass.Free;
  end;
end;

答案 1 :(得分:4)

在评论中提出了一些我想澄清的问题。

首先是构造函数需要虚拟的持续神话。它确实。考虑这个例子:

type
  TBase = class
    constructor Create(x: Integer);
  end;
  TDerived = class(TBase)
    field: string;
  end;
  TMetaclass = class of TBase;

var
  instance: TBase;
  desiredClass: TMetaclass;
begin
  desiredClass := TDerived;
  instance := desiredClass.Create(23);
  Assert(instance.ClassName = 'TDerived');
  Assert(instance is TDerived);
  Assert(instance.field = '');
end;

创建的对象将是类TDerived的完整实例。将分配足够的内存来保存字符串字段,这在基类中不存在。

在您需要虚拟构造函数之前,必须满足两个条件:

  1. 将虚拟地调用构造函数。也就是说,您将拥有基类元类类型的变量,并且它将保存派生类的值,并且您将在该变量上调用构造函数。这在上面的代码中得到了证明。如果所有构造函数调用都直接在类名本身上(即TDerived.Create(23)),那么虚拟方法就无法获得任何东西。
  2. 基类的子类需要覆盖构造函数以提供特定于类的初始化。如果所有后代都使用相同的结构,并且只在其他方法中有所不同,那么就不需要将构造函数设置为虚拟。
  3. 在这里要认识到的重要一点是,这两个规则与确定何时使任何其他方法虚拟的因素没有区别。在这方面,构造者并不特别。

    构造函数知道要构造的类不是基于定义构造函数的类,而是基于类的构造函数被调用,并且该类总是作为每个构造函数调用的隐藏的第一个参数传递。


    其次是应该调用NewInstance代替构造函数还是除了构造函数之外的问题。我认为其他评论已经确定它与旧版Delphi版本的兼容性无关。 所有版本都支持在类引用上调用构造函数,而不需要NewInstace。相反,混淆来自于TApplication.CreateForm并将其视为应如何完成事情的一个例子。这是一个错误。

    CreateForm在调用构造函数之前调用NewInstance,因为CreateForm存在的主要原因是确保IDE在表单自己的事件处理程序中声明的全局表单变量有效,包括OnCreate,它作为构造函数的一部分运行。如果CreateForm方法已经完成了通常的构造模式,那么全局表单变量将不具有有效值。以下是您可能希望看到的内容:

    TComponent(Reference) := InstanceClass.Create(Application);
    

    简单而明显,但这不起作用。 {<1}}在构造函数返回之后才会被分配一个值,这在表单触发某些事件之后很久。如果您遵循良好的编程习惯并且从不在表单类本身中引用该变量,那么您将永远不会注意到。但是,如果您按照为没有经验的受众编写的文档说明,那么从表单自己的方法中引用全局表单变量,因此Reference方法执行它的操作可以确保及时分配。

    为此,它使用两步构造技术。首先,分配内存并将引用分配给全局变量:

    CreateForm

    接下来,调用实例上的构造函数,将Instance := TComponent(InstanceClass.NewInstance); TComponent(Reference) := Instance; 对象作为所有者传递:

    TApplication

    It's my opinion Instance.Create(Self); 应该在任何程序中只调用一次CreateForm。我更喜欢零次,但它有定义Application.MainForm的副作用,这对于Delphi程序的其他方面很重要。


    第三种观点是,对象自身调用构造函数是不常见的。

    事实上,这种情况一直发生 。每次调用继承的构造函数时,都会在已存在的对象上调用构造函数。继承的构造函数未分配新对象。同样,VCL有一些非继承的构造函数调用的例子。 TCustomForm.Create将其大部分构造任务委托给CreateNew构造函数。

答案 2 :(得分:2)

修改:

没有完全记住它在旧的delphi版本中是如何的,但显然这应该适用于所有基于其他回复。

注意,只要我记得,Create就会在失败时调用Destroy。它不应该在我想到之后。

代码将是:

procedure TPageClassFactory.TryExecute(ScrnClass: TCustomPageClass);
var
  ScrnObj: TCustomPage;
begin
  ScrnObj := ScrnClass.Create(Self);  // Exception here calls the destructor
  try
    ScrnObj.Execute; // Exception here means you need to free manually      
  finally
    FreeAndNil(ScrnObj); // Be free!
  end;
end;

我删除了原始函数返回的结果,因为它永远不会为false,只有“unassigned”(异常)或true。在将结果赋值为false之前,您可以获得异常。 ;)

答案 3 :(得分:2)

当Create()引发异常时,重新提出有关内存泄露的问题:您应该自己尝试一下。我刚刚在Delphi 2007上做过,并且使用你的代码FastMM4显示了一个关于尝试在已释放的对象上调用虚方法的错误对话框,即Destroy()。因此,Create中的异常将导致析构函数被调用并释放内存,因此您的代码实际上是错误的。坚持answer by Gamecat中使用的习语,一切都应该有用。

修改

我刚试过Delphi 4,行为是一样的。测试代码:

type
  TCrashComp = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

constructor TCrashComp.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  raise Exception.Create('foo');
end;

destructor TCrashComp.Destroy;
begin
  Beep;
  inherited Destroy;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  C: TComponent;
begin
  C := TComponent(TCrashComp.NewInstance);
  try
    C.Create(nil);
    C.Tag := 42;
  finally
    C.Free;
  end;
end;

使用FastMM4,finally块中的Free会产生相同的错误,因为C已经被释放了。但是,在应用程序关闭时,异常和异常字符串将报告为内存泄漏。然而,这不是代码的问题,而是运行时。