ChatGPT解决这个技术问题 Extra ChatGPT

接口——有什么意义?

接口的原因确实让我难以理解。据我了解,这是一种解决 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
    }
}
我有一种感觉,这里有这个问题的重复,但它们似乎都只是解释了接口的合同部分,所以我不确定它们是否适用。
为了成为一个漂亮整洁的用户,我倾向于在发布内容之前先在各种论坛中寻找我的答案。不幸的是,他们中的大多数是在较晚的阶段开始的,其余的都没有帮助。我已经在为基本的“为什么要这样做?”而苦苦挣扎。因为在我看来没有必要把它复杂化。顺便提一句。感谢所有人的快速回答。我必须先消化它们,但我想我现在对它们的意义有了相当好的了解。似乎我总是从不同的角度看待它。非常感谢你的帮助。
接口也有助于建立类似 struct 类型的继承。
嗯,OP在问“据我了解,接口是C#中不存在的不存在的多继承的一种解决方法。(除此之外,在引用的教科书披萨示例中)我只想使用基类而不是接口”。然后大多数答案要么给出了一个可以由(抽象)基类实现的例子,要么给出了一个例子来说明多继承场景如何需要接口。这些答案都很好,但他们不只是重申OP已经知道的东西吗?难怪 OP 最终选择了没有示例的答案。哈哈

G
Govind Parmar

没有人真正简单地解释过接口是如何有用的,所以我要试一试(并从 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 都会自动准备每个比萨饼都基于其类型正确,即使该集合具有多种比萨饼类型。


好的答案,但也许修改它以澄清为什么在这种情况下接口比仅使用抽象类更好(对于这样一个简单的例子,抽象类可能确实更好?)
我不明白这如何回答这个问题。这个例子可以很容易地用一个基类和一个抽象方法来完成,这正是最初的问题所指出的。
接口的这种使用与使用抽象基类没有什么不同。有什么好处?
除非您有很多接口,否则接口并没有真正发挥作用。您只能从单个类继承,无论是否抽象,但您可以在单个类中实现任意数量的不同接口。突然间,你不需要门框里的门了;任何 IOpenable 都可以,无论是门、窗、信箱,你都可以。
这可以通过抽象 BasePizza 类完成,所以我想我不明白为什么我们应该使用接口而不是抽象类方法。这个答案在这里更好地解释了它:2个没有严格的is-a关系但具有共同行为的类:stackoverflow.com/questions/383947/…
S
Sizons

关键是接口代表了一个合约。任何实现类都必须具有的一组公共方法。从技术上讲,接口只控制语法,即存在哪些方法、它们得到什么参数以及返回什么。通常它们也封装语义,尽管这只是通过文档。

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

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

另请注意,通常意义上的接口不一定必须是 C# interface,它也可以是抽象类,甚至是普通类(如果所有子类都需要共享一些公共代码,这会派上用场 -然而,在大多数情况下,interface 就足够了)。


+1,尽管我不会说接口(在合同的意义上)可以是抽象类或普通类。
我要补充一点,你不能指望在几分钟内理解接口。如果你没有多年的面向对象编程经验,我认为理解接口是不合理的。您可以添加一些书籍链接。我建议:Dependency Injection in .NET 这确实是兔子洞的深度,而不仅仅是一个温和的介绍。
啊,问题是我对DI一无所知。但我想提问者的主要问题是为什么在 Python 中需要它们,而没有它们都可以工作。这就是我的答案中最大的一段试图提供的内容。我认为没有必要在这里深入研究使用接口的每个模式和实践。
好吧,那么您的问题就变成了:“当动态类型的语言更容易编程时,为什么要使用静态类型的语言?”。虽然我不是回答这个问题的专家,但我可以大胆地说性能是决定性的问题。当调用 python 对象时,进程必须在运行时确定该对象是否具有名为“Order”的方法,而如果调用 C# 对象,则已经确定它实现了该方法,并且可以拨打这样的地址。
@BolucPapuccuoglu:除此之外,如果知道 foo 实现 IWidget,则使用静态类型对象,然后看到对 foo.woozle() 的调用的程序员可以查看 IWidget 的文档并知道 该方法是什么应该这样做。程序员可能无法知道实际实现的代码来自何处,但任何遵守 IWidget 接口契约的类型都会以与该契约一致的方式实现 foo。相比之下,在动态语言中,foo.woozle() 的含义没有明确的参考点。
P
Paddy

对我来说,刚开始时,只有当您不再将它们视为使您的代码更容易/更快编写的东西时,这些点才变得清晰——这不是他们的目的。它们有多种用途:

(这将失去比萨的类比,因为它的使用并不容易想象)

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

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

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

// 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);
}

