在 Ruby 中,与 C# 的接口相当的东西是什么?

23

我目前正在学习Ruby,想更好地了解它在封装和契约方面提供了什么。

在C#中,可以使用接口定义契约。实现接口的类必须通过为每个方法和属性(以及可能的其他内容)提供实现来履行契约条款。实现接口的各个类可以在定义合同的方法范围内执行其所需操作,只要接受相同类型的参数并返回相同类型的结果。

在Ruby中是否有一种方式来强制执行这种类型的约束呢?

谢谢

C#中我的意思的一个简单示例:

interface IConsole
{
    int MaxControllers {get;}
    void PlayGame(IGame game);
}

class Xbox360 : IConsole
{
   public int MaxControllers
   {
      get { return 4; }
   }

   public void PlayGame(IGame game)
   {
       InsertDisc(game);
       NavigateToMenuItem();
       Click();
   }
}

class NES : IConsole
{
    public int MaxControllers
    {
        get { return 2; }
    }

   public void PlayGame(IGame game)
   {
       InsertCartridge(game);
       TurnOn();
   }
}

2
在一门语言中没有接口似乎对我来说是一个可怕的设计决策。 - Chris Marisic
12
Ruby(以及所有语言)接口——它们只是不是一种语言特性,因此编译器不会对其进行强制。同样,我可以说关于C/C++/Java/C#等语言,"不在语言中包含整数类型似乎是一个可怕的设计决策"——如果这很重要,你可以使用工具来构建它们。但大多数时候,我们只是在极小的整数子集上使用模算术运算,我们的软件通常也能正常运行。没有人会因此而死亡。 - Ken
我很确定你在插入卡带之前忘记吹一下它了... - JJS
6个回答

25

