3

我试图更好地理解 C# 和其他 OOP 语言中的接口。接口有什么作用?为什么需要它?我知道 c# 和 Java 不允许多重继承。大多数书籍都说接口是绕过单一继承限制并允许不同类具有共同功能的一种方法。接口只是定义方法并强制类实现它们。为什么不让类自己定义和实现方法而不处理接口?例如:

4: using System;
5:
6: public interface IShape
7: {
8:    double Area();
9:    double Circumference();
10:   int Sides();
11: }
12:
13: public class Circle : IShape
14: {
15:    public int x;
16:    public int y;
17:    public double radius;
18:    private const float PI = 3.14159F;
19:
20: public double Area()
21: {
22:    double theArea;
23:    theArea = PI * radius * radius;
24:    return theArea;
25: }
.
.
.

为什么 Circle 类不能自己定义和实现 Area()、Circumference() 和 Sides() 方法?如果正方形类继承了 IShape,则必须未实现 Circumference() 方法。我对接口的理解还差得远吗?

4

10 回答 10

5

当你想说“我不在乎你是怎么做的,但这是你需要完成的事情”时,接口是为你准备的。有关更多说明,请参阅此链接

于 2012-09-10T04:55:48.720 回答
3

C# 中的对象可能具有实际上是不同类别组合的函数;一个典型的例子是教师例子:

多重继承

在这个例子中,老师有一个人的特征(例如眼睛颜色)(尽管我有一些老师可能会破坏这个例子)和一个雇员的特征(例如薪水)

C# 不允许多重继承,因此我们寻找使用接口组合的想法。经常这样描述:

继承意味着如果 Cat 继承自 Animal 则 Cat “is-a” Animal

组合意味着如果 Cat 实现了 Noise,那么 Cat “has-a” Noise

为什么这种区别很重要?好吧,想象一下我们的猫。我们实际上可以让 Cat 继承自 Feline,而后者又继承自 Animal。有一天,我们决定要支持其他类型的 Animal,所以我们决定修改 Animal,但随后我们意识到我们将把这些更改推向所有其他子类型。我们的设计和层次结构突然变得相当复杂,如果我们一开始就没有做好,我们必须重新设计我们所有的子类。

规则是Prefer Composition over Inheritance,但请注意 Prefer 一词的使用 - 这很重要,因为这并不意味着我们应该只使用组合,而是我们的设计应该考虑什么时候继承有用,什么时候组合有用也。它还提醒我们,在基类中保留所有可能的方法是一个坏主意。

我喜欢你的形状示例。考虑:

小型分拣机

我们的 ShapeSorter 可能有这样的方法:

public bool Sort(Shape shape)
{
  foreach(Hole hole in Holes)
  {
     if(Hole.Type == HoleType.Circular && shape is Circle)
     {
         return true;
     }
     if(Hole.Type == HoleType.Square && shape is Square)
     {
         return true;
     }
     if(Hole.Type == HoleType.Triangular && shape is Triangle)
     {
         return true;
     }
  }

  return false;

}

或者,我们可以做一些轻微的控制反转:

public bool Sort(Shape shape, Hole hole)
{
   return hole.Accepts(shape); //We  end up pushing the `if` code into Hole
}  

或者它的一些变体。我们已经在编写大量代码,这些代码依赖于我们知道确切的 Shape 类型。想象一下,当您拥有其中之一时,维护起来会变得多么乏味:

大型分拣机

所以,相反,我们自己思考——有没有一种更通用的方式可以通过将问题提炼到相关属性来描述我们的问题?

你称它为 IShape:

public interface IShape{
  double Area {get;}
  double Perimeter { get; } //Prefer Perimeter over circumference as more applicable to other shapes
  int Sides { get; }
  HoleType ShapeType { get; }
}

我们的 Sort 方法会变成:

public Hole Sort(IShape shape)
{
   foreach(Hole hole in Holes)
   {
      if(hole.HoleType == shape.ShapeType && hole.Area >= shape.Area)
      {
         return hole;
      }
   }
   return null;
}

这看起来更整洁,但实际上并不是任何无法通过 Shape 直接完成的事情。

真相是没有真相。最常见的方法将涉及使用继承和组合,因为现实世界中的许多事物都将是一种类型,并且还将具有由接口最好描述的其他属性。要避免的最重要的事情是在基类中保留所有可能的方法,并使用大量的 if 语句来确定派生类型可以做什么和不能做什么——这是很多难以维护的代码。此外,在基类中放置过多的功能可能会导致您的代码具体化——由于所有潜在的副作用,您以后不会想要修改内容。

于 2012-09-10T23:11:16.557 回答
1

在这种情况下,是的,您绝对可以做到。但是有些情况mouse listeners是接口。其中声明了一些虚拟方法。稍后在您的程序中作为开发人员,您将覆盖这些方法并实现您自己的自定义逻辑。

来源:MouseListener 接口定义了五个方法:mouseClicked、mouseEntered、mouseExited、mousePressed 和 mouseReleased。它们都是 void 方法,并以单个 MouseEvent 对象作为参数。请记住,您必须定义所有这五个方法;否则,您的类必须声明为抽象类。MouseListener 接口不跟踪诸如鼠标显式移动之类的事件。

于 2012-09-10T04:58:08.847 回答
1

为什么 Circle 类不能自己定义和实现 Area()、Circumference() 和 Sides() 方法?

它可以,但它将独立于界面。接口为您提供的主要优势之一是“运行时绑定”。

对于您当前的示例,您只有一个类Circle,假设您创建了另一个类Rectangle并且您的两个类都实现了IShape。稍后在运行时的代码中,您可以创建对象Circle并将Rectangle其放在实例类型变量中。

IShape 形状;

if(User needs a Circle) // 只是一个显示运行时绑定的示例测试。形状 = 新圆();否则形状=新矩形();

现在当你做

double area = shape.Area();

您将获得形状的面积。在运行时,您将不知道 areaRectangleCircle. 但是接口对象将调用它持有引用的形状的实现方法。

于 2012-09-10T05:01:58.583 回答
1

接口是对象实现一组特定方法或属性的保证。是的,你的形状类可以在没有接口的情况下实现这些方法,但是没有办法告诉其他代码它实现了它们,你可能会不小心忘记实现一个,或者更改它的参数。

接口是一个定义。它允许其他代码知道定义是什么,而不知道实现是什么。换句话说,不是圆不能实现方法,而是圆实现了接口就保证实现了方法。

例如,您可能有一个名为 IWriter 的接口。IWriter 有一种方法:

public interface IWriter
{
    void Write(string s);
}

请注意这是多么通用。它没有说它在写什么,或者它是如何写的……只是它写的。

然后,您可以拥有实现 IWriter 的具体方法,例如 MemoryWriter、ConsoleWriter、PrinterWriter、HTMLWriter 等......每个都以不同的方式写入,但它们都实现了相同的简单接口,只有一个方法。

public class ConsoleWriter : IWriter
{
    public void Write(string s) {
         Console.WriteLine(s);
    }
}

public class MemoryWriter : IWriter
{
    public void Write(string s) {
        // code to create a memory object and write to it
    }
}

您可以使用基类完成同样的事情,但这将依赖于实际实现,并且会创建您可能不想要的依赖项。接口将实现与定义分离。

关于没有周长的正方形和没有边的圆形......这只是意味着你的 IShape 定义设计得不好。您正在尝试使对象适合可能不适用的单个定义。

于 2012-09-10T05:02:41.267 回答
1

接口也称为契约,将在两个或多个类之间的交互过程中使用。

在这种情况下,当一个类被称为正在实现IShape时,调用者将知道该类具有在被调用的合约中定义的所有方法IShape。调用者不会担心类是否为Square// RectangleCircle

回答你的问题

如果 square 类继承IShape,则Circumference()必须未实现方法。

您需要设计您的界面,使其足够通用。在这种情况下,接口应该有一个名为 Perimeter 而不是圆周的方法。

对于边属性,我认为圆应该返回一个预定义的常量,例如 int.MaxValue 来表示无穷大。

要了解接口的好处,您需要了解调用者将如何调用这些方法。

例如

public double DisplayArea(IShape shape)
{
    Console.WriteLine(shape.Area().ToString());
}

您可以通过调用上述方法

//Code to create a Circle
this.DisplayArea(circle);

//Code to create a Square
this.DisplayArea(square);

这是可能的,因为DisplayArea知道任何类型的对象IShape都会有一个调用的方法,该方法Area将返回一个double. 所以,它根本不用担心类名。将来,如果您实现Ellipse类并使其实现接口IShape,该DisplayArea方法将无需任何修改即可工作。

于 2012-09-10T05:04:00.107 回答
1

当然,您可以在不实现接口的情况下自行向类添加方法。但是界面可以确保您在不随心所欲的情况下进行操作。这类似于义务减去仲裁。如前所述,接口是必须履行的合同,而你如何履行则取决于你。

于 2012-09-10T05:21:52.380 回答
1

在现实世界中,您将不仅使用圆圈。您将需要其他形状,例如矩形/正方形等。接口允许您定义一些您将在应用程序中使用的通用合同,然后您可以轻松添加新形状。添加新形状时,您不会被迫大幅更改代码 - 您只需实现 IShape 接口即可完成(嗯,需要进行一些更改,但不会太多)。

于 2012-09-10T08:44:09.903 回答
0

实现接口和继承之间的区别在于,继承指定了“是”行为,如“A 'is' also B'(A Cat 'Is' also an animal)。接口实现更多的“Has”(或确实)行为。(一只鸟“有”翅膀,而飞机也“有”翅膀)。这是粗略的简化,但你明白了。

这就是为什么在这些语言中您只能从一个对象继承但实现许多接口。界面提供了更多的 Mixin 功能(它可以让你混合各种东西,就像艺术家混合颜料一样)。

继承或接口都不比另一个好。设计对象结构以便使用正确的构造确实取决于您。

例如。您不会仅仅因为它们都可以飞行而从 Bird 类继承您的 Airplane 类。是的,飞机和鸟有很多共同点,但飞机不是鸟。(已经说过语义可能在您的特定应用程序中有所不同,您可能不关心做出这种区分,或者您可能永远不必在您的应用程序中处理飞机)。在这里你会有一个界面,上面写着类似

public Interface ICanFly
{
    void Fly();
    void Land();
}

这样,飞机和鸟类都可以实现 ICanFly 接口,而无需假装飞机是鸟。您可能有一些其他类只关心某物是否会飞,而不管它是人造的还是天然的。假设您有一个带有 Track 方法的 Radar 类

public class Radar
{
    // This method doesn't care whether the object is a bird or plane.
    public void Track(ICanFly flyingObject)
    {

    }
}

你可以有其他专门适用于动物的课程

public class MigrationTracker
{
    // I only deal with birds since aeroplanes don't migrate.
    public void Track(Bird birdsToTrack)
    {

    }
}

如您所见,接口和继承为您设计类的方式提供了不同的选择,但如何明智地使用它们取决于您。例如,本地动物园的应用程序可能永远不必处理飞机,在这种情况下,可以将飞行功能添加到鸟类类中并继承,而不是与接口混合。

于 2012-09-10T05:33:39.190 回答
0

为什么不让类自己定义和实现方法而不处理接口?

想象一下 Circle 类,方法名是AreaOfCircle

矩形方法名称是“AreaOfRectangle”

Square没有像“Area”这样的方法

除了上面的讨论之外,方法名称没有统一性。不能保证所有这些类都实现了重要的方法。

验证所有类的重要方法需要付出很多努力。如果实现了接口,那么只要找到接口的引用,就可以保证那些同名的方法必须实现。

所以接口契约是有保证的。此外,跨继承类实现任何新的接口契约也很容易。

如果正方形类继承了 IShape,则必须未实现 Circumference() 方法。

将所有签名/合同放在单个界面中是错误的做法。当我知道正方形和矩形等多边形无法实现 Circumference() 时。我会将 Circumference() 放在不同的界面中。

所以界面帮助你组织代码,因此它有助于实现各种设计模式。

于 2018-05-28T09:08:46.230 回答