两周后,营销部门决定你也需要兽人,因为他们在推特上读到了兽人,所以你必须做类似的事情:

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 = _factory.GetCreature(creatureType);

    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(例如,如果具体实现需要 DB 访问,我们不希望我们的单元测试依赖于它)并轻松测试控制器中的代码.

D:还有其他用途,例如,您有两个项目 A 和 B,由于“遗留”原因没有很好的结构,并且 A 引用了 B。

然后,您会在 B 中找到需要调用 A 中已有方法的功能。当您获得循环引用时,您无法使用具体实现来执行此操作。

您可以在 B 中声明一个接口,然后 A 中的类实现该接口。您在 B 中的方法可以毫无问题地传递实现接口的类的实例,即使具体对象属于 A 中的类型。


当你找到一个试图说出你曾经是什么的答案时,这不是很烦人吗?但做得更好 - stackoverflow.com/a/93998/117215
好消息是它现在是一个死页,而你的不是:)。好的例子!
你的 C 讨论让我有点失望。但是,我喜欢您的 A 和 B 讨论,因为它们都从本质上解释了如何使用接口来提供跨多个类的通用功能。对我来说仍然模糊的接口领域是如何使用接口来实现更松散的耦合。也许这就是您的 C 讨论所要解决的问题?如果是这样,我想我需要一个更详细的例子:)
在我看来,在您的示例中, ICreature 更适合作为 Troll 和 Orc 的抽象基类(Creature?)。然后生物之间的任何共同逻辑都可以在那里实现。这意味着您根本不需要 ICreature 界面...
@Skarsnik - 确实,这就是本说明的内容:“从这个角度来看,需要注意的重要一点是,您也可以轻松使用抽象生物类,从这个角度来看,它具有相同的影响。”
A
Aleksandar Toplek

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

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();
    }
}

我不得不一直向下滚动才能找到一个真正回答“为什么不使用抽象类而不是接口?”的答案。看起来它真的是只处理 c# 缺乏多重继承。
是的,这对我来说是最好的解释。唯一一个清楚地解释了为什么接口比抽象类更有用。 (即:披萨是 MenuItem 并且也是 Food,而对于抽象类,它只能是一个或另一个,但不能同时是两者)
S
Shamim Hafiz - MSFT

以下是重新解释的示例:

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
    }
}

B
BenKoshy

类比简单说明

无接口(例1):

https://i.stack.imgur.com/kWjN4.jpg

无接口(示例 2):

https://i.stack.imgur.com/neCmZ.jpg

带接口:

https://i.stack.imgur.com/mlbZf.jpg

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

类比:所以我是建筑工地的工头。我不知道哪个商人会走进来。但我告诉他们该怎么做。

如果是木匠,我会说:搭建木制脚手架。如果是水管工,我会说:设置管道 如果是 BJP 政府官僚,我会说,三袋装满现金,先生。

上述方法的问题在于我必须:(i)知道谁在走进那扇门,并且根据是谁,我必须告诉他们该怎么做。这通常会使代码更难维护或更容易出错。

知道该做什么的含义:

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

其次,你不必不断地问:你是谁?好吧,这样做……你是谁?好的,这样做......多态性 - 它干燥该代码,并且在某些情况下非常有效:

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

解决方案

想象一个场景,无论谁走进门,我都可以说:“Work()”,他们会做他们擅长的工作:水管工处理管道,电工处理电线,还有一个官僚可以专攻贿赂并为其他人做双重工作。

