接口-有什么意义?


269

界面的原因确实使我难以理解。据我了解,这是针对C#中不存在的不存在的多重继承的一种解决方法(或者有人告诉我)。

我所看到的是,您预先定义了一些成员和函数,然后必须在类中重新定义它们。从而使接口冗余。只是感觉像语法...好吧,对我而言是垃圾(请不要冒犯。垃圾就像无用的东西一样)。

在下面给出的示例中,该示例是在堆栈溢出时从另一个C#接口线程获取的,我只是创建一个称为Pizza的基类而不是一个接口。

简单示例(来自不同的堆栈溢出贡献)

public interface IPizza
{
    public void Order();
}

public class PepperoniPizza : IPizza
{
    public void Order()
    {
        //Order Pepperoni pizza
    }
}

public class HawaiiPizza : IPizza
{
    public void Order()
    {
        //Order HawaiiPizza
    }
}

我觉得在SO上这个问题有很多重复之处,但是它们似乎都只是在解释接口的合同部分,所以我不确定它们是否适用。
Lasse V. Karlsen

7
为了成为一个好看又整洁的用户,在发布内容之前,我倾向于先在各个论坛中寻找答案。不幸的是,它们中的大多数是在较晚的阶段开始的,其余的并没有帮助。我已经在为基本的“为什么要这么做”而苦苦挣扎了。在我看来,这似乎不必要地使它复杂化。顺便说一句。感谢所有人的快速答复。我必须先将它们全部消化掉,但是我认为现在我对它们的要点有了一个很好的认识。似乎我总是从不同的角度看待它。非常感谢你的帮助。
Nebelhom 2011年


1
接口还可以帮助建立继承,例如struct类型。
ja72

3
嗯,OP在问:“据我了解,接口是针对C#中不存在的不存在的多重继承的一种解决方法。(除此之外,在引用的教科书比萨饼示例中)使用基类而不是接口”。然后,大多数答案要么给出一个可以由(抽象)基类实现的示例,要么给出一个示例,以说明接口对于多继承场景是如何必要的。这些答案都很好,但是难道他们不只是重申OP已经知道的内容吗?难怪OP最终选择了没有示例的答案。大声笑
RayLuo

Answers:


176

关键是接口代表合同。任何实现类都必须具有的一组公共方法。从技术上讲,该接口仅控制语法,即那里有什么方法,它们得到了什么参数以及它们返回了什么。通常,它们也封装语义,尽管只能通过文档进行封装。

然后,您可以使用接口的不同实现,并随意替换它们。在您的示例中,由于每个披萨实例都是一个,因此IPizza您可以IPizza在处理未知披萨类型的实例的任何地方使用。类型继承自的任何实例IPizza都可以保证可排序,因为它具有Order()方法。

Python不是静态类型的,因此类型会在运行时保留并查找。因此,您可以尝试Order()在任何对象上调用方法。只要对象具有这种方法,并且可能只是耸耸肩说“ Meh。”(如果没有),运行时就会很高兴。在C#中不是这样。编译器负责进行正确的调用,如果只是随机的,object则编译器尚不知道运行时实例是否具有该方法。从编译器的角度来看,它无效,因为它无法验证它。(您可以使用反射或dynamic关键字来执行此类操作,但是我猜这有点远了。)

还要注意,通常意义上的接口不一定必须是C#interface,它也可以是抽象类,甚至可以是普通类(如果所有子类都需要共享一些通用代码,则可以派上用场)–但是interface就足够了)。


1
+1,尽管我不会说该接口(从某种意义上来说)可以是抽象类或普通类。
Groo

3
我要补充一点,您不能期望在几分钟之内就能理解界面。我认为,如果您没有多年的面向对象编程经验,那么理解接口是不合理的。您可以在书籍中添加一些链接。我会建议:.NET中的依赖注入实际上是一个严重的漏洞,而不仅仅是一个简短的介绍。
knut

2
啊,这是我没有DI线索的问题。但是我想问问问者的主要问题是为什么在Python中不需要它们时为什么需要它们。这就是我回答中最大的一段。我认为没有必要在这里挖掘使用接口的每种模式和实践。
乔伊,

1
好吧,那么您的问题就变成了:“为什么在动态编程语言更易于编程时为什么要使用静态编程语言?”。尽管我不是回答这个问题的专家,但我可以大胆地说性能是决定性的问题。调用python对象时,进程必须在运行时确定该对象是否具有称为“ Order”的方法,而如果您调用C#对象,则已经确定它实现了该方法,并且可以拨打这样的地址。
Boluc Papuccuoglu

