Delphi 类引用...又名元类...何时使用它们

发布于 2024-09-11 19:28:29 字数 738 浏览 3 评论 0原文

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

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

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

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

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

I've read the official documentation and I understand what class references are but I fail to see when and why they are best solution compared to alternatives.

The example cited in the documentation is TCollection which can be instantiated with any descendant of TCollectionItem. The justification for using a class reference is that it allows you to invoke class methods whose type is unknown at compile time (I assume this is the compile time of TCollection). I'm just not seeing how using TCollectionItemClass as an argument is superior to using TCollectionItem. TCollection would still be able to hold any descendant of TCollectionItem and still be able to invoke any method declared in TCollectionItem. Wouldn't it?

Compare this with a generic collection. TObjectList appears to offer much the same functionality as TCollection with the added benefit of type safety. You are freed from the requirement to inherit from TCollectionItem in order to store your object type and you can make a collection as type specific as you want. And if you need to access item's members from within the collection you can use generic constraints. Other than the fact that class references are available to programmers prior to Delphi 2009 is there any other compelling reason to use them over generic containers?

The other example given in the documentation is passing a class reference to a function that acts as an object factory. In this case a factory for creating objects of type TControl. Its not really apparent but I'm assuming the TControl factory is invoking the constructor of the descendant type passed to it rather than the constructor of TControl. If this is the case then I'm starting to see at least some reason for using class references.

So I guess what I'm really trying to understand is when and where class references are most appropriate and what do they buy a developer?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

风吹短裙飘 2024-09-18 19:28:29

元类和“类过程”

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

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

因为 DoSomething 是一个 class 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 的后代。

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

// 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 的元类才能正常工作。

MetaClasses and "class procedures"

MetaClasses are all about "class procedures". Starting with a basic class:

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

Because DoSomething is a class procedure we can call it without an instance of TAlgorithm (it behaves like any other global procedure). We can do this:

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

Once we've got this setup we might create some alternative algorithms, all sharing some bits and pieces of the base algorithm. Like this:

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;

We've now got one base class and two descendent classes. The following code is perfectly valid because we declared the methods as "class" methods:

TAlgorithm.DoSomething;
TAlgorithmA.DoSomething;
TAlgorithmB.DoSomething;

Let's introduce the class of type:

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 is a data type that can be used like any other data type, it can be stored in a variable, passed as a parameter to a function. In other words we can do this:

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

CrunchSomeData(TAlgorithmA);

In this example the procedure CrunchSomeData can use any variation of the algorithm as long as it's an descendant of TAlgorithm.

Here's an example of how this behavior may be used in a real-world application: Imagine a payroll-type application, where some numbers need to be calculated according to an algorithm that's defined by Law. It's conceivable this algorithm will change in time, because the Law is some times changed. Our application needs to calculate salaries for both the current year (using the up-to-date calculator) and for other years, using older versions of the algorithm. Here's how things might look like:

// 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;

Virtual Constructors

A Delphi Constructor works just like a "class function"; If we have a metaclass, and the metaclass knows about an virtual constructor, we're able to create instances of descendant types. This is used by TCollection's IDE Editor to create new items when you hit the "Add" button. All TCollection needs to get this working is a MetaClass for a TCollectionItem.

花开浅夏 2024-09-18 19:28:29

是的,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 的任何后代的实例,无论它们位于何处宣布。

Yes a Collection would still be able to hold any descendant of TCollectionItem and to invoke methods on it. BUT, it wouldn't be able to instantiate a new instance of any descendant of a TCollectionItem. Calling TCollectionItem.Create constructs an instance of TCollectionItem, whereas

private
  FItemClass: TCollectionItemClass;
...

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

would construct an instance of whatever class of TCollectionItem descendant is held in FItemClass.

I haven't done much with generic containers, but I think that given a choice, I would opt for the generic container. But in either case I'd still have to use a metaclass if I wanted to have the list instantiate and do whatever else needs to be done when an item is added in the container and I do not know the exact class beforehand.

For example an observable TObjectList descendant (or generic container) could have something like:

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

I guess in short the advantage/benefit of metaclasses is any method/class having only knowledge of

type
  TMyThing = class(TObject)
  end;
  TMyThingClass = class of TMyThing;

is able to construct instances of any descendant of TMyThing whereever they may have been declared.

甜心 2024-09-18 19:28:29

泛型非常有用,而且我同意 TObjectList(通常)比 TCollection 更有用。但类引用对于不同的场景更有用。它们确实是不同范式的一部分。例如,当您有需要重写的虚拟方法时,类引用可能会很有用。虚拟方法重写必须具有与原始方法相同的签名,因此泛型范例在这里不适用。

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

Generics are very useful, and I agree that TObjectList<T> is (usually) more useful than TCollection. But class references are more useful for different scenarios. They're really part of a different paradigm. For example, class references can be useful when you have a virtual method that needs to be overridden. Virtual method overrides have to have the same signature as the original, so the Generics paradigm doesn't apply here.

One place where class references are used heavily is in form streaming. View a DFM as text sometime, and you'll see that every object is referred to by name and class. (And the name is optional, actually.) When the form reader reads the first line of an object definition, it gets the name of the class. It looks it up in a lookup table and retrieves a class reference, and uses that class reference to call that class's override of TComponent.Create(AOwner: TComponent) so it can instantiate the right kind of object, and then it starts applying the properties described in the DFM. This is the sort of thing that class references buy you, and it can't be done with generics.

一笔一画续写前缘 2024-09-18 19:28:29

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

然而,元类并不是我在 Delphi 圈子中所熟悉的术语。我相信我们称它们为类引用,它的名字听起来不那么“神奇”,所以很高兴您在问题中添加了这两个常见的绰号。

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

I also would use a metaclass whenever I need to be able to make a factory that can construct not only one hard-coded class, but any class that inherits from my base class.

Metaclasses are not the term I am familiar with in Delphi circles however. I believe we call them class references, which has a less "magic" sounding name, so it's great that you put both common monikers in your question.

A concrete example of a place I have seen this used well is in the JVCL JvDocking components where a "docking style" component provides metaclass information to the base docking component set, so that when a user drags their mouse and docks a client form to the docking host form, the "tab host" and "conjoin host" forms that show grabber bars (similar in appearance to the title bar of a regular undocked window) can be of a user-defined plug-in class, that provides a customized appearance and customized runtime functionality, on a plug-in basis.

雨巷深深 2024-09-18 19:28:29

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

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

In some of my applications I have a mechanism that connects classes to forms capable of editing instances of one or more of that classes. I have a central list where those pairs are stored: a class refernce and a form class reference. Thus when I have an instance of a class I can lookup the corresponding form class, create a form from it and let it edit the instance.

Of course this could also be implemented by having a class method returning the appropriate form class, but that would require the form class to be known by the class. My approach makes a more modular system. The form must be aware of the class, but not the other way round. This can be a key point when you cannot change the classes.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文