【问题标题】:Implementing an interface with a wider method signature使用更广泛的方法签名实现接口
【发布时间】:2021-06-08 17:16:45
【问题描述】:

在 Go 中,有没有办法使用方法来实现接口,其中实现中对应方法的返回类型“比”预期的返回类型“宽”?

这很难解释,所以这里举个例子。在 Go Playground 中运行以下示例代码时出现此错误:

./prog.go:36:14: cannot use BirdImpl{} (type BirdImpl) as type Animal in argument to foo:
    BirdImpl does not implement Animal (wrong type for Move method)
        have Move() BirdMoveResult
        want Move() AnimalMoveResult

(其中BirdMoveResult“比”AnimalMoveResult“宽”,因为BirdMoveResult 的任何实现也是AnimalMoveResult 的实现)

package main

type (
    Animal interface {
        Move() AnimalMoveResult
    }
    Bird interface {
        Move() BirdMoveResult
    }
    AnimalMoveResult interface {
        GetDistance() int
    }
    BirdMoveResult interface {
        GetDistance() int
        GetHeight() int
    }
    BirdImpl struct{}
    BirdMoveResultImpl struct{}
)

// Some implementation of BirdImpl.Move
// Some implementation of BirdMoveResultImpl.GetDistance
// Some implementation of BirdMoveResultImpl.GetHeight

func foo(animal Animal) int {
    return animal.Move().GetDistance()
}

func main() {
    foo(BirdImpl{})  // This fails because BirdImpl doesn't implement Animal. My question is why not?
}

我知道 Move() 方法签名不完全匹配,因为返回类型不同,因此 Go 不会将 BirdImpl 视为 Animal 的实现。然而,如果 Go 比较返回类型,BirdMoveResult 的任何实现也将实现AnimalMoveResult。那么,Move() BirdMoveResult 不应该是Move() AnimalMoveResult 的可接受实现吗(如果不是,为什么不呢)?

编辑:在实际场景中,AnimalAnimalMoveResultfoo 是外部包的一部分。在我自己的代码中,我希望能够使用我自己的接口方法扩展AnimalMoveResult(就像在示例中使用BirdMoveResult 所做的那样),同时仍然能够通过使用扩展接口来使用foo

【问题讨论】:

  • 虽然我很欣赏传达问题的努力,但这种类型系统看起来真的很复杂。基本上,您有多个冗余层将您从唯一的实质内容中撬开:GetDistance()(和未使用的GetHeight())。
  • 为了让一个类型满足接口,它必须实现接口中定义的所有方法完全按照接口中定义的方式
  • @HymnsForDisco 这似乎是多余的,因为这是我试图沟通的问题的(过度)简化示例
  • @Adrian 这就是我的想法(以及答案有助于确认的内容),我想我的问题更多是关于为什么 Go 不支持这种类型的接口匹配/尝试有什么限制支持一下
  • Go 的设计目标之一是简化类型系统。将接口视为使不同部分更顺畅地协同工作的油脂。它应该只应用在正确的地方,否则事情会变得非常混乱。

标签: go


【解决方案1】:

这类问题通常是由于:

  • 过早的接口
  • 假接口,或“冒泡包装”(见我的咆哮here
  • 并行接口

您遇到的错误与第 3 个问题最为相关,但每个错误都出现在您的示例设计中。

好的接口应该传达(在他们的名字和他们的方法签名中)一组行为。接口的名称应该隐含在它所包含的方法中,因为名称只是为了捕捉该行为的本质。

似乎为此目的唯一需要的接口是:

type Mover interface {
    Move() MoveResult
}

对于 MoveResult,您可以这样做(如果您愿意,可以将 float 与 int 交换):

type MoveResult struct {
    Distance, Height float64
}

考虑到以下假设,这种类型可以作为结构正常工作:

  • 移动结果只是数据点(它们不需要是动态的;您可以设置值并保留它们)
  • “额外”数据(如高度)在未指定时以零值正常工作。

现在,实现鸟类型:

type Bird struct {
    // some internal data
}

func (b *Bird) Move() MoveResult {
    // some movement calculations
    return MoveResult{Distance: howFar, Height: howHigh}
}

现在实现 foo:

func foo(m Mover) float64 {
    return m.Move().Distance
}

现在在 main 中使用它:

func main() {
    foo(&Bird{})
}

现在我们可以将其他类型的Movers 添加到程序中,并将它们与foo 一起使用而不会出现问题。


解决您的评论,其中AnimalAnimalMoveResultfoo 都是外部的,您无法修改它们:

我如何根据我的理解提出问题:有这个函数foo,旨在接受一个名为Animal 的接口,其特点是它如何返回专有类型。您想将您的类型 Bird 用作 Animal,但您无法将您想要的所有行为都放入该专有返回类型中。

那么如何解决呢?

实现您自己的更广泛的返回类型

type birdMoveResult struct {
    // some internal data
}

func (r birdMoveResult) GetDistance() int {
    // get the distance
}

func (r birdMoveResult) GetHeight() int {
    // get the height
}

现在,实现 Bird 类型

type Bird struct {
    // some internal data
}

func (b *Bird) Move() AnimalMoveResult {
    var result birdMoveResult
    // calculate the move result
    return birdMoveResult
}

注意Move 返回AnimalMoveResult 类型,所以现在Bird 将满足Animal 接口。我们能够这样做是因为birdMoveResult 满足AnimalMoveresult 类型,所以在这里返回它是合法的。

剩下的问题是BirdMove 方法缩小了宽界面,我们实际上已经失去了您添加的新功能。为了在满足Animal接口的同时取回它,我们根本不能更改Move方法签名。这里有 2 种可能的解决方法。

  1. 当使用你的鸟类型时,你可以type-assert结果来获得额外方法的访问权限
var b Bird
// ...
height := b.Move().(birdMoveResult).GetHeight()
  1. Bird 添加一个新方法,该方法返回更宽的类型,而现有Move 具有相同的签名
func (b *Bird) MoveFull() birdMoveResult {
    var result birdMoveResult
    // calculate the move result
    return birdMoveResult
}

func (b *Bird) Move() AnimalMoveResult {
    return b.MoveFull()
}

height := b.MoveFull().GetHeight()

其中任何一个都可以,这主要是一个偏好问题。

【讨论】:

  • 感谢您解释并尝试简化这一切。如果我告诉你 AnimalAnimalMoveResultfoo 来自不同的包(我无法编辑)怎么办?然而,我仍然需要扩展AnimalMoveResult 以定义更具体的其他方法,并调用函数foo。所有接口名称仅用于示例并且无关紧要(您可以想象其他名称以便它们确实描述一组行为,但代码结构仍然相同)。
  • @DavidL 更新答案
  • 我明白了...这很有帮助,谢谢!稍后我会将评论中的额外细节添加到我原来的问题中
【解决方案2】:

在 Go 中,有没有办法使用方法来实现接口,其中实现中对应方法的返回类型“比”预期的返回类型“宽”?

没有。方法签名必须在字面上匹配。 Go 没有协变或逆变的概念。

【讨论】:

    【解决方案3】:

    Go 的类型系统使用类型名称匹配。即使返回具有不同类型名称的相同接口也会失败。

    但是,您可以声明该方法以满足接口的要求,但返回一个可以进行类型断言的更广泛的实现。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-30
      • 1970-01-01
      • 1970-01-01
      • 2018-06-28
      相关资源
      最近更新 更多