3
@BolucPapuccuoglu:除此之外,如果有一个静态类型的对象(如果知道foo实现)IWidget,那么程序员看到一个调用foo.woozle()可以查看文档,IWidget并知道该方法应该做什么。程序员可能无法知道实际实现的代码将来自何处,但是遵守IWidget接口协定的任何类型都将以foo与该协定一致的方式来实现。相比之下,在动态语言中,没有什么含义明确的参考点foo.woozle()
2015年

440

没有人真正地用简单的术语解释过接口是如何有用的,所以我要给它做个尝试(并从Shamim的答案中窃取一个想法)。

让我们考虑一下比萨订购服务的想法。您可以有多种比萨饼,每个比萨饼的共同操作是在系统中准备订单。每个披萨都必须准备,但是每个披萨都准备不同。例如,订购硬皮披萨时,系统可能必须验证餐厅是否有某些食材,并将深盘披萨不需要的那些放在一旁。

用代码编写时,从技术上讲,您可以

public class Pizza()
{
    public void Prepare(PizzaType tp)
    {
        switch (tp)
        {
            case PizzaType.StuffedCrust:
                // prepare stuffed crust ingredients in system
                break;

            case PizzaType.DeepDish:
                // prepare deep dish ingredients in system
                break;

            //.... etc.
        }
    }
}

但是,深盘披萨(用C#术语)可能需要在Prepare()方法中设置与填充硬皮披萨不同的属性,因此您最终会得到很多可选属性,并且该类无法很好地缩放(如果添加新的披萨类型)。

解决此问题的正确方法是使用接口。该界面声明可以准备所有比萨饼,但是每个比萨饼可以不同地准备。因此,如果您具有以下接口:

public interface IPizza
{
    void Prepare();
}

public class StuffedCrustPizza : IPizza
{
    public void Prepare()
    {
        // Set settings in system for stuffed crust preparations
    }
}

public class DeepDishPizza : IPizza
{
    public void Prepare()
    {
        // Set settings in system for deep dish preparations
    }
}

现在,您的订单处理代码无需确切地知道订购哪种比萨饼即可处理原料。它只有:

public PreparePizzas(IList<IPizza> pizzas)
{
    foreach (IPizza pizza in pizzas)
        pizza.Prepare();
}

即使每种披萨的制备方法不同,代码的这一部分也不必关心我们要处理的披萨类型,它只是知道正在调用披萨,因此每次调用Prepare都会自动正确地准备每个披萨。基于其类型,即使该集合具有多种比萨饼也是如此。


11
+1,我喜欢使用列表显示界面使用的示例。
danielunderwood

21
很好的答案,但也许可以对其进行修改,以阐明为什么在这种情况下接口比仅使用抽象类更好(对于这样一个简单的示例,抽象类可能确实更好?)
Jez 2014年

3
这是最好的答案。有一点错字,或者您可能只是复制并粘贴了代码。在C#接口中,您无需声明访问修饰符,例如public。因此在界面内部应该只是void Prepare();
Dush

26
我看不出这如何回答问题。使用基类和抽象方法可以很容易地完成此示例,这正是原始问题所指出的。
Jamie Kitson

4
直到您有很多接口时,接口才真正发挥作用。您只能从单个类继承(无论是否抽象),但是您可以根据需要在单个类中实现尽可能多的不同接口。突然之间,您不需要门框中的门;IOpenable可以做的任何事情,无论是门,窗户,信箱,只要您命名即可。
mtnielsen

123

对我而言,刚开始时,只有当您停止将它们视为使代码更容易/更快地编写的事情时,这些问题的意义才变得清楚-这不是它们的目的。它们有许多用途:

(这将失去比萨饼的类比,因为很难直观地看到它的用法)

假设您正在屏幕上制作一个简单的游戏,它将有您与之互动的生物。

答:通过在前端和后端实现之间引入松散耦合,它们可以使将来的代码更易于维护。

您可以先写这个,因为只会有巨魔:

// This is our back-end implementation of a troll
class Troll
{
    void Walk(int distance)
    {
        //Implementation here
    }
}

前端:

function SpawnCreature()
{
    Troll aTroll = new Troll();

    aTroll.Walk(1);
}

下线两周后,行销决定您也需要兽人,因为他们在Twitter上阅读了有关兽人的信息,因此您必须执行以下操作:

class Orc
{
    void Walk(int distance)
    {
        //Implementation (orcs are faster than trolls)
    }
}

前端:

void SpawnCreature(creatureType)
{
    switch(creatureType)
    {
         case Orc:

           Orc anOrc = new Orc();
           anORc.Walk();

          case Troll:

            Troll aTroll = new Troll();
             aTroll.Walk();
    }
}

您会看到它如何开始变得混乱。您可以在此处使用一个接口,这样您的前端将被编写一次并进行测试(这是重要的一点),然后您可以根据需要插入其他后端项目:

interface ICreature
{
    void Walk(int distance)
}

public class Troll : ICreature
public class Orc : ICreature 

//etc

那么前端是:

void SpawnCreature(creatureType)
{
    ICreature creature;

    switch(creatureType)
    {
         case Orc:

           creature = new Orc();

          case Troll:

            creature = new Troll();
    }

    creature.Walk();
}

现在,前端只在乎接口ICreature-不必为巨魔或兽人的内部实现而烦恼,而只是在它们实现ICreature的情况下为之。

从这个角度来看这一点时要注意的重要一点是,您还可以轻松地使用抽象生物类,并且从这个角度来看,这具有相同的效果。

您可以将创建的内容提取到工厂:

public class CreatureFactory {

 public ICreature GetCreature(creatureType)
 {
    ICreature creature;

    switch(creatureType)
    {
         case Orc:

           creature = new Orc();

          case Troll:

            creature = new Troll();
    }

    return creature;
  }
}

然后我们的前端将变为:

CreatureFactory _factory;

void SpawnCreature(creatureType)
{
    ICreature creature = _factory.GetCreature(creatureType);

    creature.Walk();
}

现在,前端甚至不必引用实现Troll和Orc的库(前提是工厂位于单独的库中)-不需要任何有关它们的信息。

B:假设您具有某些功能,而在其他方面同质的数据结构中只有某些生物会具有这种功能,例如

interface ICanTurnToStone
{
   void TurnToStone();
}

public class Troll: ICreature, ICanTurnToStone

前端可能是:

void SpawnCreatureInSunlight(creatureType)
{
    ICreature creature;

    switch(creatureType)
    {
         case Orc:

           creature = new Orc();

          case Troll:

            creature = new Troll();
    }

    creature.Walk();

    if (creature is ICanTurnToStone)
    {
       (ICanTurnToStone)creature.TurnToStone();
    }
}

C:依赖项注入的用法

当前端代码和后端实现之间的耦合非常松散时,大多数依赖项注入框架都更易于使用。如果我们以上面的工厂示例为例,并让我们的工厂实现一个接口:

public interface ICreatureFactory {
     ICreature GetCreature(string creatureType);
}

然后,我们的前端可以通过构造函数(通常)将其注入(例如MVC API控制器):

public class CreatureController : Controller {

   private readonly ICreatureFactory _factory;

   public CreatureController(ICreatureFactory factory) {
     _factory = factory;
   }

   public HttpResponseMessage TurnToStone(string creatureType) {

       ICreature creature = _factory.GetCreature(creatureType);

       creature.TurnToStone();

       return Request.CreateResponse(HttpStatusCode.OK);
   }
}

使用我们的DI框架(例如Ninject或Autofac),我们可以对其进行设置,以便在运行时只要构造函数中需要ICreatureFactory即可创建CreatureFactory的实例-这使我们的代码更加简洁。

这也意味着,当我们为控制器编写单元测试时,我们可以提供一个模拟的ICreatureFactory(例如,如果具体实现需要数据库访问权限,我们不希望单元测试依赖于此),并且可以轻松地在控制器中测试代码。

D:还有其他用途,例如,您有两个项目A和B,出于“传统”的原因,它们的结构不够完善,A引用了B。

然后,您可以在B中找到需要调用A中已有方法的功能。在获得循环引用时,您无法使用具体的实现来实现它。

您可以在B中声明一个接口,然后由A中的类实现。即使具体对象是A中的类型,也可以向B中的方法传递实现该接口的类的实例,而不会出现问题。


6
当您找到试图说出自己的答案的答案时,会感到烦恼吗-stackoverflow.com/a/93998/117215
Paddy

18
好消息是它的页面已死,您的不是:)。好的例子!
Sealer_14年

