类助手的好用途是什么?

37

Delphi(以及可能许多其他语言)具有类助手。它们提供了一种向现有类添加额外方法的方法,而不必制作子类。

那么,类助手的好处是什么?

10个回答

34

我正在使用它们:

  • To insert enumerators into VCL classes that don't implement them.
  • To enhance VCL classes.
  • To add methods to the TStrings class so I can use the same methods in my derived lists and in TStringList.

    TGpStringListHelper = class helper for TStringList
    public
      function  Last: string;
      function  Contains(const s: string): boolean;
      function  FetchObject(const s: string): TObject;
      procedure Sort;
      procedure Remove(const s: string);
    end; { TGpStringListHelper }
    
  • To simplify access to record fields and remove casting.


14

一开始我对类助手有些怀疑。但是后来我读了一篇有趣的博客文章,现在我相信它们确实很有用。

例如,如果您想为现有实例类添加额外的功能,但由于某种原因无法更改现有源代码。您可以创建一个类助手来添加此功能。

示例:

type
  TStringsHelper = class helper for TStrings
  public
    function IsEmpty: Boolean;
  end;

function TStringsHelper.IsEmpty: Boolean;
begin
  Result := Count = 0;
end;

每次我们现在使用 (TStrings 的子类) 的一个实例,并且 TStringsHelper 在范围内。我们可以访问方法 IsEmpty。
例如:
procedure TForm1.Button1Click(Sender: TObject);
begin
  if Memo1.Lines.IsEmpty then
    Button1.Caption := 'Empty'
  else
    Button1.Caption := 'Filled';
end;

注意:

  • 类助手可以存储在单独的单元中,因此您可以添加自己的巧妙的类助手。请确保给这些单元一个易于记忆的名称,例如ClassesHelpers用于类的助手。
  • 还有记录助手。
  • 如果范围内有多个类助手,则可能会出现一些问题,只能使用一个助手。

4
你读过这条评论吗:“从在你自己的应用程序中使用类助手的角度来看,最大的问题是同一类只能有一个类助手处于作用域内。” … “也就是说,如果有两个助手在作用域内,只有一个会被编译器识别。你不会收到任何关于任何其他可能被隐藏的助手的警告或提示。” - mjn
CLASS助手可以继承其他CLASS助手,因此您可以同时拥有多个活动的CLASS助手(但它们必须相互了解,因此您只能拥有连续的“字符串”活动的CLASS助手,即如果您有Helper1,并且Helper2继承自Helper1,Helper3继承自Helper2,则不能仅使用Helper3(没有一些条件编译指令),但必须在范围内拥有所有三个助手-您可以仅拥有Helper1,仅拥有Helper1 + Helper2或同时激活所有三个助手-不能仅有Helper1 + Helper3)。 - HeartWare
类助手可以相互继承,这将使整个类助手的优点失效。 - Ian Boyd
已修改博客链接,使用 archive.org 页面。 - Michael Riley - AKA Gunny

6
这很像C#3(和VB9)中的扩展方法。我见过的最好用途是对>(和>)进行扩展,这使得LINQ可以针对任意序列工作:
var query = someOriginalSequence.Where(person => person.Age > 18)
                                .OrderBy(person => person.Name)
                                .Select(person => person.Job);

(当然,这只是示例)。所有这些都是可行的,因为扩展方法允许您有效地将对静态方法的调用链接在一起,这些方法接受与它们返回相同的类型。


5
顺便提一下,Delphi自2000年以来就拥有类助手,并且我们持有这个想法的专利权。Nick Hodges Delphi产品经理 Embarcadero Technologies - Nick Hodges
@Nick:你说的是哪个版本的Delphi,从2000年开始就有类助手?我只记得在Delphi 2007中使用它们来进行非破坏性发布时才第一次听说。这是在你声称Delphi拥有它们的6年后。 - dummzeuch
1
Delphi 8(.NET的第一个版本)中新增了类助手。 - Cary Jensen
15
你是否还申请了一项专利,防止多个帮手同时激活? - David Heffernan

4
我不建议使用它们,因为我读到了这个评论:
“从在自己的应用程序中使用类助手的角度来看,最大的问题是:对于给定的类,只有一个类助手可以在任何时候生效。” ... “也就是说,如果你有两个助手在使用,编译器只会识别其中一个。你不会收到任何有关其他可能被隐藏的助手的警告或提示。”
参考链接:http://davidglassborow.blogspot.com/2006/05/class-helpers-good-or-bad.html

