这个奇怪的小模式对于涉及泛型的某些问题很有帮助。 我们将看看它是什么,以及它的几个用例。
它是什么?
奇怪的重复通用模式(CRGP)是一个类 TFoo
从泛型类派生,使用该类 TFoo
本身作为类型参数。 使困惑? 在代码中可能更有意义:
这看起来确实很奇怪地重复出现。 我在开发通用链表时偶然发现了这种模式。 让我们看看这个模式在那里如何有用。
通用链表
此链表的想法是,您有一个基类,它提供对列表中下一项的引用,因此可以使用它来构建链表:
type TLinkedListItem = class abstract private FNext: TLinkedListItem; public property Next: TLinkedListItem read FNext; end;
然后,您可以使用类型参数定义通用链表,并限制列表中的项必须派生自 TLinkedListItem
:
type TLinkedList<T: TLinkedListItem> = class private FHead: T; public procedure Add(const AItem: T); property Head: T read FHead; end; { TLinkedList<T> } procedure TLinkedList<T>.Add(const AItem: T); begin if (AItem <> nil) then begin AItem.FNext := FHead; FHead := AItem; end; end;
例如,我们可以创建一个这样的人员链接列表:
type TPerson = class(TLinkedListItem) private FName: String; public constructor Create(const AName: String); property Name: String read FName; end; var List := TLinkedList<TPerson>.Create; List.Add(TPerson.Create('Alice')); List.Add(TPerson.Create('Bob')); List.Add(TPerson.Create('Charlie'));
(这段代码会造成各种内存泄漏,但让我们保持简单,不要受到烦人的内存管理的干扰)。
在示例中, TPerson
源自于 TLinkedListItem
以便可以将其添加到链接列表中。 我们可以(尝试)以通常的方式枚举列表:
var Person := List.Head; while (Person<> nil) do begin WriteLn(Person.Name); Person := Person.Next; //< Does not compile end;
但是,编译此代码会出现错误消息“ 不兼容的类型:'TPerson'和'TLinkedListItem' ”。 这是因为 Person
属于类型 TPerson
,但是 Person.Next
属性返回一个 TLinkedListItem
,这与赋值不兼容 TPerson
。 所以我们必须在这里进行类型转换(或者使用更具防御性的 as
操作员):
var Person := List.Head; while (Person <> nil) do begin WriteLn(Person.Name); Person := Person.Next as TPerson; end;
虽然这是可行的,但它肯定不像以前的代码那么漂亮和简单。 而且您还将类型转换的负担交给了链表类的用户。
CRGP 来救援
通过奇怪的重复通用模式,我们可以制作一个通用版本 TLinkedListItem
:
type TLinkedListItem = class abstract private FNext: TLinkedListItem; end; type TLinkedListItem<T: class> = class abstract(TLinkedListItem) private function GetNext: T; inline; public class constructor Create; public property Next: T read GetNext; end;
现在 Next
属性返回链表项的实际类型:
function TLinkedListItem<T>.GetNext: T; begin Result := T(FNext); end;
我们仍然需要在这里进行类型转换。 但不同之处在于,这种类型转换是在链表库本身内部执行的,因此您不必给此类的用户带来负担。
此技巧仅在类型参数时有效 T
是一个派生自的类 TLinkedListItem
。 不幸的是,您不能使用这样的通用约束:
type TLinkedListItem<T: TLinkedListItem> = class(TLinkedListItem)
因为稍后当您从此类派生时,这会导致编译错误。 所以我们使用一般类约束( type TLinkedListItem<T: class>
) 相反并添加一个类构造函数来检查类型 T
源自于 TLinkedListItem
:
class constructor TLinkedListItem<T>.Create; begin Assert(T.InheritsFrom(TLinkedListItem)); end;
(请记住,对于每个泛型实例化,类构造函数被调用一次,并且仅调用一次 T
)。 我在这里使用了断言,但您也可以引发异常。 如果使用链表库的开发人员尝试使用无效类型,这将提醒他们。
现在我们可以使用 CRGP 定义 person 类:
type TPerson = class(TLinkedListItem<TPerson>) ... end;
并枚举它们,无需进行类型转换:
var Person := List.Head; while (Person <> nil) do begin WriteLn(Person.Name); Person := Person.Next; end;
当我“发现”这种模式后,我想知道这是否是一种常见的模式。 事实证明它是在 C++ 世界中,它被称为“ 奇怪的重复 模板 模式” 。 虽然 C++ 模板不是 泛型 ,但它们对于此目的足够相似,因此我将 Delphi 版本称为“奇怪的重复 泛型 模式”。 我不知道这是否是一个“官方”名称,但如果不是,那就是现在😉。
因此,我很自然地想知道这种模式的一些常见 C++ 用例是否也适用于 Delphi。 虽然并非所有 C++ 用例都可以转换为 Delphi(因为模板不是泛型),但其中有一些可以。 让我们来看看它们。
通用实例计数器
这个很简单。 有时您可能想知道应用程序中某个类的实例数量。 也许用于调试目的,或者用于跟踪,或者作为负载平衡算法的输入。 你可以添加一个 FInstanceCount
类变量到您想要跟踪的每个类,然后在构造函数中递增该值并在析构函数中递减它。 但是,如果您想跟踪许多类,那么这会导致大量代码重复。 CRGP 使事情变得更加容易:
type TCounter<T> = class private class var FInstanceCount: Integer; public constructor Create; destructor Destroy; override; class property InstanceCount: Integer read FInstanceCount; end; type TFoo = class(TCounter<TFoo>) end; type TBar = class(TCounter<TBar>) end; { TCounter<T> } constructor TCounter<T>.Create; begin inherited; Inc(FInstanceCount); end; destructor TCounter<T>.Destroy; begin Dec(FInstanceCount); inherited; end;
因为 TCounter<T>
是一个泛型类,有一个单独的实例 FInstanceCount
每个实例化类型的类变量( TCounter<TFoo>
和 TCounter<TBar>
在此示例中)。 所以下面的示例代码...
TFoo.Create; TBar.Create; TFoo.Create; TFoo.Create; TBar.Create; WriteLn('Number of TFoo instances: ', TFoo.InstanceCount); WriteLn('Number of TBar instances: ', TBar.InstanceCount);
..输出以下内容:
Number of TFoo instances: 3
Number of TBar instances: 2
流畅的界面
您可能已经熟悉流畅界面的概念。 它使用方法链来调用同一对象的多个方法,而不必为每次调用指定对象。
这是通过让这些方法返回对象本身来完成的,这样您就可以对其调用另一个方法。 这种方法在 Delphi RTL
中的几个地方使用,例如在一个对象上链接多个调用。 TStringBuilder
一起:
var Builder := TStringBuilder.Create; try Builder.Append('Answer = ').Append(42); WriteLn(Builder.ToString) finally Builder.Free; end;
在这里, Append
方法返回字符串生成器本身,因此您可以立即调用它的另一个方法。 一个简单的(自定义但低效的)实现 TStringBuilder
可能看起来像这样:
type TStringBuilder = class private FData: String; public function Append(const AValue: String): TStringBuilder; property Data: String read FData; end; { TStringBuilder } function TStringBuilder.Append( const AValue: String): TStringBuilder; begin FData := FData + AValue; Result := Self; end;
如您所见, Append
方法返回 Self
启用方法链接。
现在假设我们想要创建一个子类来帮助构建 HTML 字符串。 例如,它具有打开和关闭粗体文本的方法:
type THtmlStringBuilder = class(TStringBuilder) public function BoldOn: THtmlStringBuilder; function BoldOff: THtmlStringBuilder; end; { THtmlStringBuilder } function THtmlStringBuilder.BoldOff: THtmlStringBuilder; begin Append('</b>'); Result := Self; end; function THtmlStringBuilder.BoldOn: THtmlStringBuilder; begin Append('<b>'); Result := Self; end;
同样,由于这些方法返回 Self
,它们可用于链接。 但是,以下代码将无法编译:
Builder.Append('This is ').BoldOn.Append('bold').BoldOff.Append('!');
这是因为 Append
方法返回一个 TStringBuilder
,并且该类没有名为的方法 BoldOn
。
你猜对了:CRGP 可以解决这个问题:
type TStringBuilder= class private FData: String; public class constructor Create; public function Append(const AValue: String): T; property Data: String read FData; end; type THtmlStringBuilder = class(TStringBuilder<THtmlStringBuilder>) public function BoldOn: THtmlStringBuilder; function BoldOff: THtmlStringBuilder; end; { TStringBuilder<T> } function TStringBuilder<T>.Append(const AValue: String): T; begin FData := FData + AValue; Result := T(Self); end; class constructor TStringBuilder<T>.Create; begin Assert(T.InheritsFrom(TStringBuilder<T>)); end;
我们在这里使用与之前对链表所做的相同类型的类型转换和断言。 现在我们可以毫无问题地将这些方法链接在一起:
var Builder := THtmlStringBuilder.Create; Builder.Append('This is ').BoldOn.Append('bold').BoldOff.Append('!'); WriteLn(Builder.Data);
哪个输出:
1 | This is <b>bold</b>! |
源代码
与往常一样,本文中的示例可以在 GitHub 上的 JustAddCode 目录中 存储库中的CuriouslyRecurringGenericPattern 找到。
其他用例?
可以说,这种模式在 C++ 世界中最常见的用例是实现某种形式的静态多态性。 这种形式的多态性在编译时而不是运行时解析方法调用。 这避免了虚拟方法的开销,因此可以稍微提高性能。 但由于这需要 C++ 特定的模板功能,因此无法在 Delphi 中复制(尽管我希望在这里被证明是错误的)。
但也许您可以想到此模式的其他有趣用例。 我很想知道你想出了什么!
还没有评论,来说两句吧...