在 Delphi XE 中始终使用通用容器?

11

当需要一个项目和该项目的一种强类型列表时,通用容器可以节省时间。它可以避免重复编码,例如创建一个带有TList内部变量、类型化的Add/Delete类型方法以及其他优点(例如由通用容器类提供的所有新功能)。

然而,推荐在以后的代码中始终使用通用容器来创建强类型列表吗? 这样做的具体缺点是什么? (如果不担心代码向后兼容性。)昨天我正在编写一个服务器应用程序,我有一个项目列表,最初打算用通用列表替代,但出于习惯还是保留了它。(我们是否应该打破这个习惯,通过始终使用通用容器来开始一个新习惯?)


1
从收到的答案来看,这就是为什么StackOverflow做得如此出色的例子! - Darian Miller
使用泛型也会减慢构建速度,如此处所述:https://dev59.com/QmAf5IYBdhLWcg3wbCN3#24672687 - Darian Miller
7个回答

12

在 Delphi XE 中,没有理由不使用泛型容器。

与旧的强制类型转换方法相比,使用泛型容器会给您带来:

  • 更清晰、类型安全、错误更少的代码,
  • 枚举器,用于 for-in 循环,
  • 同样的更好的性能特性。

5
性能特征不同,性能更好,因为你不再需要修复返回类型的虚构getter。枚举器对我来说是"致命的"。简言之,现在我有许多类看起来像这样 TSomethingList = class(TObjectList<TSomething>):这些类曾经作为手写的TObjectList后代存在,但现在已转变为泛型容器,以便我可以使用for in循环。 - Cosmin Prund
1
@Cosmin 我不确定平均性能提升是否真实。Getter 并不是很耗时,而且最好使用本地变量来访问循环中的多个属性到索引项,例如。而泛型生成的所有重复代码(请参见 Mason 的答案)可能会使程序变慢:CPU 缓存将填充所有这些不必要的代码。 - Arnaud Bouchez
1
@A.Bouchez 有趣的例子,但使用泛型会更简单和清晰。而且你的动态数组使用了二分查找(如果我调查正确的话),比字典的搜索慢。 - Linas
1
@darian,你所接受的答案中的陷阱实际上是答案作者知识的局限性,而不是真正的问题。很抱歉你被这个论点所迷惑。 - David Heffernan
请注意,这不仅仅是Deltics的例子,Mason在他的评论中也提供了一篇文章链接。我正在尝试在新开发中使用泛型,但在原帖中,我提到只是编写了具有标准tlist和类型安全方法的代码,并且我希望简单地用一个通用列表替换它...我确实使用了一些单位来完成这个过程,但随后开始想知道我可能会遇到什么麻烦。今后,我将停止使用我的方便快捷的IDE工具,该工具可以在几个按键中生成类型安全的TList代码。 - Darian Miller
显示剩余7条评论

10

这是由Deltic的回答引发的,我想提供一个反例来证明您可以使用通用类进行动物饲养程序。(即:多态泛型列表)

首先是一些背景知识:您可以使用通用基类列表类来喂养通用动物的原因是因为通常会有这种继承:

TBaseList = class
  // Some code to actually make this a list
end

TSpecificList = class(TBaseList)
  // Code that reintroduces the Add and GetItem routines to turn TSpecificList
  // into a type-safe list of a different type, compatible with the TBaseList
end

由于通常会有以下情况,所以这无法与泛型一起使用:

TDogList = TList<TDog>
end

TCatList = TList<TCat>
end

...而且两个列表之间的唯一“公共祖先”是TObject - 没有任何帮助。但是我们可以定义一个新的通用列表类型,它接受两个类参数:一个TAnimal和一个TSpecificAnimal,生成与TAnimal 的通用列表兼容的TSpecificAnimal类型安全列表。以下是基本类型定义:

TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
private
  function GetItem(i: Integer): T2;