3
我已经阅读了那条评论。但这并不意味着不能使用它们。就像使用其他工具一样,只要小心使用即可。 - Toon Krijthe
1
@GamecatisToonKrijthe,这意味着开发人员现在必须扫描所有第三方库的源代码(不仅一次,而是每次更新)来检测“冲突助手”,甚至没有任何提示... - mjn
1
这是正确的答案。Helpers 不是 Delphi 版本的 C# 扩展方法。Helpers 不是一种通用的编程解决方案,不应该在任何新代码中使用。如果您尝试在类上声明一个已经存在的 helper(比如 TGuidHelper),那么您将会破坏现有的代码。如果 Delphi 支持扩展方法,那将是一个非常有用的语言特性,我会非常喜欢它。但 helpers 并不是这个功能。 - Ian Boyd

4

它们对于插件非常有用。例如,假设您的项目定义了某个数据结构,并以某种方式保存到磁盘中。但是,其他一些程序执行了非常类似的操作,但数据文件不同。但是,您不想在EXE中膨胀一堆导入代码,因为很多用户不需要使用此功能。您可以使用插件框架并将导入器放入一个插件中,该插件将像这样工作:

type
   TCompetitionToMyClass = class helper for TMyClass
   public
      constructor Convert(base: TCompetition);
   end;

然后定义转换器。有一个需要注意的地方:类“helper”不是类“friend”。只有当可以通过其公共方法和属性完全设置新的TMyClass对象时,此技术才能正常工作。但如果可以,它会非常有效。


3
我记得第一次接触你所谓的“类助手”是在学习Objective C时。 Cocoa(苹果的Objective C框架)使用的是所谓的“分类”。
分类允许您通过添加自己的方法来扩展现有类,而无需子类化。实际上,Cocoa鼓励您尽可能避免子类化。通常情况下,子类化是有意义的,但是使用分类可以避免。
Cocoa中使用分类的一个很好的例子是所谓的“键值代码(KVC)”和“键值观察(KVO)”。
该系统是使用两个类别(NSKeyValueCoding和NSKeyValueObserving)实现的。这些类别定义并实现了可以添加到任何想要的类的方法。例如,Cocoa使用这些类别将方法添加到NSArray,从而添加了对KVC / KVO的“符合性”。
- (id)valueForKey:(NSString *)key

NSArray类没有声明也没有实现这个方法。然而,通过使用分类,您可以在任何NSArray类上调用该方法。您不需要子类化NSArray来获得KVC/KVO一致性。

NSArray *myArray = [NSArray array]; // Make a new empty array
id myValue = [myArray valueForKey:@"name"]; // Call a method defined in the category

使用这种技术可以方便地为自己的类添加KVC/KVO支持。Java接口允许您添加方法声明,但是分类允许您还可以将实际实现添加到现有类中。


3
作为GameCat所展示的,TStrings是一个避免输入大量代码的好选择:
type
  TMyObject = class
  public
    procedure DoSomething;
  end;

  TMyObjectStringsHelper = class helper for TStrings
  private
    function GetMyObject(const Name: string): TMyObject;
    procedure SetMyObject(const Name: string; const Value: TMyObject);
  public
    property MyObject[const Name: string]: TMyObject read GetMyObject write SetMyObject; default;
  end;

function TMyObjectStringsHelper.GetMyObject(const Name: string): TMyObject;
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    result := nil
  else
    result := Objects[idx] as TMyObject;
end;

procedure TMyObjectStringsHelper.SetMyObject(const Name: string; const Value:
    TMyObject);
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    AddObject(Name, Value)
  else
    Objects[idx] := Value;
end;

var
  lst: TStrings;
begin
  ...
  lst['MyName'] := TMyObject.Create; 
  ...
  lst['MyName'].DoSomething;
  ...
end;

你是否曾经需要访问注册表中的多行字符串?


type
  TRegistryHelper = class helper for TRegistry
  public
    function ReadStrings(const ValueName: string): TStringDynArray;
  end;

function TRegistryHelper.ReadStrings(const ValueName: string): TStringDynArray;
var
  DataType: DWord;
  DataSize: DWord;
  Buf: PChar;
  P: PChar;
  Len: Integer;
  I: Integer;