1
您的C讨论让我有些失落。但是,我喜欢您的A和B讨论,因为它们本质上都解释了如何使用接口来跨多个类提供通用的功能。对我而言仍然模糊的接口领域是如何将接口用于松散耦合。也许这就是您的C讨论所要解决的问题?如果是这样,我想我需要一个更详细的示例:)
user2075599

1
在您的示例中,我似乎认为ICreature更适合作为Troll和Orc的抽象基类(Creature?)。然后,生物之间的任何通用逻辑都可以在那里实现。这意味着您根本不需要ICreature接口...
Skarsnik

1
@Skarsnik-相当,这就是本笔记的意思:“从这种角度来看这一点时,需要注意的重要一点是,您还可以轻松地使用抽象生物类,并且从这个角度来看,它具有相同的含义影响。”
帕迪(Paddy)

33

这是您解释的示例:

public interface IFood // not Pizza
{
    public void Prepare();

}

public class Pizza : IFood
{
    public void Prepare() // Not order for explanations sake
    {
        //Prepare Pizza
    }
}

public class Burger : IFood
{
    public void Prepare()
    {
        //Prepare Burger
    }
}

27

上面的例子没有多大意义。您可以使用类完成上述所有示例(抽象类,如果您希望它仅作为合同使用):

public abstract class Food {
    public abstract void Prepare();
}