public
  procedure Add(A:T2);
  property Item[i:Integer]:T2 read GetItem;default;
end;

使用这个,我们可以做到:

TAnimal = class; 
TDog = class(TAnimal); 
TCat = class(TAnimal);

TDogList = TCompatibleList<TAnimal, TDog>;
TCatList = TCompatibleList<TAnimal, TCat>;

这样,TDogList和TCatList实际上都继承自TObjectList<TAnimal>,因此我们现在拥有了一个多态的通用列表!

下面是一个完整的控制台应用程序,展示了这个概念的实现。而且这个类现在将被放入我的ClassLibrary以供未来重用!

program Project23;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

type

  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TCat = class(TAnimal)
  end;

  TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
  private
    function GetItem(i: Integer): T2;
  public
    procedure Add(A:T2);
    property Item[i:Integer]:T2 read GetItem;default;
  end;

{ TX<T1, T2> }

procedure TCompatibleList<T1, T2>.Add(A: T2);
begin
  inherited Add(T1(TObject(A)));
end;

function TCompatibleList<T1, T2>.GetItem(i: Integer): T2;
begin
  Result := T2(TObject(inherited Items[i]));
end;

procedure FeedTheAnimals(L: TObjectList<TAnimal>);
var A: TAnimal;
begin
  for A in L do
    Writeln('Feeding a ' + A.ClassName);
end;

var Dogs: TCompatibleList<TAnimal, TDog>;
    Cats: TCompatibleList<TAnimal, TCat>;
    Mixed: TObjectList<TAnimal>;

begin
  try
    // Feed some dogs
    Dogs := TCompatibleList<TAnimal, TDog>.Create;
    try
      Dogs.Add(TDog.Create);
      FeedTheAnimals(Dogs);
    finally Dogs.Free;
    end;
    // Feed some cats
    Cats := TCompatibleList<TAnimal, TCat>.Create;
    try
      Cats.Add(TCat.Create);
      FeedTheAnimals(Cats);
    finally Cats.Free;
    end;
    // Feed a mixed lot
    Mixed := TObjectList<TAnimal>.Create;
    try
      Mixed.Add(TDog.Create);
      Mixed.Add(TCat.Create);
      FeedTheAnimals(Mixed);
    finally Mixed.Free;
    end;
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

我并没有说使用泛型是不可能的,而是它们会成为一种阻碍。恭喜你和David,你们非常有效地证明了我的观点,谢谢! ;) -1是因为它根本没有回答问题。这是对另一个答案的回应 - 它应该是一个评论,并包含指向博客或其他扩展媒介的链接来阐述你的观察/解释。 - Deltics
1
@Deltics,我理解你的观点并尊重你的决定进行了反对投票。尽管如此,我还是使用通用多态列表在79行代码中实现了你整个动物喂养例程,甚至没有试图让它变得更短。请注意,这是一个完整的控制台应用程序,包括测试。如果泛型使我能够做到这一点,那么它们怎么可能成为障碍呢?而且,我决定使用另一个“SO Answer”来反驳你的例子,这符合SO的惯例,因为你不能在评论中放置代码。请不要因为我决定不写博客而对我有所保留! - Cosmin Prund
1
@Deltics - 这是对原问题的详细说明。在我看来完全没问题。 - Leonardo Herrera
点赞,因为这个详细的示例代码对我的工作非常有用。我感谢花费的时间,并想抵消不必要的踩! - LaKraven
这正是我正在寻找的。谢谢。 - Nix

4

我们是否应该打破惯例,始终使用泛型? 是的


1
把下面关于编程的内容从英语翻译成中文。只返回翻译后的文本:+1表示明确和简洁地回答了所提出的问题。 :) - jachguate
将以下与程序相关的内容从英语翻译成中文。仅返回已翻译的文本:-1为未回答有关缺点的问题方面。 ;) - Deltics
@Deltics- 从某种程度上来说,Robert展示了其中的一个缺点。 - Gabriel

