206

我正处于开发学习阶段,我觉得我必须更多地了解接口。

我经常阅读它们,但似乎我无法掌握它们。

我读过这样的例子:Animal base class, with IAnimal interface for things like 'Walk', 'Run', 'GetLegs', etc - 但我从来没有做过什么,感觉就像“嘿,我应该使用一个接口这里!”

我错过了什么?为什么我很难掌握这个概念!我只是被这样一个事实吓倒了,我可能永远不会意识到对一个人的具体需求——主要是由于缺乏理解它们的一些方面!这让我觉得作为一名开发人员,我错过了一些顶级的东西!如果有人有过这样的经历并取得了突破,我会很感激一些关于如何理解这个概念的提示。谢谢你。

4

24 回答 24

158

它解决了这个具体问题:

您有 4 种不同类型的 a、b、c、d。在你的代码中,你有类似的东西:

a.Process();
b.Process();
c.Process();
d.Process();

为什么不让他们实现 IProcessable,然后做

List<IProcessable> list;

foreach(IProcessable p in list)
    p.Process();

当您添加例如 50 种类型的类都做同样的事情时,这将更好地扩展。


另一个具体问题:

你看过 System.Linq.Enumerable 吗?它定义了大量的扩展方法,可以对任何实现 IEnumerable 的类型进行操作。因为任何实现 IEnumerable 的东西基本上都说“我支持以无序的 foreach 类型模式进行迭代”,所以您可以为任何可枚举类型定义复杂的行为(Count、Max、Where、Select 等)。

于 2009-01-14T19:06:32.050 回答
140