public class Pizza : Food  {
    public override void Prepare() { /* Prepare pizza */ }
}

public class Burger : Food  {
    public override void Prepare() { /* Prepare Burger */ }
}

您将获得与接口相同的行为。您可以创建一个List<Food>并进行迭代,而无需知道哪个类排在最前面。

更适当的例子是多重继承:

public abstract class MenuItem {
    public string Name { get; set; }
    public abstract void BringToTable();
}

// Notice Soda only inherits from MenuItem
public class Soda : MenuItem {
    public override void BringToTable() { /* Bring soda to table */ }
}


// All food needs to be cooked (real food) so we add this
// feature to all food menu items
public interface IFood {
    void Cook();
}

public class Pizza : MenuItem, IFood {
    public override void BringToTable() { /* Bring pizza to table */ }
    public void Cook() { /* Cook Pizza */ }
}

public class Burger : MenuItem, IFood {
    public override void BringToTable() { /* Bring burger to table */ }
    public void Cook() { /* Cook Burger */ }
}

然后,您可以将它们全部用作,MenuItem而不必关心它们如何处理每个方法调用。

public class Waiter {
    public void TakeOrder(IEnumerable<MenuItem> order) 
    {
        // Cook first
        // (all except soda because soda is not IFood)
        foreach (var food in order.OfType<IFood>())
            food.Cook();

        // Bring them all to the table
        // (everything, including soda, pizza and burger because they're all menu items)
        foreach (var menuItem in order)
            menuItem.BringToTable();
    }
}

2
我必须一直向下滚动才能找到一个答案,该答案实际上回答了“为什么不只使用抽象类而不是接口?”这个问题。似乎确实只处理c#缺少多重继承。
SyntaxRules

2
是的,这是对我最好的解释。唯一一个清楚地说明了为什么接口比抽象类更有用的原因。(即:比萨饼既是MenuItem,又是食物,而具有抽象类的比萨饼只能是一个或另一个,但不能同时是两者)
Maxter

25

用类推简单解释

要解决的问题:多态性的目的是什么?

打个比方:所以我是一个建筑工地的领班。

商人一直在施工现场行走。我不知道谁会穿过那些门。但是我基本上告诉他们该怎么做。

  1. 如果是木匠,我会说:建立木制脚手架。
  2. 如果是水管工,我会说:“设置管道”
  3. 如果是电工,我会说:“拔出电缆,然后换成光纤电缆”。

上述方法的问题在于,我必须:(i)知道谁在那扇门里走,并且根据是谁,我必须告诉他们该怎么做。这意味着我必须了解有关特定交易的所有信息。与这种方法相关的成本/收益:

知道该怎么做的含义:

  • 这意味着,如果木匠的代码从:更改BuildScaffolding()BuildScaffold()(即稍微更改名称),那么我还必须更改调用类(即Foreperson类)-您必须对代码进行两次更改,而不是(基本上) ) 只有一个。使用多态性,您(基本上)只需要进行一项更改即可获得相同的结果。

  • 其次,您不必经常问:您是谁?好吧...你是谁?好的。.....多态性-它使该代码干燥,并且在某些情况下非常有效:

  • 使用多态性,您可以轻松添加其他类别的商人,而无需更改任何现有代码。(即SOLID设计原则的第二个原则:开闭原则)。

解决方案

想象一下这样一个场景:无论谁走到门上,我都可以说:“ Work()”,他们会尽自己所擅长的尊重工作:水管工将处理管道,电工将处理电线。

这种方法的好处是:(i)我不需要确切知道谁正在走进那扇门-我所需要知道的是,他们将成为传统的人,他们可以工作,其次,(ii)我不需要了解该特定交易。交易将解决这一问题。

所以代替这个:

If(electrician) then  electrician.FixCablesAndElectricity() 

if(plumber) then plumber.IncreaseWaterPressureAndFixLeaks() 

我可以做这样的事情:

ITradesman tradie = Tradesman.Factory(); // in reality i know it's a plumber, but in the real world you won't know who's on the other side of the tradie assignment.

tradie.Work(); // and then tradie will do the work of a plumber, or electrician etc. depending on what type of tradesman he is. The foreman doesn't need to know anything, apart from telling the anonymous tradie to get to Work()!!

有什么好处?

这样做的好处是,如果木匠等人的具体工作要求发生变化,那么领班人就不需要更改他的代码了-他不需要知道或不在乎。重要的是木匠知道Work()的含义。其次,如果有一种新型的建筑工人来到工地,那么领班就不需要了解任何交易-领班所关心的只是建筑工人(例如Welder,Glazier,Tiler等)是否可以完成一些工作。


