在Delphi中使用接口的优缺点是什么?

28

我已经使用Delphi类有一段时间了,但从未真正开始使用接口。我已经读了一些关于它们的内容,但想学习更多。

我想听听您在使用Delphi中接口时遇到的优缺点,包括编码、性能、可维护性、代码清晰度、层次分离以及您可以想到的任何方面。

9个回答

27

目前我能想到的有:

优点:

  • 清晰地分离了接口和实现
  • 减少单元间的依赖关系
  • 多重继承
  • 引用计数(如果需要,可以禁用)

缺点:

  • 类和接口引用不能混合使用(至少在引用计数中是这样)
  • 所有属性都需要Getter和Setter函数
  • 引用计数无法处理循环引用
  • 调试困难(感谢gabr和Warren指出这一点)

3
好的清单。我只想补充一个缺点 - 调试接口问题(引用没有被减少/过早减少 - 或者翻译成中文就是内存没有被释放/内存过早被释放)可能会变得非常混乱。 - gabr
3
也许可以在缺点中加入使用接口时会有一定的性能损失(虽然很小)。但总的来说,Delphi本地接口(非COM)的实现非常快速和优化(与虚拟方法一样快,再加上一些引用计数)。 - Arnaud Bouchez
5
调试接口对我来说相当困难。在大多数 Delphi 版本中(包括 XE),通过跟踪接口调用步骤并不容易。当接口被广泛使用时,检查和理解应用程序变得更加困难。在许多强烈依赖接口的应用程序中,修复内存泄漏几乎成为一件不可能的任务。 - Warren P
@A.Bouchez:我认为性能惩罚如你所说是如此微小,将其列在这里可能会产生误导。我认为在99%的情况下,可以忽略此项内容。 - jpfollenius
2
@Dangph 不要从 TInterfacedObject 继承,而是自己实现 IInterface 中的方法,并始终将引用计数返回 -1。如果需要更多细节,请提出单独的问题。 - jpfollenius
显示剩余4条评论

12

除了上面的回答之外,还有以下几个优点:

  1. 使用接口来表示行为,每种行为的实现都将实现该接口。
  2. API发布:在发布API时使用接口非常好。你可以发布一个接口,而不必公开实际的实现。因此,你可以自由地进行内部结构更改,而不会对客户造成任何问题。

1
组件供应商怎么能没有接口...但我还没有看到任何一个通过接口公开API的。 - Eugene Mayevski 'Callback
2
@Eugene:你提到了组件/库,但Bharat没有。对于应用程序的API来说,接口非常适合。对答案进行负评似乎完全不公平。 - mghie
4
COM已经死了吗?实际上,我不仅仅是在谈论COM/ActiveX。 - Bharat
1
@Eugene 大多数 Delphi 组件供应商并不公开真正的 API。他们在每个新版本中都会更改接口。也就是说,接口本身并不意味着一个稳定的接口。 - David Heffernan
1
@David:“悲伤却是真实的”(出自Metallica)。我认为普通用户甚至没有注意到这只是 .NET 的另一个版本,因为它已经随着服务包安装。作为开发人员,当你查看系统时,会发现很多不必要的东西。但用户很少这样做或者关心这个。 - Eugene Mayevski 'Callback
显示剩余11条评论

12

我想说的是,没有引用计数的接口非常高在我的Delphi愿望清单上!

--> 接口的真正用途是声明一个接口,而不是具备引用计数的能力!


5
在有人告诉我在_AddRef / _Release中返回-1之前,不,那不是我的意思。 我的意思是一种接口,Delphi甚至不调用这些方法! - Daniel
@Daniel,@Smasher在他的回答中提到引用计数可以被禁用(我猜是通过在_AddRef / _Release中返回-1)。为什么这不足够呢?还有其他需要考虑的事情吗?谢谢。 - Guillem Vicens
@Daniel:调用这些方法有什么不好的?你真的担心一个函数调用的性能问题吗(而且这个函数可能已经被编译器内联了)? - jpfollenius
重要的是方法被调用的事实(在你背后),如果你想自己管理接口对象的生命周期(而不使用引用计数),这会给你带来麻烦。 - Uli Gerhardt
@Smasher:前段时间我尝试使用非引用计数接口时,总是遇到问题 - 如果我没记错的话 - 在接口变量上调用_IntfClear,这些变量指向已释放的对象。然而,我现在无法创建一个小的示例来展示这个问题,所以目前我也看不到任何问题。 :-) - Uli Gerhardt
2
这些方法调用有什么问题其实非常简单。原因是进行了接口清理:创建一个对象,将其作为接口引用传递给一个方法,然后将其删除,离开当前方法-> 访问冲突,因为已销毁的对象上调用了_Release()。 - Daniel