Ruby语言中没有接口,因为Ruby是一种动态类型语言。接口通常用于使不同的类可互换,而不会破坏类型安全性。只要某个类像控制台那样行事(在C#中意味着实现IConsole接口),你的代码就可以与每个这样的控制台一起使用。“鸭子类型”是一个关键字,您可以使用它来跟上处理此类问题的动态语言的方式。

另外,您可以编写单元测试以验证代码的行为。每个对象都有一个respond_to?方法,您可以在断言中使用它。


1
不可否认,能够说[respond_to? <some interface>]可能是有用的,其中<some interface>表示一些操作的集合。对于一个期望输入对象列表并支持预期操作(添加、删除、查找、长度等)的方法,它可以验证其输入是否确实满足这些要求,而无需检查每个操作。所讨论的对象不需要是任何类型的对象,只需要支持“该接口”声明的操作即可。 - RHSeeger
1
@myself - 看起来完全可以定义一个respond_to?类型的命令,该命令针对一个“接口”(方法列表)进行验证,只需迭代“接口”中的方法,并在满足所有方法时返回true。 - RHSeeger
7
动态类型与此无关。看看 PHP,它是动态类型的并且有接口。为什么 Ruby 的开发者不能承认语言中存在一些缺失的功能呢?另一个缺失的功能是类型提示。 - ChocoDeveloper
@ChocoDeveloper 什么?如果你真的需要/想要它们,你可以直接实现你想要的功能。Ruby允许你这样做,而PHP则不行。 - Sancarn

14

像其他语言一样,Ruby也有接口。

请注意,你必须小心,不要混淆接口的概念,它是一个单位职责、保证和协议的抽象规范,与interface的概念相混淆。在Java、C#和VB.NET编程语言中,interface是一个关键字。在Ruby中,我们经常使用前者,但后者根本不存在。

非常重要的是要区分这两个概念。重要的是接口,而不是interfaceinterface几乎没有任何有用的信息。 Java中的标记接口最好地证明了这一点,这些接口根本没有成员:只需查看java.io.Serializablejava.lang.Cloneable; 这两个interface意味着非常不同的事情,但它们具有完全相同的签名。

因此,如果两个具有不同含义的interface具有相同的签名,则interface到底保证了什么?

另一个很好的例子:

interface ICollection<T>: IEnumerable<T>, IEnumerable
{
    void Add(T item);
}

什么是System.Collections.Generic.ICollection<T>.Add接口?

  • 集合的长度不会减少
  • 之前在集合中的所有项目仍然存在
  • item在集合中

那么这些内容中哪一个实际上出现在interface中呢?没有!interface中没有任何内容表明Add方法必须添加,它也可能从集合中删除元素。

这是该interface的完全有效实现:

class MyCollection<T>: ICollection<T>
{
    void Add(T item)
    {
        Remove(item);
    }
}

另一个例子:在 java.util.Set<E> 中,它实际上在哪里说它是一个集合?没有!或者更准确地说,在文档中。用英语写的。
在几乎所有的 Java 和 .NET 接口中,所有相关信息都在文档中,而不是在类型中。那么,如果这些类型无论如何都不能告诉你任何有趣的东西,为什么还要保留它们呢?为什么不只使用文档呢?这正是 Ruby 所做的。
请注意,在其他一些语言中,“接口”可以以有意义的方式描述。然而,这些语言通常不将描述“接口”的结构称为“interface”,而是称之为“type”。在依赖类型编程语言中,例如,您可以表达“sort”函数返回与原始长度相同的集合、原始集合中的每个元素也在排序后的集合中出现,并且没有更大的元素出现在较小的元素之前等属性。
因此,简而言之:Ruby 没有与 Java 的 “interface” 相当的东西。但它确实有一个与 Java 的 “Interface” 相当的东西,而且它和 Java 中的一样:文档。
此外,就像在 Java 中一样,验收测试也可以用来指定接口。
特别是在 Ruby 中,对象的接口是由它能做什么来确定的,而不是由它是什么类或者混入了什么模块来确定的。任何具有“<<”方法的对象都可以追加。这在单元测试中非常有用,您可以简单地传递一个“Array”或“String”,而不是一个更复杂的“Logger”,即使“Array”和“Logger”除了它们都有一个名为“<<”的方法之外,没有共享明确的接口。
另一个例子是 StringIO,它实现了与“IO”相同的接口,因此实现了“File”的大部分接口,但除了“Object”之外没有共同的祖先。

11
是的,文档可以描述类应该做什么,但这并不能保证任何东西。通过接口实现,您可以确信Add(item)方法至少存在。无论它是否执行您期望它执行的功能都不重要。您知道如果调用Add(item),它将出现并且它将执行某些操作。以上内容听起来像是一个松散类型的宗教争论。 - Ian Suttle
1
@Ian 如果你的需求是通过接口确保一个对象具有Add方法,那么respond_to?是实现这一目标的Ruby方式。 - Tom Lianza
1
我同意Ian的观点,接口(如Java等语言中所采用的)的目的是为类提供一个方便的契约。当一个类声明它实现了一个接口时,我可以确定会有一个名为“Add”的方法,我很可能会用它来添加东西。是的,文档也会告诉我这一点,但这不是重点。我可以有丑陋的代码,没有任何行间距,并说“阅读文档”以了解这个类的作用。话虽如此,Tom关于使用respond_to?的评论是正确的。但这并不方便。 - KG -
为什么使用respond_to不方便?您可以轻松编写单元测试,仅测试特定类的接口。 - webpapaya
@IanSuttle 实际上,情况比这更好,因为“已实现”的方法的存在意味着有义务正确地实现它:也就是说,如果它不能按预期工作,则可以客观地描述该类型为“错误”。这正是我对结构类型持低看法的原因,因为意图变得隐含而不是明确。我认为这个答案完全忽略了类型接口的好处。 - VisualMelon

5

接口通常在静态类型的面向对象语言中引入,以弥补多继承的缺乏。换句话说,它们更像是必要的恶,而不是实用的东西。

而另一方面,Ruby:

  1. 是一种具有“鸭子类型”的动态类型语言,因此如果您想在两个对象上调用方法foo,它们既不需要继承相同的祖先类,也不需要实现相同的接口。
  2. 通过混合(mixin)的概念支持多继承,在这里也不需要接口。

1
组合优于继承,这是正确的道路。 - redigaffi

4
Ruby并没有接口和契约的概念,这些通常更多地存在于静态世界而非动态世界。
如果你真的需要,有一个叫做Handshake的宝石可以实现非正式的契约。

0
Jorg提出了一个很好的观点,Ruby有接口,只是没有关键字。在阅读了一些回复后,我认为这是动态语言的一个负面因素。与其通过语言强制执行接口,你必须创建单元测试来捕捉未实现的方法而不是让编译器去处理。这也使得理解方法更加困难,因为你必须找到一个对象并尝试调用它。
以一个例子来说明:
def my_func(options)
  ...
end

如果你看这个函数,你不知道options是什么,它应该调用哪些方法或属性,除非去寻找单元测试、其他调用它的地方,甚至查看方法。更糟糕的是,这个方法可能根本不使用这些选项,而是将其传递给进一步的方法。为什么要编写单元测试,当这应该被编译器捕捉到呢?问题在于,在动态语言中,必须以不同的方式编写代码来表达这种缺点。

然而,动态编程语言有一个好处,那就是编写代码的速度很快。我不需要编写任何接口声明,稍后可以添加新的方法和参数,而无需去接口中公开它们。这种权衡是速度与维护之间的平衡。


请查看此链接,它将对提高您的质量有帮助。 - Willie Cheng

0

接口没有功能,模块有。 - Marc-André Lafortune
一个接口不是一个模块。它们都可以被组合,但这就是它们的共同之处了。 - mlibby

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