图解的问题和解决方案(有接口和无接口):

无接口(示例1):

示例1:没有接口

无接口(示例2):

示例2:不带接口

使用界面:

示例3:使用接口的好处

摘要

界面使您可以让工作人员完成分配给他们的工作,而无需知道确切的身份或他们可以做什么。这使您可以轻松地添加(交易)新类型,而无需更改现有代码(从技术上讲,您确实需要对其进行一点点更改),这是OOP方法与功能更强大的编程方法相比的真正好处。

如果您不了解上述任何内容,或者不清楚,请在评论中提问,我将尽力使答案更好。


4
再加上一个要穿连帽衫工作的家伙。
Yusha

11

在没有鸭子类型的情况下(您可以在Python中使用它),C#依靠接口来提供抽象。如果类的依赖项都是具体类型,则不能传递任何其他类型-使用接口可以传递实现接口的任何类型。


3
+1是的,如果您使用鸭子输入,则不需要界面。但是它们确实增强了类型安全性。
Groo

11

Pizza的例子很糟糕,因为您应该使用处理订单的抽象类,例如,Pizza应该只覆盖Pizza的类型。

当您拥有共享属性,但类从不同的地方继承,或者没有任何通用代码时,可以使用接口。例如,这是可以处置的二手物品IDisposable,您知道它将被处置,只是不知道处置后会发生什么。

接口只是一个契约,它告诉您对象可以执行的某些操作,期望的参数和返回类型。


10

考虑不控制或不拥有基类的情况。

以可视控件为例,在.NET for Winforms中,它们都继承自Control的基类,而Control是在.NET框架中完全定义的。

假设您从事创建自定义控件的工作。您想要构建新的按钮,文本框,列表视图,网格,诸如此类,并且希望它们全部具有特定于控件集的某些功能。

例如,您可能想要一种通用的方式来处理主题,或一种通用的方式来处理本地化。

在这种情况下,您不能“仅创建基类”,因为如果这样做,则必须重新实现与控件相关的所有内容

相反,您将来自Button,TextBox,ListView,GridView等,并添加您的代码。

但这带来了一个问题,您现在如何确定哪些控件是“您自己的”,如何构建一些代码,说明“对于属于我的表单上的所有控件,将主题设置为X”。

输入接口。

接口是一种查看对象的方法,可以确定对象是否遵守特定合同。

您将创建“ YourButton”,从Button继承,并添加对所需所有接口的支持。

这将使您可以编写如下代码:

foreach (Control ctrl in Controls)
{
    if (ctrl is IMyThemableControl)
        ((IMyThemableControl)ctrl).SetTheme(newTheme);
}

没有接口,这将是不可能的,相反,您必须编写如下代码:

foreach (Control ctrl in Controls)
{
    if (ctrl is MyThemableButton)
        ((MyThemableButton)ctrl).SetTheme(newTheme);
    else if (ctrl is MyThemableTextBox)
        ((MyThemableTextBox)ctrl).SetTheme(newTheme);
    else if (ctrl is MyThemableGridView)
        ((MyThemableGridView)ctrl).SetTheme(newTheme);
    else ....
}

是的,我知道,您不应该使用“ is”然后进行强制转换,将其放在一边。
Lasse V. Karlsen

4
我知道叹息,但这在这里是偶然的。
Lasse V. Karlsen

7

在这种情况下,您可以(并且可能会)仅定义一个Pizza基类并从中继承。但是,有两个原因使接口允许您执行其他方式无法实现的事情:

  1. 一个类可以实现多个接口。它只是定义了类必须具有的功能。实现一系列的接口意味着一个类可以在不同的地方实现多种功能。

  2. 可以在比类或调用者更大的范围内定义接口。这意味着您可以分离功能,分离项目依赖关系,并将功能保留在一个项目或类中,并将其实现保留在其他位置。

2的含义是您可以更改正在使用的类,仅要求它实现适当的接口即可。




4

如果我正在使用API​​绘制形状,则可能要使用DirectX或图形调用或OpenGL。因此,我将创建一个接口,该接口将从您所说的内容中抽象出我的实现。

因此,您调用了工厂方法:MyInterface i = MyGraphics.getInstance()。然后,您有了合同,因此您知道可以在中使用什么功能MyInterface。因此,您可以调用i.drawRectanglei.drawCube知道,如果将一个库换成另一个库,则支持该功能。

如果您使用的是依赖注入,这将变得更加重要,因为这样您就可以在XML文件中交换实现。

因此,您可能有一个可以导出的密码库供一般使用,而另一个可以出售给美国公司,其区别在于您可以更改配置文件,而程序的其余部分则没有改变了。

在.NET中,此方法在集合中非常有用,因为您应该只使用List变量,而不必担心它是ArrayList还是LinkedList。