9

使用接口也存在一些微妙的缺点,可能在使用时人们并没有考虑到:

  1. 调试变得更加困难。我曾经见过很多在调试器中调用接口方法时出现的奇怪问题。

  2. Delphi中的接口带有IUnknown语义,无论你喜欢与否,你都必须处理好引用计数,并且如果处理不当,会导致内存泄漏。当你想要避免引用计数时,唯一的选择是重写addref/decref,但这也不是没有问题的。我发现那些使用接口较多的代码库中,最难找到访问冲突和内存泄漏,我认为这是因为很难将引用计数语义和默认的Delphi语义(所有者释放对象,其他人不释放,大多数对象的生命周期与其父对象相同)结合起来。

  3. 糟糕的接口实现可能会导致一些令人讨厌的代码气味。例如,在定义类的初始具体实现的同一单元中定义接口,增加了接口的负担,却没有真正提供用户和实现者之间的正确分离。我知道这不是接口本身的问题,而更多地是对那些编写基于接口的代码的人的挑剔。请将您的接口声明放在只包含这些接口声明的单元中,并避免将接口声明与实现类混合在同一个单元中,导致单元之间依赖关系混乱。


添加一些缺点。我真的不喜欢其中之一的缺点是,您无法从接口引用向对象实例引用进行强制转换(这是固有的,因为接口可能由像COM这样不基于Delphi TObject的东西实现)。 - Jeroen Wiert Pluimers
我相信你可以编写一个“IS检查”,如果传入的接口是TMyImplementation,那么你可以通过as转换来恢复TMyImplementation。 - Warren P
你甚至可以在Delphi 2010中使用硬转换来实现这一点:http://wiert.me/2011/03/23/delphi-xe-introduced-safeintfasclass-to-cast-back-interface-to-class-via-oop-delphi-proxy-design-pattern-interface-problem-stack-overflow/ as转换也适用于Delphi 2010及以上版本:http://blogs.embarcadero.com/abauer/2009/08/21/38893 - Jeroen Wiert Pluimers
我想,既然它受到is-check的保护,as-check只是重复了is-check,因此根据我所钟爱的DRY原则(不要重复自己),采用is,然后硬制模式转换是理想的。 - Warren P

5

当我需要不同祖先的对象提供共同服务时,我通常使用接口。我自己经历中最好的例子是一个名为IClipboard的接口:

IClipboard = interface
  function CopyAvailable: Boolean;
  function PasteAvailable(const Value: string): Boolean;
  function CutAvailable: Boolean;
  function SelectAllAvailable: Boolean;
  procedure Copy;
  procedure Paste(const Value: string);
  procedure Cut;
  procedure SelectAll;
end;

我有一堆从标准VCL控件派生而来的自定义控件。它们每个都实现了这个接口。当剪贴板操作到达我的某个表单时,它会查看活动控件是否支持此接口,如果支持,则调度相应的方法。
对于非常简单的接口,您可以使用 of object 事件处理程序来完成,但一旦变得足够复杂,使用接口效果很好。事实上,我认为这是一个很好的比喻。在需要单个 of object 事件无法适用功能的情况下,请使用接口。

这是一种很棒的技术,我只希望VCL的TAction框架是基于接口的。(如果您不想自定义所有控件,则可以使用Model-GUI-Mediator模式,让中介实现所需的接口。) - Ian Goldby

4

接口解决了某些问题。其主要功能是定义接口,区分定义和实现。

当您想要指定或检查类是否支持一组方法时,请使用接口。

您不能以其他方式完成此操作。

(如果所有类都继承自相同的基类,则抽象类将定义接口。但是,当您处理不同的类层次结构时,需要接口来定义它们共有的方法...)


