Delphi类引用...也称元类...何时使用它们

17

我已经阅读了官方文档,了解了类引用是什么,但是我不明白相较于其他替代方案,何时何地使用它们是最佳方案。

文档中引用的示例是TCollection,可以使用任何TCollectionItem的后代来实例化。使用类引用的理由是,它允许您调用在编译时(我假设这是TCollection的编译时)未知类型的类方法。我只是看不到使用TCollectionItemClass作为参数比使用TCollectionItem更好。TCollection仍然能够容纳任何TCollectionItem的后代,并且仍然能够调用TCollectionItem中声明的任何方法。不是吗?

将其与通用集合进行比较。TObjectList似乎提供与TCollection类似的功能,还具有类型安全性的附加优点。您不必继承自TCollectionItem以存储对象类型,而且可以使集合尽可能具体化。如果需要从集合内访问项的成员,则可以使用通用约束。除了在Delphi 2009之前程序员可以使用类引用外,是否有其他强制使用它们而不是通用容器的理由?

文档中提供的另一个示例是将类引用传递给充当对象工厂的函数。在这种情况下,是用于创建TControl类型对象的工厂。它并不明显,但我认为TControl工厂调用了传递给它的后代类型的构造函数,而不是TControl的构造函数。如果是这样的话,那么我开始看到使用类引用的一些理由。

所以,我想真正理解的是,何时何地使用类引用最合适,以及它们对开发人员有什么帮助


1
如果你正在用TObjectList替换TCollection,那么你实际上并没有真正使用TCollection的设计目的。它是用于在对象检查器中设计时需要编辑一组事物的情况。 - Rob Kennedy
2
@Rob 我使用TCollection作为示例,因为官方的Delphi文档也是这样做的。我提出问题的目的是更好地理解类引用。 - Kenneth Cochran
5个回答

29

元类和“类过程”

元类主要涉及“类过程”。从一个基本的class开始:

type
  TAlgorithm = class
  public
    class procedure DoSomething;virtual;
  end;

因为DoSomething是一个类方法,我们可以不需要TAlgorithm的实例就调用它(它像任何其他全局过程一样)。我们可以这样做:

TAlgorithm.DoSomething; // this is perfectly valid and doesn't require an instance of TAlgorithm

一旦我们完成了这个设置,我们可能会创建一些替代算法,所有这些算法都共享基础算法的一些部分。就像这样:

type
  TAlgorithm = class
  protected
    class procedure DoSomethingThatAllDescendentsNeedToDo;
  public
    class procedure DoSomething;virtual;
  end;

  TAlgorithmA = class(TAlgorithm)
  public
    class procedure DoSomething;override;
  end;

  TAlgorithmB = class(TAlgorithm)
  public
    class procedure DoSomething;override;
  end;

我们现在有一个基础类和两个派生类。以下代码是完全有效的,因为我们将方法声明为“class”方法:
TAlgorithm.DoSomething;
TAlgorithmA.DoSomething;
TAlgorithmB.DoSomething;

让我们介绍一下 "class of" 类型:

type
  TAlgorithmClass = class of TAlgorithm;

procedure Test;
var X:TAlgorithmClass; // This holds a reference to the CLASS, not a instance of the CLASS!
begin
  X := TAlgorithm; // Valid because TAlgorithmClass is supposed to be a "class of TAlgorithm"
  X := TAlgorithmA; // Also valid because TAlgorithmA is an TAlgorithm!
  X := TAlgorithmB;
end;

TAlgorithmClass是一种数据类型,可以像其他数据类型一样使用,可以存储在变量中,作为函数的参数传递。换句话说,我们可以这样做:

procedure CrunchSomeData(Algo:TAlgorithmClass);
begin
  Algo.DoSomething;
end;

CrunchSomeData(TAlgorithmA);

在这个例子中,CrunchSomeData过程可以使用任何算法变种,只要它是TAlgorithm的后代。
以下是该行为在实际应用程序中的示例:想象一个类似于工资单的应用程序,其中一些数字需要根据法律规定的算法进行计算。由于法律有时会更改,因此这个算法可能会随之改变。我们的应用程序需要使用最新版本的计算器来计算当年的工资,并使用较旧版本的算法来计算其他年份的工资。下面是具体实现的方式:
// Algorithm definition
TTaxDeductionCalculator = class
public
  class function ComputeTaxDeduction(Something, SomeOtherThing, ThisOtherThing):Currency;virtual;
end;

// Algorithm "factory"
function GetTaxDeductionCalculator(Year:Integer):TTaxDeductionCalculator;
begin
  case Year of
    2001: Result := TTaxDeductionCalculator_2001;
    2006: Result := TTaxDeductionCalculator_2006;
    else Result := TTaxDeductionCalculator_2010;
  end;
end;