只要您对接口进行编码,开发人员就可以更改实际的实现,并且程序的其余部分保持不变。

这在单元测试时也很有用,因为您可以模拟整个接口,因此,我不必去数据库,而可以模拟一个只返回静态数据的实现,因此我可以测试我的方法而不必担心数据库是否关闭以进行维护。


4

接口实际上是实现类必须遵循的契约,实际上,它是我所知道的几乎所有设计模式的基础。

在您的示例中,创建接口的原因是,然后保证IS Pizza的所有内容(即实现Pizza接口)都已实现

public void Order();

在您提到的代码之后,您可能会得到以下内容:

public void orderMyPizza(IPizza myPizza) {
//This will always work, because everyone MUST implement order
      myPizza.order();
}

这样,您就可以使用多态,而您所关心的就是您的对象响应order()。


4

我在此页面上搜索了“组成”一词,但一次也没有看到。除了前面提到的答案以外,这个答案还很多。

在面向对象的项目中使用接口的绝对关键原因之一是,它们使您更喜欢组合而不是继承。通过实现接口,您可以将实现与要应用到它们的各种算法分离。

德里克·巴纳斯(Derek Banas)撰写的这份精妙的“装饰器模式”教程(很有趣-还以比萨饼为例)是一个值得说明的例子:

https://www.youtube.com/watch?v=j40kRwSm4VE


2
我真的很震惊,这不是最好的答案。接口在其所有用途中,在组合中的作用最大。
Maciej Sitko '16

3

接口用于在不同类之间应用连接。例如,您有一个关于汽车和树木的课程;

public class Car { ... }

public class Tree { ... }

您想为两个类都添加可刻录功能。但是每个班级都有自己的燃烧方式。所以你简单地做

public class Car : IBurnable
{
public void Burn() { ... }
}

public class Tree : IBurnable
{
public void Burn() { ... }
}

2
在这样的示例中困扰我的问题是:为什么有帮助?我现在可以将IBurnable类型的参数传递给方法,并且可以处理具有IBurnable接口的所有类吗?与我发现的Pizza示例相同。可以执行此操作很好,但是我看不到这样做的好处。您是否可以扩展示例(因为目前我真的很胖)还是给出一个无法使用的示例(再次,因为我现在真的很胖)。非常感谢。
Nebelhom

1
同意。接口=“可以做”。Class / Abstract Calss =“是A”
Peter.Wang

3

当需要它们时,您将获得接口:)您可以研究示例,但需要Aha!真正获得它们的效果。

现在您知道了什么是接口,只需编写没有接口的代码即可。迟早您会遇到一个问题,其中使用接口是最自然的事情。


3

令我惊讶的是,没有多少文章包含一个界面的最重要原因:设计模式。这是使用合同的大图,尽管它是对机器代码的语法修饰(老实说,编译器可能会忽略它们),但是抽象和接口对于OOP,人类理解和复杂的系统体系结构至关重要。

让我们扩展一下比萨的类比,说一顿完整的三道菜大餐。我们仍将拥有Prepare()所有食物类别的核心界面,但我们还将为课程选择(入门,主菜,甜点)以及食物类型的不同属性(美味/甜食,素食/非素食,不含麸质等)。

基于这些规范,我们可以实现“ 抽象工厂”模式来概念化整个过程,但是可以使用接口来确保仅基础是具体的。其他所有内容都可以变得灵活或鼓励多态,但仍可以在Course实现该ICourse接口的不同类之间保持封装。

如果我有更多时间,我想画一个完整的例子,或者有人可以为我扩展这个例子,但是总而言之,C#接口将是设计此类系统的最佳工具。


1
这个答案值得加分!当用于设计模式(例如状态模式)时,界面会发光。有关更多信息,请参见plus.google.com/+ZoranHorvat-Programming
Alex Nolasco

3

这是矩形对象的接口:

interface IRectangular
{
    Int32 Width();
    Int32 Height();
}

它所需要的只是您实现访问对象的宽度和高度的方法。

现在,让我们定义一个将对以下任何对象都有效的方法IRectangular

static class Utils
{
    public static Int32 Area(IRectangular rect)
    {
        return rect.Width() * rect.Height();
    }
}

这将返回任何矩形对象的面积。

让我们实现一个SwimmingPool矩形的类:

class SwimmingPool : IRectangular
{
    int width;
    int height;

    public SwimmingPool(int w, int h)
    { width = w; height = h; }

    public int Width() { return width; }
    public int Height() { return height; }
}

另一个House也是矩形的类:

class House : IRectangular
{
    int width;
    int height;

    public House(int w, int h)
    { width = w; height = h; }

    public int Width() { return width; }
    public int Height() { return height; }
}

鉴于此,您可以Area在房屋或游泳池上调用该方法:

var house = new House(2, 3);

var pool = new SwimmingPool(3, 4);