这种方法的好处是:(i)我不需要确切地知道谁从那扇门走进来——我只需要知道他们将是一种传统,他们可以工作,其次, (ii) 我不需要知道任何关于该特定交易的信息。技师会处理的。

所以代替这个:

if(electrician) then  electrician.FixCablesAndElectricity() 

if(plumber) then plumber.IncreaseWaterPressureAndFixLeaks() 

if(keralaCustoms) then keralaCustoms.askForBribes() 

我可以做这样的事情:

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() 的含义。其次,如果一个新型建筑工人来到工作现场,那么工头不需要了解任何有关该行业的知识 - 工头关心的是建筑工人(例如焊工、玻璃工、瓷砖工等)是否可以完成一些工作()。

概括

界面允许您让该人完成分配给他们的工作,而无需您确切了解他们是谁或他们可以做什么的细节。这使您可以轻松地添加新类型(交易),而无需更改现有代码(从技术上讲,您确实会稍微更改它),这是 OOP 方法与功能更强大的编程方法相比的真正好处。

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


B
BrokenGlass

在没有 duck typing 的情况下,您可以在 Python 中使用它,C# 依赖于接口来提供抽象。如果一个类的依赖项都是具体类型,则不能传入任何其他类型——使用接口可以传入实现该接口的任何类型。


+1 没错,如果你有鸭子打字,你就不需要接口。但它们确实强制执行了更强的类型安全性。
b
bevacqua

Pizza 的例子很糟糕,因为您应该使用一个处理排序的抽象类,并且比萨应该只覆盖比萨类型,例如。

当你有一个共享属性,但你的类从不同的地方继承,或者你没有任何可以使用的公共代码时,你使用接口。例如,这是使用的东西,可以被处置IDisposable,你知道它会被处置,你只是不知道它被处置时会发生什么。

接口只是一个契约,它告诉你一个对象可以做的一些事情、什么参数和期望的返回类型。


L
Lasse V. Karlsen

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

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

假设您从事创建自定义控件的业务。您想构建新的按钮、文本框、列表视图、网格、诸如此类的东西,并且您希望它们都具有您的控件集独有的某些功能。

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

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

相反,您将从 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”然后强制转换,把它放在一边。
叹息我知道,但这是偶然的。
T
Tez Wingfield

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

一个类可以实现多个接口。它只是定义了类必须具有的特性。实现一系列接口意味着一个类可以在不同的地方实现多种功能。接口可以在比类或调用者更大的范围内定义。这意味着您可以分离功能,分离项目依赖关系,并将功能保留在一个项目或类中,并在其他地方实现。

2 的一个含义是您可以更改正在使用的类,只需要它实现适当的接口。


C
Claus Jørgensen

考虑你不能在 C# 中使用多重继承,然后再看看你的问题。


J
Jay Edwards

我在此页面上搜索了“组合”一词,但一次都没有看到。除了上述答案之外,这个答案还有很多。

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

这个由 Derek Banas 编写的出色的“装饰器模式”教程(有趣的是,它也以比萨饼为例)是一个值得说明的例子:

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


D
Dean Kuga

接口 = 合同,用于 loose coupling(参见 GRASP)。


J
James Black

如果我正在使用 API 来绘制形状,我可能想要使用 DirectX 或图形调用,或者 OpenGL。所以,我将创建一个接口,它将我的实现从你调用的内容中抽象出来。

所以你调用了一个工厂方法:MyInterface i = MyGraphics.getInstance()。然后,您有一个合同,因此您知道在 MyInterface 中可以预期哪些功能。因此,您可以调用 i.drawRectanglei.drawCube 并知道如果将一个库换成另一个库,则支持这些函数。

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

因此,您可能有一个可以导出的通用加密库,另一个仅出售给美国公司的加密库,不同之处在于您更改了配置文件,而程序的其余部分则没有改变了。

这在 .NET 中大量用于集合,因为您应该只使用 List 变量,而不必担心它是 ArrayList 还是 LinkedList。

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

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


