在Delphi中创建自身实例的类函数

7

你是否可以拥有一个类函数来创建一个类的实例:

TMyClass = class(TSomeParent)
public
  class function New(AValue : integer) : TMyClass; 
end;

TDerivedClass = class(TMyClass)
public
  function Beep;
end;

然后按照以下方式使用它。
...   
var
  myList : TList<T>;
  item : TDerivedClass;
begin
  myList.Add(TDerivedClass.New(1))
  myList.Add(TDerivedClass.New(3))
  myList.Add(TDerivedClass.New(5))

  for item in myList do
    item.Beep; //times the count in the class function
...

如果是这样的话,这个函数代码看起来是什么样子的呢?您会使用TObject的NewInstance方法并且每次都为每个派生类重新实现吗?使用构造函数是否更安全/更好?

目标是在命令模式中使用此方法,并使用类类型和接收器加载命令列表,例如:

//FYI: document is an instance of TDocument
commandList.Execute(TOpenDocument(document)); 
commandList.Execute(TPasteFromClipboard(document)); 
//... lots of actions - some can undo
commandList.Execute(TPrintDocument(document)); 
commandList.Execute(TSaveDocument(document));

原因是有些命令将通过文本/脚本指定,并需要在运行时解析。

5个回答

13
你需要的是称为工厂模式的东西。在Delphi中可以实现它;这是VCL反序列化表单的方式之一。你缺少的是系统的注册和查找部分。基本思路如下:
  • 在某个地方,你设置了一个注册表。如果你使用Delphi XE,你可以将其实现为TDictionary<string, TMyClassType>,其中TMyClassType定义为class of TMyClass。这很重要。你需要一个类名和类类型引用之间的映射。
  • 在TMyClass上放置一个虚构造函数。从它派生的所有内容都将在工厂模式创建它时使用此构造函数或其重写。
  • 当你创建一个新的派生类时,请调用一个方法,使其在注册表中注册自己。这应该在程序启动时发生,可以在initialization或类构造函数中完成。

当你需要从脚本实例化某些东西时,请按如下方式操作:

 class function TMyClass.New(clsname: string; [other params]): TMyClass;
 begin
   result := RegistrationTable[clsName].Create(other params);
 end;
你可以使用注册表从类名中获取类引用,并在类引用上调用虚拟构造函数,以从中获取正确类型的对象。

哇,谈论过度设计一个简单的问题! - Rob Kennedy
1
@Rob:嗯?这是最简单的方式之一,“创建从基类型派生的对象,其实际类型来自脚本”,并使其正常工作。 - Mason Wheeler
@Mason:目前我使用RegisterClass()GetClass()(因为如果不在代码中使用类,则该类不可用)。分辨率当前基于类名作为输入,但这是旧风格,上述方法也采用自定义属性,当加载到字典时,将导致RTTI注册类,无法避免注册部分+使用字典方法,您可以有多个分辨率入口到命令。问题是构造函数/新问题(真的很愚蠢),感谢您对模式的输入。 - MX4399

5

是的,在技术上,通过调用实际的构造函数并返回它创建的实例,可以从类方法中创建一个实例,例如:

type
  TMyClass = class(TSomeParent)
  public
    constructor Create(AValue : Integer); virtual;
    class function New(AValue : integer) : TMyClass;
  end;

  TDerivedClass = class(TMyClass)
  public
    constructor Create(AValue : Integer); override;
    function Beep;
  end;

constructor TMyClass.Create(AValue : Integer);
begin
  inherited Create;
  ...
end;

function TMyClass.New(AValue : integer) : TMyClass;
begin
  Result := Create(AValue);
end;

constructor TDerivedClass.Create(AValue : Integer);
begin
  inherited Create(AValue);
  ...
end;

var
  myList : TList<TMyClass>;
  item : TMyClass;
begin
  myList.Add(TDerivedClass.New(1))
  myList.Add(TDerivedClass.New(3))
  myList.Add(TDerivedClass.New(5))
  for item in myList do
    TDerivedClass(item).Beep;

如果是这种情况,您最好直接使用构造函数:
type
  TMyClass = class(TSomeParent)
  end;

  TDerivedClass = class(TMyClass)
  public
    constructor Create(AValue : Integer);
    function Beep;
  end;

var
  myList : TList<TDerivedClass>;
  item : TDerivedClass;
begin
  myList.Add(TDerivedClass.Create(1))
  myList.Add(TDerivedClass.Create(3))
  myList.Add(TDerivedClass.Create(5))
  for item in myList do
    item.Beep;

2
你能否创建一个类函数来创建类的实例?使用构造函数是否更安全/更好?
构造函数是创建类实例的类函数。 只需这样写:
constructor New(); virtual;

现在你已经准备就绪。

virtual; 部分将允许您为所有派生类调用相同的 New() 构造函数。


没必要将构造函数命名为New而不是Create,也没有必要将其声明为虚函数。 - Rob Kennedy
@Rob:当通过类变量进行构造时,存在构造函数重载的原因。我同意Create是一个更好的名称;这里使用New()只是为了与问题相关,其中New()被作为类函数的名称。 - dmajkic
1
虚拟构造函数只有在使用类引用变量时才有用,这是正确的,但并不意味着虚拟构造函数是必需的。只有在需要在派生类中重写它们时才需要虚拟构造函数;这与任何其他使用虚拟方法的情况没有区别。问题没有表明后代需要与其基类不同的构造行为(除了构造不同的类,但这是自动的)。 - Rob Kennedy

1
另一个选择是使用RTTI。下面的代码在我的类中作为普通方法运行,以获取具有子集项目的对象的新实例,但由于项目(以及列表对象本身)可能是后代对象,因此创建定义该方法的实例不够好,因为它需要与实例的相同类型。即:
TParentItem = Class
End;

TParentList = Class
  Items : TList<TParentItem>;
  Function GetSubRange(nStart,nEnd : Integer) : TParentList;
End;

TChildItem = Class(TParentItem)
end

TChildList = Class(TParentList)
end

List := TChildList.Create;
List.LoadData;

SubList := List.GetSubRange(1,3);

GetSubRange的实现大概是这样的...
Function TParentList.GetSubRange(nStart,nEnd : Integer) : TParentList;
var
  aContext: TRttiContext;
  aType: TRttiType;
  aInsType : TRttiInstanceType;
  sDebug : String;
begin
  aContext := TRttiContext.Create;
  aType := aContext.GetType(self.ClassType);
  aInsType := aType.AsInstance;
  Result := aInsType.GetMethod('Create').Invoke(aInsType.MetaclassType,[]).AsType<TParentList>;
  sDebug := Result.ClassName; // Should be TChildList

  // Add the items from the list that make up the subrange.
End;

我知道有些事情可能过于夸张,但在上面的设计中,它起作用并且是另一种选择,尽管我知道它不是一个类方法。


0

你应该使用构造函数(一种特殊的类函数)。TObject.NewInstance 不是一个合适的选项,除非你需要特殊的内存分配。

关于命令列表的 Execute 程序:现在涉及到的操作取决于对象的类型。想象一下一个文档可以同时打开、打印、粘贴和保存(这不是奇怪的假设),在这个结构中实现起来会很困难。相反,考虑添加接口(IOpenDocument、IPasteFromClipboard、IPrintable、ISaveDocument),它们确实都可以成为一个文档实例的操作。


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