Console.WriteLine(Utils.Area(house));
Console.WriteLine(Utils.Area(pool));

这样,您的类可以从任意数量的接口“继承”行为(静态方法)。



3

什么 ?

接口基本上是所有实现接口的类都应遵循的约定。它们看起来像一个类,但是没有实现。

C#按惯例命名的接口名称中,通过在前缀I之前进行定义,因此,如果要使用一个名为shapes的接口,则可以将其声明为IShapes

现在为什么呢?

Improves code re-usability

假设您要绘制CircleTriangle. 可以将它们组合在一起并调用它们,Shapes并具有绘制方法CircleTriangle 但是进行具体实现将是一个坏主意,因为明天您可能会决定再添加2个Shapes RectangleSquare。现在,当您添加它们时,很有可能会破坏代码的其他部分。

使用Interface,您可以将不同的实现与合同隔离


第一天现场直播

您被要求创建一个应用程序来绘制圆和三角形

interface IShapes
{
   void DrawShape();
   
 }

class Circle : IShapes
{
    
    public void DrawShape()
    {
        Console.WriteLine("Implementation to Draw a Circle");
    }
}

Class Triangle: IShapes
{
     public void DrawShape()
    {
        Console.WriteLine("Implementation to draw a Triangle");
    }
}
static void Main()
{
     List <IShapes> shapes = new List<IShapes>();
        shapes.Add(new Circle());
        shapes.Add(new Triangle());

        foreach(var shape in shapes)
        {
            shape.DrawShape();
        }
}

现场场景第2天

如果要求您添加Square和添加Rectangle它,您要做的就是class Square: IShapesMain添加和添加到列表中为其创建功能。shapes.Add(new Square());


您为什么要在一个6岁的问题上添加一个答案,而其他数十个答案却要被投票数百次?这里没有什么话要说的。
乔纳森·莱因哈特

@JonathonReinhart,是的,我是这么认为的,但是后来我想到了这个例子,它的解释方式将比其他例子更好地与某个身体相关。
Clint

2

这里有很多很好的答案,但我想从稍微不同的角度尝试。

您可能熟悉面向对象设计的SOLID原理。综上所述:

S-单一责任原则O-开放/封闭原则L-Liskov替代原则I-接口隔离原则D-依赖倒置原则

遵循SOLID原则有助于生成干净,充分分解,具有凝聚力和松散耦合的代码。鉴于:

“依赖管理是各种规模的软件面临的主要挑战”(Donald Knuth)

那么任何有助于依赖性管理的东西都是一个巨大的胜利。接口和“依赖倒置原则”确实有助于将代码与具体类上的依赖解耦,因此可以根据行为而不是实现来编写和推理代码。这有助于将代码分解为可以在运行时而不是编译时组成的组件,并且还意味着可以很容易地将这些组件插入和拔出,而无需更改其余代码。

接口特别有助于依赖倒置原则,该原则可以将代码组成服务集合,而每个服务都由接口描述。然后,可以通过将服务作为构造函数参数传递给服务,从而在运行时将它们“注入”到类中。如果您开始编写单元测试并使用测试驱动的开发,那么这项技术就变得至关重要。试试吧!您将快速了解接口如何帮助将代码分解为可管理的块,这些块可以单独进行测试。



1

Therese提出了很好的例子。

另一个,在使用switch语句的情况下,您不再需要每次想要以特定方式执行任务时都进行维护和切换。

在您的披萨示例中,如果要制作披萨,则只需要接口即可,每个披萨都从那里开始处理自己的逻辑。

这有助于减少耦合和圈复杂度。您仍然必须实现逻辑,但是从更广泛的角度来看,您将需要更少的精力。

然后,您可以为每个披萨跟踪特定于该披萨的信息。其他比萨饼没有什么关系,因为只有其他比萨饼需要知道。


1

考虑接口的最简单方法是识别继承的含义。如果CC类继承C类,则意味着:

  1. CC类可以使用C类的任何公共成员或受保护成员,就好像它们是自己的一样,因此仅需要实现父类中不存在的东西。
  2. 可以将对CC的引用传递或分配给希望引用C的例程或变量。

从某种意义上说,继承的这两个功能是独立的。尽管继承是同时应用的,但也可以不使用第一个而应用第二个。这很有用,因为允许对象从两个或多个不相关的类继承成员比使一种类型的事物可替换为多种类型复杂得多。

接口有点像抽象基类,但是有一个关键的区别:继承基类的对象不能继承任何其他类。相比之下,对象可以实现一个接口而不影响其继承任何所需类或实现任何其他接口的能力。