// And we'd use it from some other complex algorithm
procedure Compute;
begin
  Taxes := (NetSalary - GetTaxDeductionCalculator(Year).ComputeTaxDeduction(...)) * 0.16;
end;

虚拟构造函数

Delphi中的构造函数就像一个"类函数";如果我们有一个元类,并且该元类知道一个虚构造函数,那么我们就可以创建后代类型的实例。TCollection的IDE编辑器在您按下"添加"按钮时使用此功能来创建新项目。TCollection只需要获取TCollectionItem的元类即可使其正常工作。


2
我很好奇为什么有人会对这个答案投反对票。Cosmin显然在这方面付出了很多思考和努力。除非它明显是错误或误导的,而它似乎并不是,否则这个反对票是没有根据的。 - Kenneth Cochran
1
呵呵,我的第一个踩!耶耶……我真的花了很多心思来解释整个元类概念,将“虚拟构造函数”转化为更大概念的特殊情况。我还有意识地给出了一个不在文档中的示例(毕竟每个人都知道文档中的内容)。但是嘿,这就是SO的工作方式。我有时会对被作者标记为“无效”的答案被点赞感到惊讶。那么为什么不踩一个好答案呢? - Cosmin Prund
普通的虚方法需要一个实例才能工作,而类虚方法可以在没有实例的情况下工作。但我承认这非常不常见。 - Cosmin Prund
1
实际上,在 Delphi 中这是非常常见的。最好的例子就是 TComponent,它的虚拟构造函数每次从流中加载组件时都会被调用(比如当创建/加载一个窗体及其所有组件都从流中加载时)。 - davidmw

9

是的,一个集合仍然可以持有任何TCollectionItem的后代,并调用它的方法。但是,它将无法实例化任何TCollectionItem的后代的新实例。调用TCollectionItem.Create会构造一个TCollectionItem的实例,而

private
  FItemClass: TCollectionItemClass;
...

function AddItem: TCollectionItem;
begin
  Result := FItemClass.Create;
end;

会构造一个TCollectionItem子类的实例,该子类由FItemClass持有。

我对通用容器并不熟悉,但如果可以选择,我会选择通用容器。但无论哪种情况,如果我想要列表在容器中添加项目时实例化并执行其他操作,而且我事先不知道确切的类,我仍然需要使用元类。

例如,一个可观察的TObjectList子类(或通用容器)可以有如下内容:

function AddItem(aClass: TItemClass): TItem;
begin
  Result := Add(aClass.Create);
  FObservers.Notify(Result, cnNew);
  ...
end;

我想简单来说,元类的优点/好处是任何只知道方法/类的知识的东西都可以使用。
type
  TMyThing = class(TObject)
  end;
  TMyThingClass = class of TMyThing;

可以构造任何TMyThing的子类实例,无论它们在哪里声明。

这比我的解释更好。 - Warren P

5

泛型非常有用,我同意TObjectList<T>通常比TCollection更有用。但是对于不同的场景,类引用更加有用。它们实际上是不同范例的一部分。例如,当您有一个需要重写的虚方法时,类引用可以很有用。虚方法重写必须具有与原始方法相同的签名,因此泛型范例在这里不适用。

类引用广泛用于窗体流式传输。有时将DFM视为文本,您会看到每个对象都按名称和类引用。 (实际上名称是可选的。)当窗体读取器读取对象定义的第一行时,它获得类的名称。它在查找表中查找并检索类引用,然后使用该类引用调用该类的TComponent.Create(AOwner:TComponent)重写,以便它可以实例化正确类型的对象,然后开始应用DFM中描述的属性。这就是类引用带给您的东西,而泛型无法做到。


2

每当我需要能够创建不仅是一个硬编码类,而是任何继承自我的基类的类的工厂时,我也会使用元类。

然而,在 Delphi 领域中,我不熟悉“元类”这个术语。我们称之为类引用,这个名字听起来没有那么“神奇”,所以你在问题中提到了两个常见的名称,这非常好。

我见过这种方法被成功地应用的一个具体例子是在 JVCL JvDocking 组件中,其中“停靠样式”组件向基本停靠组件集提供元类信息。因此,当用户拖动鼠标将客户端表单停靠到停靠主机表单时,显示抓取器栏(类似于常规未停靠窗口的标题栏)的“选项卡主机”和“连接主机”表单可以是用户定义的插件类,该类提供自定义外观和基于插件的自定义运行时功能。


1
在我的一些应用程序中,我有一个机制,它可以将类连接到能够编辑该类的一个或多个实例的表单上。我有一个中央列表,其中存储了这些配对:一个类引用和一个表单类引用。因此,当我有一个类的实例时,我可以查找相应的表单类,从中创建一个表单并让它编辑该实例。
当然,这也可以通过一个返回适当表单类的类方法来实现,但这需要类知道表单类。我的方法使系统更加模块化。表单必须知道类,但反过来则不需要。当您无法更改类时,这可能是关键点。

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