为什么Go语言中方法接收类型不能是接口?

6
Go方法声明文档中可以看到:
接收器类型必须是T或*T的形式,其中T是类型名称。T被称为接收器基础类型或仅为基础类型。基础类型不能是指针或接口类型,并且必须在与方法相同的包中声明。
有人能给我一些关于这个问题的见解吗?是否有其他(静态类型)语言允许这样做?我真的想在一个接口上定义方法,以便将给定接口类型的任何实例视为另一个接口类型。例如(从模板方法模式维基百科文章中借用示例),如果以下内容是有效的:
type Game interface {
    PlayOneGame(playersCount int)
}

type GameImplementation interface {
    InitializeGame()
    MakePlay(player int)
    EndOfGame() bool
    PrintWinner()
}

func (game *GameImplementation) PlayOneGame(playersCount int) {
    game.InitializeGame()
    for j := 0; !game.EndOfGame(); j = (j + 1) % playersCount {
        game.MakePlay(j)
    }
    game.PrintWinner()
}

我可以使用任何实现了"GameImplementation"的实例作为"Game",无需进行任何转换:

var newGame Game
newGame = NewMonopolyGame() // implements GameImplementation
newGame.PlayOneGame(2)

更新:这是为了尝试在不涉及显式层次结构的情况下实现所有抽象基类的好处。如果我想定义一个新的行为PlayBestOfThreeGames,抽象基类将要求我更改基类本身 - 而在这里,我只需在GameImplementation接口之上定义一个方法即可。

4个回答

3

我想这和Java中不能在接口上定义方法的原因是一样的。

接口应该是一组对象的外部接口的描述,而不是它们如何实现底层行为。在Java中,如果您需要预定义某些行为的部分,则可能会使用抽象类,但我认为在Go中唯一的方法是使用函数而不是方法。

对于您的示例,我认为更符合Go语言习惯的代码应该是这样的:

type GameImplementation interface {
    InitializeGame()
    MakePlay(player int)
    EndOfGame() bool
    PrintWinner()
}

func PlayOneGame(game GameImplementation, playersCount int) {
    game.InitializeGame()
    for j := 0; !game.EndOfGame(); j = (j + 1) % playersCount {
        game.MakePlay(j)
    }
    game.PrintWinner()
}

PlayOneGame和任何特定的游戏实现可能位于不同的包中。

这里有一些关于golang-nuts的讨论


1
“game *GameImplementation” 不应该被传递为指针,因为它是一个接口而不是结构体。 - jimt
已经纠正了。我在发布这篇文章后不久才发现这个信息。我对这门语言仍然很陌生,在此之前还没有看到过这个事实的参考资料。 - Asgeir
感谢链接-看起来主要原因是为了保持简单和快速编译。 - Tom Carver
1
我认为这不是一个令人满意的答案 - 你说它“可能是因为你不能在Java接口中定义方法”,但没有详细说明原因。实际上,你不能在Java接口中定义方法,是因为Java有一个非常缺乏想象力的面向对象系统。有许多语言允许类似的事情发生,而事实证明(请参见我的答案),在Java中允许接口定义方法比Go更安全 :) - hobbs

1
首先,需要注意的是类型隐式地实现接口——也就是说,接口是“鸭子类型”。只要一个类型提供了接口所需的方法,就可以将其分配给接口类型的变量,而无需原始类型的任何合作。这与Java或C#不同,在这些语言中,实现接口的类必须声明其意图实现接口,除了实际提供方法之外。
Go也有很强的“局部性”倾向。例如,即使方法与类型分别声明,也不能在与其接收器类型不同的包中声明方法。你不能只是添加方法到os.File中。
如果接口可以提供方法(使它们成为traits/roles),那么实现接口的任何类型都会获得一堆新的方法。阅读代码并看到使用这些方法的人可能很难弄清楚它们来自何处。
有一个脆弱性的问题——更改接口所需的方法的签名,一堆其他方法会出现或消失。在它们消失的情况下,不清楚它们“本应该”来自哪里。如果类型必须声明它们实现接口的意图,那么违反契约将会引发错误(而“意外地”实现接口则没有任何作用),但是当接口隐式满足时,情况就会变得更加棘手。
更糟糕的是,可能会存在名称冲突——一个接口提供了一个与实现该接口的类型提供的方法同名的方法,或者两个接口都提供了一个同名的方法,并且某些类型恰好实现了这两个接口。解决这个冲突是Go真正喜欢避免的复杂问题,在很多情况下,根本没有令人满意的解决方案。
基本上,如果接口可以提供方法,那将是非常酷的——作为可组合行为单元的角色很酷,并且与Go的组合优先于继承的哲学相吻合——但实际上做到这一点对于Go来说太过复杂和太过遥远。

1
回答你的问题,是否有其他静态类型语言允许这样做:是的,大多数语言都可以。任何具有多重继承的语言都允许类具有抽象和具体方法的任意混合。此外,请参阅Scala的特质,它们类似于Java的接口,但可以具有具体方法。Scala还具有结构类型,这实际上就是Go接口的全部内容。

谢谢您的建议-我最初并不清楚,我正在寻找一种完全避免继承的机制,即允许我们在不改变GameImplementation的情况下实现上述目标。 - Tom Carver

1

你所描述的接口实际上可能是其他地方所称的抽象类——即定义了一些方法但不是全部,必须通过子类化才能实例化的类。

然而,Go语言没有任何类层次结构——整个类型结构都是平面的。类上的每个方法都是专门为该类定义的,而不是在任何父类、子类或接口上定义的。这是一个有意识的设计决策,而不是遗漏。

因此,在Go中,接口不是类型层次结构的组成部分(因为不存在这样的东西)。相反,它只是给定目的所必须实现的方法集的临时规范。就是这样。它们是动态类型的替代品,可以预先声明将要使用给定类型上的哪些函数,然后任何类型满足这些要求的变量都可以使用。

这使得在Go中无法使用泛型等模式,Rob Pike在一次会议上表示,如果有人能提出一个优雅的实现和一个令人信服的用例,这可能会在未来发生改变。但这还有待观察。


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