此功能的一个不错的功能(在.net框架中,IMHO未得到充分利用)使得它们可以声明性地指示对象可以执行的操作。例如,某些对象将需要数据源对象,它们可以通过索引从中检索内容(如List一样),但是它们不需要在此存储任何内容。其他例程将需要一个数据存储库对象,可以在其中不按索引存储内容(与Collection.Add一样),但是它们不需要回读任何内容。某些数据类型将允许按索引访问,但不允许写入。其他将允许写入,但不允许按索引访问。当然,有些会同时允许两者。

如果ReadableByIndex和Appendable是不相关的基类,则不可能定义可以同时传递给期望ReadableByIndex和期望Appendable的类型的类型。一个可以尝试通过从另一个派生ReadableByIndex或Appendable来减轻这种情况。派生类将必须同时为这两个目的提供公共成员,但是警告某些公共成员可能实际上没有工作。Microsoft的某些类和接口可以做到这一点,但是这很棘手。一种更清洁的方法是拥有用于不同目的的接口,然后让对象实现其实际可以执行的操作的接口。如果有一个接口IReadableByIndex和另一个接口IAppendable,


1

接口也可以菊花链方式创建另一个接口。这种实现多个接口的能力使开发人员具有在其类中添加功能而不必更改当前类功能的优势(SOLID原则)

O =“类应为扩展而开放,而应为修改而封闭”


1

对我来说,接口的优点/好处是它比抽象类更灵活。由于您只能继承1个抽象类,但可以实现多个接口,因此对在许多地方继承一个抽象类的系统所做的更改将成为问题。如果在100个地方继承,则更改需要将所有100个更改。但是,使用该接口,您可以将新更改放置在新接口中,并仅在需要的地方使用该接口(来自SOLID的接口序列)。此外,接口的内存使用情况似乎更少,因为接口示例中的对象在内存中仅使用一次,尽管实现接口的位置有多少。


1

接口用于松散耦合的方式来驱动一致性,这使其与紧密耦合的抽象类有所不同。这就是为什么它也通常定义为协定的原因。实现接口的所有类都遵守“规则/语法”由接口定义,并且其中没有具体元素。

我只给出下面的图形支持的示例。

想象一下,在工厂中有3种类型的机器。矩形机器,三角形机器和多边形机器。时间竞争激烈,您想简化操作员培训。您只想用一种启动和停止机器的方法来培训他们,有一个绿色的启动按钮和一个红色的停止按钮。因此,现在在3台不同的计算机上,您可以使用一致的方式启动和停止3种不同类型的计算机。现在想象这些计算机是类,并且这些类需要具有start和stop方法,您如何如何在这些类别之间提高一致性,这可能会大不相同?界面就是答案。

在此处输入图片说明

一个简单的示例可以帮助您形象化,可能会问为什么不使用抽象类?使用接口,对象不必直接相关或继承,您仍然可以在不同类之间实现一致性。

public interface IMachine
{
    bool Start();
    bool Stop();
}

public class Car : IMachine
{
    public bool Start()
    {
        Console.WriteLine("Car started");
        return true;
    }

    public bool Stop()
    {
        Console.WriteLine("Car stopped");
        return false;
    }
}

public class Tank : IMachine
{
    public bool Start()
    {
        Console.WriteLine("Tank started");
        return true;
    }

    public bool Stop()
    {
        Console.WriteLine("Tank stopped");
        return false;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var car = new Car();
        car.Start();
        car.Stop();

        var tank = new Tank();
        tank.Start();
        tank.Stop();

    }
}

1
class Program {
    static void Main(string[] args) {
        IMachine machine = new Machine();
        machine.Run();
        Console.ReadKey();
    }

}

class Machine : IMachine {
    private void Run() {
        Console.WriteLine("Running...");
    }
    void IMachine.Run() => Run();
}

interface IMachine
{
    void Run();
}

让我以不同的角度来描述这一点。让我们根据上面显示的示例创建一个故事;

程序,机器和IMachine是我们故事的参与者。程序想运行,但是它不具备这种能力,并且机器知道如何运行。Machine和IMachine是最好的朋友,但是Program与Machine并不是一成不变的。因此Program和IMachine达成协议,并决定IMachine将通过查看Machine(像反射器)来告诉Program如何运行。

并且Program借助IMachine学习如何运行。

接口提供通信和开发松散耦合的项目。

PS:我将具体课程的方法设为私有。我的目的是通过防止访问具体的类属性和方法来实现松散耦合,而只允许通过接口访问它们。(所以我明确定义了接口的方法)。


1

我知道我已经很晚了(将近九年),但是如果有人想要小小的解释,那么您可以这样做:

简而言之,当您知道对象可以做什么或我们将要在对象上实现的功能时,便可以使用接口。示例:插入,更新和删除。

interface ICRUD{
      void InsertData(); // will insert data
      void UpdateData(); // will update data
      void DeleteData(); // will delete data
}

重要说明:接口始终是公共的。

希望这可以帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.