begin
  result := nil;
  if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, nil, @DataSize) = ERROR_SUCCESS then begin
    if DataType = REG_MULTI_SZ then begin
      GetMem(Buf, DataSize + 2);
      try
        if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, PByte(Buf), @DataSize) = ERROR_SUCCESS then begin
          for I := 0 to 1 do begin
            if Buf[DataSize - 2] <> #0 then begin
              Buf[DataSize] := #0;
              Inc(DataSize);
            end;
          end;

          Len := 0;
          for I := 0 to DataSize - 1 do
            if Buf[I] = #0 then
              Inc(Len);
          Dec(Len);
          if Len > 0 then begin
            SetLength(result, Len);
            P := Buf;
            for I := 0 to Len - 1 do begin
              result[I] := StrPas(P);
              Inc(P, Length(P) + 1);
            end;
          end;
        end;
      finally
        FreeMem(Buf, DataSize);
      end;
    end;
  end;
end;

4
哇,覆盖了“默认”!太神奇了。可能会导致奇怪的错误源 :) - gabr
同意,这有点偏离主题。但是我大多数情况下在紧密的范围内使用类助手,所以这并不是一个问题 - 直到现在... - Uwe Raabe

0
如果 Delphi 支持扩展方法,我想要使用的一个例子是:
TGuidHelper = class
public
   class function IsEmpty(this Value: TGUID): Boolean;
end;

class function TGuidHelper(this Value: TGUID): Boolean;
begin
   Result := (Value = TGuid.Empty);
end;

所以我可以调用 if customerGuid.IsEmpty then ...

另一个很好的例子是使用 IDataRecord 范例(我喜欢)从 XML 文档(或 JSON,如果你喜欢)中读取值:

orderGuid := xmlDocument.GetGuid('/Order/OrderID');

这比以下的方式要好得多:

var
   node: IXMLDOMNode;

   node := xmlDocument.selectSingleNode('/Order/OrderID');
   if Assigned(node) then
      orderID := StrToGuid(node.Text) //throw convert error on empty or invalid
   else
      orderID := TGuid.Empty; // "DBNull" becomes the null guid

0

我见过它们被用于使给定“类型”的所有类方法保持一致:将打开/关闭和显示/隐藏添加到所有类中,而不仅仅是活动和可见属性。


0

其他编程语言都有经过精心设计的类帮助器。

Delphi引入了类帮助器,仅仅是为了帮助Borland工程师解决Delphi和Delphi.net之间的兼容性问题。

它们从未被设计用于“用户”代码,并且自引入以来一直没有得到改进。如果开发框架(在框架内部私下使用,就像最初的.NET兼容性解决方案一样),它们可能会有所帮助;但是,将Delphi类帮助器与其他语言中的类帮助器等同起来,或者试图从其他语言中的示例中寻找Delphi中的用例,这是非常错误的

直到今天,当前的Delphi文档对类和记录帮助器的描述如下:

它们不应被视为开发新代码时使用的设计工具

参考:https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Class_and_Record_Helpers_(Delphi)

因此,在Delphi中,“类帮助器的良好用途是什么”的答案非常简单: 只有一种安全的用法:只在实现和使用帮助器的单一代码库中的特定上下文扩展的效用和兴趣(详细示例:https://www.deltics.co.nz/blog/posts/683)。
该示例是一个restful API框架,其中“客户端助手”扩展提供了针对仅适用于客户端代码的感兴趣类别的扩展,明确地从客户端特定单元导入,而不是在具有服务器和客户端上下文的原始类中加载客户端关注点(过载)。
除此之外:根本不要使用它们(无论是实现自己的还是消耗他人提供的),除非你准备好应对后果
主要问题是:任何时候只能有一个帮助器处于范围内 次要问题是:没有方法限定引用的帮助器 由于主要问题的存在:
  1. uses子句中添加一个单元或者仅仅改变单元的顺序,可能会意外地“隐藏”了你代码所需的帮助程序(你甚至可能不知道该帮助程序来自哪里)。

  2. 向已经存在于uses列表中的单元添加一个帮助程序,可能会隐藏之前从另一个单元“导入”和使用的某些其他帮助程序。

由于这个次要问题,如果你无法重新排列uses列表以使一个所需的帮助程序“可见”,或者你需要两个不相关的帮助程序(彼此不知晓因此无法相互“扩展”),那么就没有办法使用它!

值得强调的是,Delphi类帮助程序破坏其他人的代码的能力是一种几乎独一无二的糟糕特性。许多语言的许多语言特性可以被滥用以破坏自己的代码;但并不多能让你去破坏别人的代码!

在这里的各种文章中有更多详细信息:https://www.deltics.co.nz/blog/?s=class+helpers

特别是这个链接:https://www.deltics.co.nz/blog/posts/273/

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