3
在大多数情况下,通用容器是一个好东西。但是,编译器会生成大量重复代码,不幸的是链接器还不知道如何删除它,因此过度使用泛型可能会导致可执行文件膨胀。但除此之外,它们非常好。

大多数常见的使用模式不会导致太多的膨胀。这与新的RTTI产生的情况完全不同。 - David Heffernan
@Mason Wheeler - 它只是一个臃肿的 EXE 文件,还是会导致速度下降?如果只是 EXE 大小的问题,那就不是大问题。 - Gabriel

2

在Cosmin的回答精神中,本质上是对Deltic答案的回应,下面是如何修复Deltic代码的方法:

type
  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TAnimalList<T:TAnimal> = class(TList<T>)
    procedure Feed;
  end;
  TDogList = TAnimalList<TDog>;

现在你可以写成这样:
var
  Dogs: TDogList;
...
  Dogs.Feed;

@Cosmin 它解决了Deltics的问题。我看过你的多态列表。我并不是试图复制那个。 - David Heffernan
@Cosmin,给我一个你需要用到解决方案的实际例子。在我的代码库中我没有这样的例子。我不是说不存在重要的情况。我的观点是,为了避免从未影响过你的缺点而剥夺自己使用通用容器的好处感觉很愚蠢! - David Heffernan
我不认为Deltics误导了Darian,我认为Darian正在寻找保持他“旧方式”的理由。但我确实认为Deltics是错的:正如证明的那样,您可以拥有通用和多态容器。 - Cosmin Prund
缺点主要是泛型的新颖性和可能会像Deltics帖子中指出的那样遇到时间消耗较大的例子。(时间消耗=分散开发流程。)如果将一个快速列表添加到服务器应用程序中,其中最有可能需要的只是Add(x)Count()和可能是自定义Find()例程,那么我认为最好(对我来说)坚持使用旧的标准,直到我的泛型代码库扩展并且它们变得不再是潜在问题。这都是迭代学习周期的一部分,非常感谢! - Darian Miller
2
我个人在需要具有共同祖先的简单对象列表时,将使用Cosmin的解决方案。通常我都这样做。我想忘记使用TStringLists作为对象容器的旧时代! - Leonardo Herrera
显示剩余11条评论

1
如果您需要多态列表,那么泛型反而会成为阻碍而不是帮助。例如,以下代码甚至无法编译,因为您不能在需要TAnimalList的地方使用TDogList:
  uses
    Generics.Collections;

  type
    TAnimal = class
    end;

    TDog = class(TAnimal)
    end;

    TAnimalList = TList<TAnimal>;
    TDogList = TList<TDog>;


  procedure FeedTheAnimals(const aList: TAnimalList);
  begin
    // Blah blah blah
  end;


  var
    dogs: TDogList;
  begin
    dogs := TDogList.Create;
    try
      FeedTheAnimals(dogs);

    finally
      dogs.Free;
    end;
  end;

这其中的原因非常清晰易懂,但同样也很反直觉。

我个人认为,使用泛型可以节省几秒钟或几分钟(如果你打字速度较慢),而不是使用更具体和适合你需求的类型安全容器,但你可能会花费更多时间来解决泛型的问题和限制,这比一开始使用它们节省的时间更多(而且根据定义,如果你到现在还没有使用泛型容器,那么你不知道这些问题/限制可能是什么,直到你遇到它们为止)。

如果我需要一个TAnimalList,那么我需要或者可以从该列表类中受益,因为我想要继承TDogList中的其他TAnimal特定方法,这反过来可能会引入与其TDog项目相关的其他特定成员。

(当然,动物和狗仅用于说明目的。我目前并不从事兽医代码工作-哈哈)

问题是,你并不总是在开始时就知道这一点。

防御性编程原则建议(对我而言,可能因人而异)为了节省一点时间而将自己陷入困境,最终可能会付出很大的代价。即使没有这种情况发生,不采取前期节约的额外“成本”本身也微不足道。