我非常喜欢吉米的回答,但我觉得我需要添加一些东西。整个事情的关键是IProcessable中的“able . 它表示实现接口的对象的能力(或属性,但意思是“内在品质”,而不是 C# 属性的意义)。IAnimal 可能不是一个很好的接口示例,但如果您的系统有很多可以行走的东西,IWalkable 可能是一个很好的接口。您可能有从 Animal 派生的类,例如 Dog、Cow、Fish、Snake。前两个可能会实现 IWalkable,后两个不会走路,所以他们不会。现在你问“为什么不创建另一个超类,WalkingAnimal,它派生自 Dog 和 Cow?”。答案是当你有完全在继承树之外也可以行走的东西时,比如机器人。Robot 将实现 IWalkable,但可能不会从 Animal 派生。如果你想要一个可以走路的东西的清单,

现在将 IWalkable 替换为更软件化的东西,例如 IPersistable,这个类比变得更接近于您在真实程序中看到的内容。

于 2009-01-14T19:13:45.637 回答
75

当相同功能的实现不同时使用接口。

当您需要共享一个通用的具体实现时,请使用抽象/基类。

于 2009-01-14T19:10:25.350 回答
34

想想像合同这样的接口。这是一种说法,“这些类应该遵循这些规则。”

所以在 IAnimal 示例中,这是一种说法,“我必须能够在实现 IAnimal 的类上调用 Run、Walk 等。”

为什么这很有用?您可能想要构建一个函数,该函数依赖于您必须能够在对象上调用 Run 和 Walk 的事实。你可以有以下内容:

public void RunThenWalk(Monkey m) {
    m.Run();
    m.Walk();
}

public void RunThenWalk(Dog d) {
    d.Run();
    d.Walk();
}

...并对所有你知道可以跑和走的物体重复这个过程。但是,使用您的 IAnimal 接口,您可以定义一次函数,如下所示:

public void RunThenWalk(IAnimal a) {
    a.Run();
    a.Walk();
}

通过针对接口进行编程,您实际上是在信任类来实现接口的意图。因此,在我们的示例中,想法是“我不在乎他们如何跑步和走路,只要他们跑步和走路。只要他们履行该协议,我的 RunThenWalk 就会有效。它运行良好,无需了解其他任何信息班上。”

在这个相关问题中也有很好的讨论。

于 2009-01-14T19:05:12.987 回答
21

不要太担心。许多开发人员很少需要编写接口。您将经常使用.NET框架中可用的接口,但如果您不觉得有必要很快编写一个,那也就不足为奇了。

我总是给别人举的例子是,如果你有一个 Sailboat 类和一个 Viper 类。它们分别继承了 Boat 类和 Car 类。现在说您需要遍历所有这些对象并调用它们的Drive()方法。虽然您可以编写如下代码:

if(myObject is Boat)
    ((Boat)myObject).Drive()
else
    if (myObject is Car)
        ((Car)myObject).Drive()

写起来会简单得多:

((IDrivable)myObject).Drive()
于 2009-01-14T19:11:02.790 回答
18

我喜欢军队的比喻。

中士不在乎你是软件开发者音乐家还是律师
你被当作士兵对待。

微升

中士更容易不理会与他一起工作的人的具体细节,将每个
人都视为士兵的抽象(......并在他们没有表现得像他们时惩罚他们)。

人像士兵一样行动的能力称为多态性。

接口是有助于实现多态性的软件结构。

需要抽象细节以实现简单性是您问题的答案。

多态性,在词源上意味着“多种形式”,是一种将基类的任何子类的对象视为基类的对象的能力。因此,基类有多种形式:基类本身及其任何子类。

(..) 这使您的代码更易于编写,也更易于他人理解。它还使您的代码可扩展,因为稍后可以将其他子类添加到类型族中,并且这些新子类的对象也可以与现有代码一起使用。

于 2011-04-07T11:36:54.740 回答
16

Jimmy 说得对,当您希望能够将单个变量用于多种类型时,但所有这些类型都通过接口声明实现相同的方法。然后你可以在接口类型变量上调用它们的 main 方法。

然而,使用接口还有第二个原因。当项目架构师与实现编码员是不同的人时,或者有多个实现编码员和一个项目经理。负责人可以写一大堆接口,看看系统互通,然后留给开发人员用实现类来填充接口。这是确保多人编写兼容类的最佳方式,并且他们可以并行执行。

于 2009-01-14T19:10:33.663 回答
14

根据我的经验,直到我开始使用模拟框架进行单元测试时,才会出现创建接口的驱动力。很明显,使用接口将使模拟变得更加容易(因为框架依赖于虚拟方法)。一旦我开始,我就看到了从实现中抽象出我的类的接口的价值。即使我没有创建实际的接口,我现在也尝试使我的方法虚拟化(提供一个可以被覆盖的隐式接口)。

我发现还有很多其他原因可以强化重构接口的良好实践,但是单元测试/模拟是提供实践经验的最初“啊哈时刻”的原因。

编辑:为了澄清,通过单元测试和模拟,我总是有两个实现——真实的、具体的实现和用于测试的替代模拟实现。一旦你有了两个实现,接口的价值就变得显而易见了——从接口的角度来处理它,这样你就可以随时替换实现。在这种情况下,我将其替换为模拟接口。我知道如果我的类构造正确,我可以在没有实际接口的情况下做到这一点,但是使用实际接口可以加强这一点并使其更清晰(对读者来说更清楚)。如果没有这种推动力,我认为我不会欣赏接口的价值,因为我的大多数类只有一个具体的实现。

于 2009-01-14T19:09:52.510 回答
10

一些非编程示例可能会帮助您了解编程中接口的适当用途。

电气设备和电网之间有一个接口——它是关于插头和插座的形状以及它们之间的电压/电流的一组约定。如果你想实现一个新的电气设备,只要你的插头遵守规则,它就能从网络中获得服务。这使得可扩展性非常容易,并消除或降低了协调成本:您不必通知电力供应商您的新设备如何工作,也不必就如何将新设备插入网络达成单独的协议。

各国有标准的轨距。这允许放下铁轨的工程公司和建造在这些铁轨上运行的火车的工程公司之间进行分工,并且使铁路公司可以在不重新构建整个系统的情况下更换和升级火车。

企业向客户提供的服务可以描述为一个接口:定义良好的接口强调服务,隐藏手段。当您将一封信放入邮箱时,您希望邮政系统在给定的时间内送达信件,但您对信件的送达方式没有任何期望:您不需要知道,邮政服务可以灵活地选择最符合要求和当前情况的交付方式。一个例外是客户选择航空邮件的能力——这不是现代计算机程序员会设计的那种界面,因为它揭示了太多的实现。

来自大自然的例子:我不太喜欢eats()、makesSound()、moves()等例子。它们确实描述了行为,这是正确的,但它们没有描述交互以及它们是如何启用的。在自然界中实现交互的接口的明显示例与繁殖有关,例如,一朵花为蜜蜂提供了一个特定的接口,以便可以进行授粉。

于 2009-01-14T20:07:22.580 回答
6

作为 .net 开发人员,完全有可能终其一生,从不编写自己的接口。毕竟,我们在没有它们的情况下存活了几十年,而且我们的语言仍然是图灵完备的。

我不能告诉你为什么需要接口,但我可以给你一个我们在当前项目中使用它们的列表:

  1. 在我们的插件模型中,我们通过接口加载插件并将该接口提供给插件编写者以符合。

  2. 在我们的机器间消息传递系统中,消息类都实现了一个特定的接口,并使用该接口“解包”。

  3. 我们的配置管理系统定义了一个用于设置和检索配置设置的接口。

  4. 我们有一个接口可以用来避免讨厌的循环引用问题。(如果不需要,请不要这样做。)

我想如果有一个规则,那就是当你想在一个 is-a 关系中对几个类进行分组时使用接口,但你不想在基类中提供任何实现。

于 2009-01-14T19:12:11.570 回答
6

假设您想要模拟当您尝试入睡时可能发生的烦恼。

接口前的模型

在此处输入图像描述

class Mosquito {
    void flyAroundYourHead(){}
}

class Neighbour{
    void startScreaming(){}
}

class LampJustOutsideYourWindow(){
    void shineJustThroughYourWindow() {}
}

正如您清楚地看到的那样,当您尝试入睡时,许多“事情”可能会很烦人。

使用没有接口的类

但是在使用这些类时,我们遇到了问题。他们没有任何共同之处。您必须分别调用每个方法。

class TestAnnoyingThings{
    void testAnnoyingThinks(Mosquito mosquito, Neighbour neighbour, LampJustOutsideYourWindow lamp){
         if(mosquito != null){
             mosquito.flyAroundYourHead();
         }
         if(neighbour!= null){
             neighbour.startScreaming();
         }
         if(lamp!= null){
             lamp.shineJustThroughYourWindow();
         }
    }
}

带接口的模型

为了克服这个问题,我们可以引入一个 iterface在此处输入图像描述

interface Annoying{
   public void annoy();

}

并在类中实现它

class Mosquito implements Annoying {
    void flyAroundYourHead(){}

    void annoy(){
        flyAroundYourHead();
    }
}

class Neighbour implements Annoying{
    void startScreaming(){}

    void annoy(){
        startScreaming();
    }
}

class LampJustOutsideYourWindow implements Annoying{
    void shineJustThroughYourWindow() {}

    void annoy(){
        shineJustThroughYourWindow();
    }
}

使用接口

这将使这些类的使用更加容易

class TestAnnoyingThings{
    void testAnnoyingThinks(Annoying annoying){
        annoying.annoy();
    }
}
于 2017-05-26T07:49:18.993 回答
5

一个代码示例(Andrew's 与我在what-is-the-purpose-of-interfaces 中的一个额外的组合),这也说明了为什么在不支持多重继承的语言上使用接口而不是抽象类(c# 和爪哇):

interface ILogger
{
    void Log();
}
class FileLogger : ILogger
{
    public void Log() { }
}
class DataBaseLogger : ILogger
{
    public void Log() { }
}
public class MySpecialLogger : SpecialLoggerBase, ILogger
{
    public void Log() { }
}

请注意,FileLogger 和 DataBaseLogger 不需要接口(可以是 Logger 抽象基类)。但考虑到您需要使用强制您使用基类的第三方记录器(假设它公开了您需要使用的受保护方法)。由于该语言不支持多重继承,您将无法使用抽象基类方法。

底线是:尽可能使用接口以获得额外的代码灵活性。您的实现没有那么紧密,因此它可以更好地适应变化。

于 2009-03-20T18:49:35.020 回答
4

一旦需要强制类的行为,就应该定义一个接口。

动物的行为可能涉及步行、进食、跑步等。因此,您将它们定义为接口。

另一个实际示例是 ActionListener(或 Runnable)接口。当您需要跟踪特定事件时,您将实施它们。actionPerformed(Event e)因此,您需要在您的类(或子类)中提供该方法的实现。同样,对于 Runnable 接口,您提供public void run()方法的实现。

此外,您可以让这些接口由任意数量的类实现。

使用接口(在 Java 中)的另一个实例是实现 C++ 中提供的多重继承。

于 2009-01-14T19:13:07.463 回答
4

我不时使用接口,这是我的最新用法(名称已被概括):

我在 WinForm 上有一堆自定义控件,需要将数据保存到我的业务对象。一种方法是分别调用每个控件:

myBusinessObject.Save(controlA.Data);
myBusinessObject.Save(controlB.Data);
myBusinessObject.Save(controlC.Data);

这个实现的问题是,每当我添加一个控件时,我都必须进入我的“保存数据”方法并添加新控件。

我更改了我的控件以实现一个具有方法 SaveToBusinessObject(...) 的 ISaveable 接口,所以现在我的“保存数据”方法只是遍历控件,如果它找到一个 ISaveable,它调用 SaveToBusinessObject。因此,现在当需要一个新控件时,只需在该对象中实现 ISaveable(并且永远不要接触另一个类)。

foreach(Control c in Controls)
{
  ISaveable s = c as ISaveable;

  if( s != null )
      s.SaveToBusinessObject(myBusinessObject);
}

接口通常未实现的好处是您可以本地化修改。一旦定义,您将很少更改应用程序的整体流程,但您通常会在细节级别进行更改。当您将详细信息保留在特定对象中时,ProcessA 中的更改不会影响 ProcessB 中的更改。(基类也给你这个好处。)

编辑:另一个好处是行动的特异性。就像在我的示例中一样,我要做的就是保存数据;我不在乎它是什么类型的控件,或者它是否可以做任何其他事情——我只想知道我是否可以将数据保存在控件中。它使我的保存代码非常清晰——没有检查它是否是文本、数字、布尔值或其他任何内容,因为自定义控件处理所有这些。

于 2009-01-14T19:52:09.960 回答
2

最简单的例子是支付处理器(Paypal、PDS 等)。

假设您创建了一个具有 ProcessACH 和 ProcessCreditCard 方法的接口 IPaymentProcessor。

您现在可以实现一个具体的 Paypal 实现。使这些方法调用 PayPal 特定功能。

如果您稍后决定需要切换到另一个提供商,您可以。只需为新提供者创建另一个具体实现。由于您所绑定的只是您的接口(合同),因此您可以在不更改使用它的代码的情况下更换您的应用程序使用的接口。

于 2009-01-14T19:13:42.377 回答
2

如果您浏览 .NET Framework 程序集并深入了解任何标准对象的基类,您会注意到许多接口(名为 ISomeName 的成员)。

接口基本上用于实现框架,无论大小。在我想编写自己的框架之前,我对接口也有同样的感觉。我还发现理解接口帮助我更快地学习框架。当您想为几乎任何事情编写更优雅的解决方案时,您会发现界面非常有意义。这就像一种让班级为工作穿上合适衣服的方法。更重要的是,接口允许系统变得更加自文档化,因为当类实现接口时,复杂对象变得不那么复杂,这有助于对其功能进行分类。

当类希望能够显式或隐式地参与框架时,它们会实现接口。例如,IDisposable 是一个通用接口,它为流行且有用的 Dispose() 方法提供方法签名。在框架中,您或其他开发人员需要了解的关于类的所有信息是,如果它实现了 IDisposable,那么您就知道 ((IDisposable)myObject).Dispose() 可用于调用以进行清理。

经典示例:如果不实现 IDisposable 接口,就不能在 C# 中使用“using()”关键字构造,因为它要求任何指定为参数的对象都可以隐式转换为 IDisposable。

复杂示例:更复杂的示例是 System.ComponentModel.Component 类。此类实现 IDisposable 和 IComponent。大多数(如果不是全部)具有与其关联的可视化设计器的 .NET 对象都实现了 IComponent,以便 IDE 能够与组件进行交互。

结论:随着您对 .NET Framework 越来越熟悉,当您在对象浏览器或 .NET Reflector(免费)工具 ( http://www.red-gate.com ) 中遇到新类时,首先要做的就是/products/reflector/ ) 是检查它继承自哪个类以及它实现的接口。.NET Reflector 甚至比对象浏览器更好,因为它还可以让您查看派生类。这使您可以了解派生自特定类的所有对象,从而可能了解您不知道存在的框架功能。当更新或新的命名空间添加到 .NET Framework 时,这一点尤其重要。

于 2009-01-14T22:31:06.410 回答
2

它还允许您执行模拟单元测试 (.Net)。如果您的类使用接口,您可以在单元测试中模拟对象并轻松测试逻辑(无需实际访问数据库或 Web 服务等)。

http://www.nmock.org/

于 2009-01-14T22:36:42.063 回答
2

假设您正在制作第一人称射击游戏。玩家有多种枪可供选择。

我们可以有一个Gun定义函数的接口shoot()

我们需要类的不同子GunShotGun Sniper,等等。

ShotGun implements Gun{
    public void shoot(){
       \\shotgun implementation of shoot.
    } 
}

Sniper implements Gun{
    public void shoot(){
       \\sniper implementation of shoot.
    } 
}

射手班

射手的盔甲里有所有的枪。让我们创建一个List来表示它。

List<Gun> listOfGuns = new ArrayList<Gun>();

射手在需要时使用该功能循环使用他的枪switchGun()

public void switchGun(){
    //code to cycle through the guns from the list of guns.
    currentGun = //the next gun in the list.
}

我们可以使用上面的函数设置当前的 Gun ,并在调用 shoot()时简单地调用函数fire()

public void fire(){
    currentGun.shoot();
}

拍摄功能的行为会根据Gun接口的不同实现而有所不同。

结论

当一个类函数依赖于另一个类的函数时,创建一个接口,该函数会根据实现的类的实例(对象)改变其行为。

例如fire(),类中的函数Shooter需要 guns( Sniper, ShotGun) 来实现该shoot()函数。所以如果我们换枪开火。

shooter.switchGun();
shooter.fire();

我们改变了fire()函数的行为。

于 2013-07-15T17:53:05.900 回答
1

扩展拉塞纳尔所说的内容。接口是所有实现类必须遵循的契约。因此,您可以对合约使用一种称为编程的技术。这使您的软件变得独立于实现。

于 2009-01-14T19:07:28.520 回答
1

当您想要定义对象可以展示的行为时,通常会使用接口。

.NET 世界中的一个很好的例子是IDisposable接口,它用于任何使用必须手动释放的系统资源的 Microsoft 类。它要求实现它的类具有 Dispose() 方法。

(Dispose() 方法也由VB.NETC#的 using 语言构造调用,仅适用于IDisposables)

请记住,您可以通过使用TypeOf ... Is(VB.NET)、is(C#)、instanceof(Java) 等结构来检查对象是否实现了特定接口...

于 2009-01-14T20:02:10.953 回答
1

正如一些人可能已经回答的那样,接口可用于在类之间强制执行某些行为,这些行为不会以相同的方式实现这些行为。因此,通过实现一个接口,您是在说您的类具有该接口的行为。IAnimal 接口不会是典型的接口,因为 Dog、Cat、Bird 等类是动物的类型,并且应该可能扩展它,这是继承的一种情况。相反,在这种情况下,接口更像是动物行为,例如 IRunnable、IFlyable、ITrainable 等。

接口有很多好处,关键之一是可插拔性。例如,声明一个具有 List 参数的方法将允许传入实现 List 接口的任何内容,从而允许开发人员在以后删除和插入不同的列表,而无需重写大量代码。

您可能永远不会使用接口,但如果您从头开始设计一个项目,尤其是某种框架,您可能希望熟悉它们。

我建议阅读Coad、Mayfield 和 Kern的Java 设计中的接口一章。他们比一般的介绍性文字解释得更好。如果你不使用 Java,你可以阅读本章的开头,主要是概念。

于 2009-01-14T20:03:05.057 回答
1

与任何增加系统灵活性的编程技术一样,接口也增加了一定程度的复杂性。它们通常很棒,你可以在任何地方使用它(你可以为你的所有类创建一个接口)——但是这样做,你会创建一个更复杂的系统,并且更难维护。

像往常一样,这里有一个权衡:灵活性优于可维护性。哪个更重要?没有答案 - 这取决于项目。但请记住,每个软件都必须维护......

所以我的建议:在你真正需要它们之前不要使用接口。(使用 Visual Studio,您可以在 2 秒内从现有类中提取接口 - 所以不要着急。)

话虽如此,你什么时候需要创建一个界面?

当我重构一个突然需要处理两个或更多类似类的方法时,我会这样做。然后我创建一个接口,将此接口分配给两个(或更多)相似的类,并更改方法参数类型(将类类型替换为接口类型)。

它有效:o)

一个例外:当我模拟对象时,界面更容易使用。所以我经常为此创建界面。

PS:当我写“接口”时,我的意思是:“任何基类的接口”,包括纯接口类。请注意,抽象类通常比纯接口更好,因为您可以向它们添加逻辑。

问候,西尔万。

于 2009-01-14T20:30:42.877 回答
1

使用接口有很多用途。

  1. 在多态行为中使用。您想在其中调用具有对子类的引用的接口的子类的特定方法。

  2. 与类签订合同以实现所有必要的方法,就像最常见的使用 COM 对象一样,在继承接口的 DLL 上生成包装类;这些方法在幕后调用,您只需要实现它们,但具有与 COM DLL 中定义的相同结构,您只能通过它们公开的接口知道。

  3. 通过在类中加载特定方法来减少内存使用。就像如果您有三个业务对象并且它们在一个类中实现,那么您可以使用三个接口。

例如 IUser、IOrder、IOrderItem

public interface IUser()
{

void AddUser(string name ,string fname);

}

// Same for IOrder and IOrderItem
//


public class  BusinessLayer: IUser, IOrder, IOrderItem

{    
    public void AddUser(string name ,string fname)
    {
        // Do stuffs here.
    }

    // All methods from all interfaces must be implemented.

}

如果您只想添加一个用户,请执行以下操作:

IUser user = new (IUser)BusinessLayer();

// It will load  all methods into memory which are declared in the IUser interface.

user.AddUser();
于 2009-06-24T11:37:04.233 回答
1

当您成为库开发人员(为其他编码人员编写代码的人)时,接口将变得明显。我们大多数人都是从应用程序开发人员开始的,我们使用现有的 API 和编程库。

就像接口是一种契约一样,没有人提到接口是使代码的某些部分稳定的好方法。当它是一个团队项目时(或者当您正在开发其他开发人员使用的代码时),这尤其有用。所以,这里有一个具体的场景:

当您在团队中开发代码时,其他人可能会使用您编写的代码。当他们对您的(稳定的)接口进行编码时,他们会非常高兴,而当您可以自由地更改您的实现(隐藏在接口后面)而不破坏团队的代码时,您会很高兴。它是信息隐藏的一种变体(接口是公开的,实现对客户端程序员隐藏)。阅读有关受保护变体的更多信息。

另请参阅有关对 Interface 进行编码的相关问题

于 2012-11-28T20:41:02.800 回答