h
hakan

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

public class Car { ... }

public class Tree { ... }

您想为这两个类添加可刻录功能。但是每个班级都有自己的燃烧方式。所以你只需制作;

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

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

在这样的例子中困扰我的问题是:为什么这有帮助?我现在可以将 IBurnable 类型的参数传递给一个方法,并且所有具有 IBurnable 接口的类都可以处理吗?与我发现的 Pizza 示例相同。你可以这样做很好,但我没有看到这样的好处。你能否扩展这个例子(因为我现在真的很厚)或者举一个不适用的例子(再次,因为我现在觉得很厚)。非常感谢。
同意。接口=“可以做”。类/抽象类 = “是 A”
d
default locale

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

在您的示例中,创建了接口,因为任何 IS A Pizza(即实现 Pizza 接口)都保证已实现

public void Order();

在你提到的代码之后,你可能会有这样的事情:

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

这样您就使用了多态性,您所关心的只是您的对象响应 order()。


S
ShaunnyBwoy

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

让我们将披萨类比扩展为完整的 3 道菜餐。对于所有食物类别,我们仍将拥有核心 Prepare() 界面,但我们还将为课程选择(开胃菜、主菜、甜点)和不同的食物类型(咸味/甜味、素食/非-素食,无麸质等)。

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

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


这个答案值得加分!接口在设计模式中使用时会发光,例如状态模式。有关其他信息,请参阅 plus.google.com/+ZoranHorvat-Programming
s
sventevit

您将获得接口,当您需要它们时 :) 您可以学习示例,但您需要 Aha!效果才能真正得到它们。

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


d
dharmatech

这是具有矩形形状的对象的界面:

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));

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


C
Chris

接口定义了特定功能的提供者和相应的消费者之间的契约。它将实现与合同(接口)分离。你应该看看面向对象的体系结构和设计。您可能想从维基百科开始:http://en.wikipedia.org/wiki/Interface_(computing)


C
Community

什么 ?

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

C# 中,接口名称的约定是通过前缀“I”来定义的,因此如果您想要一个称为形状的接口,您可以将其声明为 IShapes

现在为什么?

提高代码的可重用性

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

使用接口,您可以将不同的实现与合同隔离开来

现场情景第 1 天

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

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 天

如果您被要求添加 SquareRectangle,您所要做的就是在 class Square: IShapes 中为其创建实现,然后在 Main 中添加到列表 shapes.Add(new Square());


你为什么要为一个 6 岁的问题添加一个答案,而其他答案有几十个,被投票了数百次?这里没有什么是尚未说过的。
@JonathonReinhart,是的,我是这么认为的,但后来我在想这个例子以及它的解释方式将有助于更好地与某些身体相关联。
T
Tim Long

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

您可能熟悉面向对象设计的 SOLID 原则。总之:

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

遵循 SOLID 原则有助于生成干净、分解良好、内聚和松散耦合的代码。鉴于:

“依赖管理是软件在各个规模的关键挑战”(Donald Knuth)

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

接口特别有助于依赖倒置原则,其中代码可以组件化为服务集合,每个服务由接口描述。然后可以在运行时将服务“注入”到类中,方法是将它们作为构造函数参数传入。如果您开始编写单元测试并使用测试驱动开发,这种技术真的变得至关重要。试试看!您将很快了解接口如何帮助将代码分解为可单独进行单独测试的可管理块。


n
noobprogrammer

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

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

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

重要提示:接口始终是公开的。

希望这可以帮助。


A
Abhishek Tewari

这么多答案!尽我所能。呵呵。

所以首先,是的,您可以在这里使用具体的基类和派生类。在这种情况下,您必须为基类中的 Prepare 方法执行空的或无用的实现,同时使该方法成为 virtual,然后派生类将覆盖此 Prepare 方法为自己。在这种情况下,Prepare 在 Base 类中的实现是没有用的。

您选择使用 Interface 的原因是您必须定义一个合同,而不是一个实现