此外,如果您愿意慷慨一些,您的代码可以与使用旧版Delphi的用户共享。

:)


@Darian:如果你对这个例子中“清晰但反直觉”的原则感到困惑,我之前写过一篇文章详细讲解了它。http://tech.turbu-rpg.com/149/generics-and-the-covariance-problem - Mason Wheeler
@Deltics,你的例子是Generics.Collections中默认TList<T>的限制,而不是泛型本身的固有限制。请看我的答案,其中提供了一个使用单个例程喂养不同种类的泛型动物的示例! - Cosmin Prund
@David,你如何通过“适当使用约束”来解决这个问题?我认为仅仅使用约束是不够的……你仍然会遇到不兼容的继承问题(TList<T> 本质上直接从 TObject 继承)。 - Cosmin Prund
@David - 我还没学会如何使用新的泛型猫毛处理工具,让我能够剥下我需要的猫毛。我忙得不可开交,正在使用非常有效且易于学习和教授的猫毛剥离工具高效地、有效地和生产地剥下猫毛,无法浪费时间去学习冗余的知识或者花时间教授其他在同一只猫皮加工厂工作的人那些冗余的知识。 - Deltics
我认为我们不想学习并不是因为不愿意,而是一种成本效益的考虑。使用泛型可能会带来潜在的成本,即使是在日常使用的简单列表上也需要进行切换,这似乎超过了立即采用泛型所带来的好处。从长远来看,我同意这样做是有道理的。 - Darian Miller
显示剩余2条评论

1

你提到了向后兼容性...这是我最大的关注点,如果(像我一样)你正在编写应该与大多数常见版本的Delphi编译更好的库。

即使你只是在一个封闭项目中使用XE,你可能也会制作一些自己的自定义库,即使你从未发布过代码。我们都有这样的喜爱单元可用,只需在每个项目中重新发明轮子。

在将来的任务中,您可能需要维护一些旧代码,无法升级到新版本的Delphi(没有资金进行100万行代码迁移和审查)。在这种情况下,您可能会错过您的XE-only库,带有闪亮的基于泛型的列表...

但对于一个100%“私人”应用程序,如果您确定永远不需要维护旧的Delphi代码,我认为没有理由不使用泛型。我的唯一担忧是重复的代码问题(如Mason所引用的):CPU缓存可能会填充不必要的代码,因此执行速度可能会受到影响。但在实际应用程序中,我认为您不会看到任何区别。

注意:我刚刚为我的TDynArray包装器添加了一些新功能。我试图模仿EMB docwiki的示例代码。因此,您可以在旧版本的Delphi中拥有类似泛型的功能...当然,泛型更适合处理类,但对于某些数组和记录,它真的很棒!


+1 我最初说不用担心向后兼容性,但你当然是对的...我必须支持 Delphi 5 应用程序、Delphi 7 应用程序、Delphi 2006 应用程序、Delphi 2007 应用程序和 Delphi XE 应用程序...虽然我正在努力将所有东西转换为 XE。 - Darian Miller
A.Bouchez,你是一个“特殊情况”,因为你编写库供他人使用,我完全理解你想要使用向后兼容的语言结构的愿望。但这并不适用于我们其他人:当我们打开 Delphi (N+1) 盒子时,我们知道我们将使用 Delphi (N) 中不可用的功能 - 这就是为什么我们购买了 Delphi (N+1) 的原因。 - Cosmin Prund
@Cosmin - 编写库是向后兼容性的“最坏情况”,你是对的。但是有些公司维护着多个软件,使用不同版本的Delphi。由于技术原因(第三方组件)或商业预算(迁移成本,特别是到Delphi Unicode版本),升级到新版Delphi并非总是可行的。所以,就像Darian所说,同一家公司和计算机中可能仍然存在多个Delphi版本。 - Arnaud Bouchez

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