15

我已经阅读了官方文档,并且我了解类引用是什么,但是与替代方案相比,我看不到它们何时以及为什么是最佳解决方案。

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

将此与通用集合进行比较。TObjectList 似乎提供了与 TCollection 大致相同的功能,并增加了类型安全的好处。您无需从 TCollectionItem 继承以存储您的对象类型,并且您可以根据需要将集合设为特定类型。如果您需要从集合中访问项目的成员,您可以使用通用约束。除了 Delphi 2009 之前的程序员可以使用类引用这一事实之外,还有其他令人信服的理由在通用容器上使用它们吗?

文档中给出的另一个示例是将类引用传递给充当对象工厂的函数。在这种情况下,一个用于创建 TControl 类型对象的工厂。它不是很明显,但我假设 TControl 工厂正在调用传递给它的后代类型的构造函数,而不是 TControl 的构造函数。如果是这种情况,那么我开始看到至少有一些使用类引用的理由。

所以我想我真正想了解的是何时何地类引用最合适,他们为开发人员了什么?

4

5 回答 5

26

元类和“类过程”

元类都是关于“类过程”的。从一个基本的开始class

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

因为DoSomething是 aclass procedure我们可以在没有 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;

我们现在有一个基类和两个后代类。以下代码完全有效,因为我们将这些方法声明为“类”方法:

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 的后代。

以下是如何在实际应用程序中使用此行为的示例:想象一个工资单类型的应用程序,其中一些数字需要根据 Law 定义的算法进行计算。可以想象这个算法会随着时间的推移而改变,因为法律有时会改变。我们的应用程序需要计算当前年份(使用最新的计算器)和其他年份的薪水,使用旧版本的算法。以下是事情的样子:

// 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 工作所需的所有 TCollection 都是 TCollectionItem 的 MetaClass。

于 2010-07-30T20:26:22.713 回答
8

是的,一个 Collection 仍然能够保存 TCollectionItem 的任何后代并在其上调用方法。但是,它不能实例化 TCollectionItem 的任何后代的新实例。调用 TCollectionItem.Create 构造 TCollectionItem 的实例,而

private
  FItemClass: TCollectionItemClass;
...

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

将构造 FItemClass 中保存的任何 TCollectionItem 后代类的实例。

我对通用容器没有做太多,但我认为如果可以选择,我会选择通用容器。但是在任何一种情况下,如果我想让列表实例化并在容器中添加项目并且我事先不知道确切的类时做任何其他需要做的事情,我仍然必须使用元类。

例如,可观察的 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 的任何后代的实例,无论它们可能已被声明。

于 2010-07-30T16:00:29.340 回答
4

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

大量使用类引用的一个地方是表单流。有时将 DFM 视为文本,您会看到每个对象都按名称和类引用。(实际上,名称是可选的。)当表单阅读器读取对象定义的第一行时,它会获取类的名称。它在查找表中查找它并检索一个类引用,并使用该类引用来调用该类的覆盖,TComponent.Create(AOwner: TComponent)以便它可以实例化正确类型的对象,然后它开始应用 DFM 中描述的属性。这是类引用给你买的那种东西,它不能用泛型来完成。

于 2010-07-30T15:46:11.573 回答
1

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

然而,元类并不是我在 Delphi 圈子中熟悉的术语。我相信我们称它们为类引用,它的名字听起来不那么“神奇”,所以你把这两个常见的名字都放在你的问题中真是太好了。

我见过的一个很好用的地方的具体示例是在 JVCL JvDocking 组件中,其中“停靠样式”组件向基本停靠组件集提供元类信息,因此当用户拖动鼠标并将客户端表单停靠到停靠主机表单,显示抓取栏的“标签主机”和“联合主机”表单(外观类似于常规非停靠窗口的标题栏)可以是用户定义的插件类,提供自定义外观以及基于插件的自定义运行时功能。

于 2010-07-30T16:17:23.220 回答
0

在我的一些应用程序中,我有一种将类连接到能够编辑一个或多个类的实例的表单的机制。我有一个存储这些对的中央列表:一个类引用和一个表单类引用。因此,当我有一个类的实例时,我可以查找相应的表单类,从中创建一个表单并让它编辑该实例。

当然,这也可以通过让类方法返回适当的表单类来实现,但这需要类知道表单类。我的方法使系统更加模块化。表单必须知道类,但不能反过来。当您无法更改课程时,这可能是一个关键点。

于 2010-07-30T15:55:26.227 回答