有一个 IPizza 类型,它为Prepare 提供功能。这是合同。它是如何准备的就是实施,它不是你的监视。它是各种 Pizza 实现的责任。 interfaceabstract 类在此优先于具体的基类,因为您必须创建一个抽象,即 Prepare 方法。您不能在具体基类中创建抽象方法。

现在你可以说,为什么不使用抽象类呢?

因此,当您需要实现 100% 抽象时,您需要使用 Interface。但是当您需要一些抽象和具体实现时,请使用 abstract 类。它的意思是。

示例:假设您所有的比萨饼都有底料,并且底料的制备过程相同。但是,所有比萨饼类型和配料都会有所不同。在这种情况下,您可以使用抽象方法 Prepare 和具体方法 PreparePizzaBase 创建一个抽象类。

public abstract class Pizza{
    // concrete method which is common to all pizzas.
    public PizzaBase PreparePizzaBase(){
        // code for pizza base preparation.
    }
    public abstract void Prepare();
}

public class DeluxePizza: Pizza{
    public void Prepare(){
        var base=PreparePizzaBase();
        // prepare deluxe pizza on pizza base.
    }
}

S
Samir Adel

接口的主要目的是它在你和任何其他实现该接口的类之间建立一个契约,这使得你的代码解耦并允许可扩展性。


t
tam

Therese是问非常好的例子。

另外,在 switch 语句的情况下,您不再需要在每次希望 rio 以特定方式执行任务时维护和切换。

在您的比萨示例中,如果要制作比萨,界面就是您所需要的,每个比萨从那里处理自己的逻辑。

这有助于减少耦合和圈复杂度。您仍然必须实现逻辑,但在更广泛的情况下您需要跟踪的会更少。

对于每个披萨,您可以跟踪特定于该披萨的信息。其他披萨有什么并不重要,因为只有其他披萨需要知道。


s
supercat

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

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

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

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

一个很好的特性(在 .net 框架中未充分利用,恕我直言)是它们可以以声明方式指示对象可以做的事情。例如,某些对象需要数据源对象,它们可以通过索引从中检索事物(就像使用 List 一样),但它们不需要在那里存储任何东西。其他例程将需要一个数据存储对象,它们可以在其中存储内容而不是按索引(如 Collection.Add),但它们不需要读回任何内容。某些数据类型将允许按索引访问,但不允许写入;其他人将允许写入,但不允许按索引访问。当然,有些人会允许两者。

如果 ReadableByIndex 和 Appendable 是不相关的基类,就不可能定义一个既可以传递给期望 ReadableByIndex 的事物又传递给期望 Appendable 的事物的类型。可以尝试通过从另一个派生 ReadableByIndex 或 Appendable 来缓解这种情况;派生类必须为这两个目的提供公共成员,但警告某些公共成员可能实际上不起作用。 Microsoft 的一些类和接口可以做到这一点,但这很麻烦。一种更简洁的方法是为不同目的提供接口,然后让对象为它们实际可以做的事情实现接口。如果有一个接口 IReadableByIndex 和另一个接口 IAppendable,则可以执行其中一个或另一个的类可以为它们可以执行的操作实现适当的接口。


佚名

接口也可以菊花链式创建另一个接口。这种实现多个接口的能力为开发人员提供了向其类添加功能而无需更改当前类功能的优势(SOLID 原则)

O = "类应该对扩展开放但对修改关闭"


S
ScottS

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


C
C.Poh

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

我将仅举一个下图支持的示例。

想象在工厂里有 3 种类型的机器。矩形机器、三角形机器和多边形机器。时代竞争激烈,您希望简化操作员培训。您只想用一种启动和停止机器的方法来培训他们,这样您有一个绿色的启动按钮和红色的停止按钮。所以现在在 3 台不同的机器上,你有一个一致的方式来启动和停止 3 种不同类型的机器。现在想象这些机器是类,这些类需要有启动和停止方法,你如何将推动这些可能非常不同的类的一致性?界面就是答案。

https://i.stack.imgur.com/bTQW3.jpg

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

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();

    }
}