本文作者:icy

Delphi-奇怪地重复出现的通用模式

icy 2023-07-11 1300 抢沙发
Delphi-奇怪地重复出现的通用模式 摘要: 这个奇怪的小模式对于涉及泛型的某些问题很有帮助。  我们将看看它是什么,以及它的几个用例。 它是什么?奇怪的重复通用模式(CRGP)是一个类 TFoo从泛型类派...

这个奇怪的小模式对于涉及泛型的某些问题很有帮助。  我们将看看它是什么,以及它的几个用例。 

它是什么?

奇怪的重复通用模式(CRGP)是一个类 TFoo从泛型类派生,使用该类 TFoo本身作为类型参数。  使困惑?  在代码中可能更有意义: 

crgpdefinition-2.webp

这看起来确实很奇怪地重复出现。  我在开发通用链表时偶然发现了这种模式。  让我们看看这个模式在那里如何有用。 

通用链表 

此链表的想法是,您有一个基类,它提供对列表中下一项的引用,因此可以使用它来构建链表: 

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 中复制(尽管我希望在这里被证明是错误的)。

但也许您可以想到此模式的其他有趣用例。  我很想知道你想出了什么! 


觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏

分享

发表评论

快捷回复:

评论列表 (暂无评论,1300人围观)参与讨论

还没有评论,来说两句吧...