1
Jorn,你可以使用RTTI检查一个类是否实现了某个方法,也可以使用RTTI调用它:接口并不是唯一的方法,但它们是更加优雅的方式。 - Cosmin Prund
2
@Cosmin 错了。RTTI 很长一段时间内只对发布的方法可用(而不是公共和以下的方法),所以概括地说“你可以”是不正确的。 - Eugene Mayevski 'Callback
2
@Cosmin:然而,接口更像是一种契约。您可以使用RTTI检查方法及其参数,但匹配可能仅仅是偶然的。请注意答案中的“方法集”。例如,一个类可以实现多个接口,每个接口都可以包含一个GetCount()方法。它们不必是相同的方法。这是无法通过RTTI实现的。 - mghie

4

关于缺点的额外说明:性能

我认为许多人对接口的性能惩罚过于轻率地忽视了。(并不是说我不喜欢和使用接口,但你应该意识到自己所涉及的内容)。接口可能会很昂贵,不仅因为_AddRef / _Release 的影响(即使你只返回-1),而且属性必须具有Get方法。根据我的经验,类中的大多数属性都有直接访问的读取器(例如,propery Prop1:Integer read FProp1 write SetProp1)。将直接、无惩罚的访问更改为函数调用可能会严重影响您的速度(特别是当您开始在循环内添加10个以上的属性调用时)。

例如,使用类的简单循环:

for i := 0 to 99 do
begin
  j := (MyClass.Prop1 + MyClass.Prop2 + MyClass.Prop3) / MyClass.Prop4;
  MyClass.Update;
  // do something with j
end;

当类成为接口时,函数调用次数从0到400不等。如果在循环中添加更多属性,则情况会变得更糟。

您可以使用一些技巧来缓解 _AddRef / _Release 惩罚(我相信还有其他技巧。这是我随意想到的):

  • 使用 WITH 或将其赋值给临时变量,以仅对每个代码块产生一个 _AddRef / _Release 惩罚。
  • 始终使用 const 关键字将接口传递到函数中(否则,每次调用该函数时都会发生额外的 _AddRef / _Release。)

1
样例循环是一种代码异味,表明执行计算的责任在错误的位置。例如,在这种情况下,用j := MyClass.CalculateJ;替换j := ...。提示:如果分析识别出通过接口重复调用导致性能问题,请考虑重构以将责任移动到更合适的位置。 - Disillusioned

1

除了 COM/ActiveX 等情况下,我们唯一需要使用接口的情况是当我们需要多重继承时,而接口是唯一的实现方式。在其他几种情况下,当我们尝试使用接口时,我们遇到了各种问题,主要是引用计数方面的问题(当对象既作为类实例又通过接口访问时)。

因此,我的建议是只有在您知道您需要它们时才使用它们,而不是认为它们可以在某些方面使您的生活更轻松。

更新:正如 David 提醒的那样,使用接口只能获得接口的多重继承,而不能获得实现的多重继承。但对于我们的需求来说,这是可以接受的。


5
尊敬的欧金,“-1”您提供的建议是“只有当您知道需要它们时才使用它们,而不是认为可以让生活更轻松”。您肯定在实现中遇到了一些问题,当然,如果接口可以使生活更加轻松,就应该使用它们。如果误用或误解普通对象,人们可能遇到大致相同数量的麻烦。 - Cosmin Prund
1
@Cosmin,你一生中开发了多少个Delphi项目?我们能看到一些吗? - Eugene Mayevski 'Callback
@Eugene 同意。我仍然认为你对Cosmin的评论听起来非常粗鲁,但也许你并不是有意这么轻蔑地表达。 - David Heffernan
1
@Eugene 但是你之前并没有澄清这一点,就像你现在所做的那样。很多人可能会把你最初的评论解释为攻击性和不屑一顾的。你本可以使用更温和的语言来达到良好的效果。无论如何,你现在已经澄清了这一点,这是好的。 - David Heffernan
5
@Eugene,任何人的个人资料都是不可验证的,所以这并不重要。我不想在这里讨论我的个人问题,而且你似乎攻击了我的人格,而不是评论我的想法。我相信你并不是有意的,但不幸的是,它给人留下了那样的印象。 - Cosmin Prund
显示剩余7条评论

1

除了其他人已经列出的内容之外,接口的一个重要优点是能够聚合它们。

我之前写过一篇关于这个主题的博客文章,可以在这里找到:http://www.nexusdb.com/support/index.php?q=intf-aggregation(简而言之:您可以拥有多个对象,每个对象都实现一个接口,然后将它们组装成一个聚合体,对外界来说,它看起来像是一个实现了所有这些接口的单个对象)

您还可以查看链接的“接口基础”和“高级接口使用和模式”文章。


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