实现具有更宽方法签名的接口

huangapple go评论82阅读模式
英文:

Implementing an interface with a wider method signature

问题

在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

(其中BirdMoveResultAnimalMoveResult要"宽泛",因为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不认为BirdImplAnimal的实现。然而,如果Go比较返回类型,那么BirdMoveResult的任何实现也将实现AnimalMoveResult。所以,Move() BirdMoveResult不应该是Move() AnimalMoveResult的可接受实现吗(如果不是,为什么不是)?

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

英文:

In Go, is there a way to implement an interface using a method, where the return type of the corresponding method in the implementation is a "wider than" the expected return type?

This is difficult to explain so here is an example. I get this error when running the below example code in 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

(where BirdMoveResult is "wider than" AnimalMoveResult because any implementation of BirdMoveResult is also an implementation of 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?
}

I understand that the Move() method signatures do not match completely because of the different return types, thus Go does not consider BirdImpl as an implementation of Animal. However, if Go compares the return types, any implementation of BirdMoveResult would also implement AnimalMoveResult. So, shouldn't Move() BirdMoveResult be an acceptable implementation for Move() AnimalMoveResult (and if not, why not)?

Edit: In the actual scenario, Animal, AnimalMoveResult, and foo are part of an external package. Within my own code, I want to be able to extend AnimalMoveResult with my own interface methods (as it's done in the example with BirdMoveResult), while still being able to make use of foo with using the extended interface.

答案1

得分: 3

这些问题通常是以下原因导致的:

  • 过早的接口定义
  • 虚假接口,或者所谓的“泡沫包装”(请参阅我在这里的抱怨)
  • 并行接口

你遇到的错误主要与第三个问题有关,但每个问题都存在于你的示例设计中。

良好的接口应该通过它们的名称和方法签名传达一组行为。接口的名称应该由它包含的方法隐含表示,因为名称只是捕捉该行为本质的简单表达。

这个目的似乎只需要一个接口:

type Mover interface {
    Move() MoveResult
}

对于MoveResult,你可以这样做(如果你愿意,可以将float替换为int):

type MoveResult struct {
    Distance, Height float64
}

假设以下条件成立,这种类型可以作为一个结构体正常工作:

  • 移动结果只是数据点(它们不需要是动态的;你可以设置值并保留它们)
  • “额外”数据(如Height)在未指定时能够正确处理零值。

现在,实现bird类型:

type Bird struct {
    // 一些内部数据
}

func (b *Bird) Move() MoveResult {
    // 一些移动计算
    return MoveResult{Distance: howFar, Height: howHigh}
}

现在实现foo:

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

现在在main函数中使用它:

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

现在,我们可以向程序中添加其他类型的Mover并在foo中使用它们而不会出现问题。


回答你的评论,其中AnimalAnimalMoveResultfoo都是外部的,你无法修改它们:

根据我的理解,我提出了这个问题:有一个名为foo的函数,设计为接受一个名为Animal的接口,它的特点是它返回一个专有类型。你想将你的类型Bird用作Animal,但你无法将你想要的所有行为都适应那个专有的返回类型中。

那么如何解决这个问题呢?

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

type birdMoveResult struct {
    // 一些内部数据
}

func (r birdMoveResult) GetDistance() int {
    // 获取距离
}

func (r birdMoveResult) GetHeight() int {
    // 获取高度
}

现在,实现Bird类型

type Bird struct {
    // 一些内部数据
}

func (b *Bird) Move() AnimalMoveResult {
    var result birdMoveResult
    // 计算移动结果
    return birdMoveResult
}

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

剩下的问题是BirdMove方法缩小了广泛的接口,我们实际上失去了你添加的新功能。为了在满足Animal接口的同时恢复它,我们不能改变Move方法的签名。以下是两种可能的解决方法。

  1. 当使用你的bird类型时,你可以使用type-assert来获得对额外方法的访问权限
var b Bird
// ...
height := b.Move().(birdMoveResult).GetHeight()
  1. Bird中添加一个新的方法,返回更广泛的类型,保持现有的Move方法签名不变
func (b *Bird) MoveFull() birdMoveResult {
    var result birdMoveResult
    // 计算移动结果
    return birdMoveResult
}

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

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

这两种方法都可以工作,主要是个人偏好的问题。

英文:

These kinds of problems are usually the result of:

  • Premature interfaces
  • Fake interfaces, or "bubble-wrapping" (see my rant here)
  • Parallel interfaces

Your error you have is most specifically related to the 3rd issue, but each is present in your example design here.

Good interfaces should convey (in their name and their method signatures) a set of behaviour. The name of the interface should be implied by method(s) it contains, as the name is simply supposed to capture the essence of that behaviour.

The only interface seemingly needed for this purpose is:

type Mover interface {
    Move() MoveResult
}

For MoveResult, you can do this (exchange float with int if you like):

type MoveResult struct {
    Distance, Height float64
}

This type can potentially function just fine as a struct, given the following assumptions:

  • Move results are just data points (they don't need to be dynamic; you can set the values and leave them)
  • "Extra" data like Height function properly with a zero value when it's not specified.

Now, implement the bird type:

type Bird struct {
    // some internal data
}

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

Now implement foo:

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

Now use it in main:

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

Now we can add other kinds of Movers to the program and use them with foo without issue.


Addressing your comment, where Animal, AnimalMoveResult and foo are all external and you can't modify them:

How I'm posing the problem with my understanding: There is this function foo, designed to accept an interface called Animal, which is characterized by how it returns an proprietary type. You want to use your type Bird as an Animal, but you can't fit all of the behaviour you want into that proprietary return type.

So how to resolve this?

Implement your own, wider return type

type birdMoveResult struct {
    // some internal data
}

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

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

Now, implement the Bird type

type Bird struct {
    // some internal data
}

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

Notice that Move returns the AnimalMoveResult type, so now Bird will satisfy the Animal interface. We are able to do this because birdMoveResult satisfies the AnimalMoveresult type, so returning it here is legal.

The remaining issue is that Bird's Move method narrows the wide interface, we've effectively lost that new functionality you added. In order to get it back while still satisfying the Animal interface, we can't change the Move method signature at all. Here are 2 possible workarounds.

  1. When using your bird type, you can type-assert the result to gain access to the extra method
var b Bird
// ...
height := b.Move().(birdMoveResult).GetHeight()
  1. Add a new method to Bird that returns the wider type, leaving the existing Move with the same signature
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()

Either of these will work, it's mostly a matter of preference.

答案2

得分: 2

在Go语言中,是否有一种方法可以使用一个方法来实现接口,其中实现中对应方法的返回类型比预期的返回类型“更宽泛”?

没有。方法签名必须完全匹配。Go语言没有协变或逆变的概念。

英文:

> In Go, is there a way to implement an interface using a method, where the return type of the corresponding method in the implementation is a "wider than" the expected return type?

No. Methods signatures must match literally. Go has no notion of co- or contravariance.

答案3

得分: 1

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

然而,你可以声明满足接口的方法,但返回一个更宽泛的实现,可以进行类型断言。

英文:

Go's type system uses type name match. This would fail even if the same interface is returned with a different type name.

However, you can declare the method to satisfy the interface, but return a wider implementation that can be type-asserted.

huangapple
  • 本文由 发表于 2021年6月9日 01:16:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/67891717.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定