Golang接口转换为嵌入式结构体

6
我希望实现一个碰撞库,使用Collidable接口。
type Collidable interface{
    BoundingBox() (float64,float64,float64,float64)
    FastCollisionCheck(c2 Collidable) bool
    DoesCollide(c2 Collidable) bool
    Collide(c2 Collidable)
}

它拥有预定义的形状,例如:
type Circle struct{
X,Y,Radius float64
}

想法是我可以做到
type Rock struct{
    collision.Circle
    ....
}

然后实现接口Collidable,这样我就可以将其传递给期望可碰撞对象的Spatial Hash Map。唯一需要做的就是根据我的需求重写Collide()函数。

然而,圆形类型的函数无法处理岩石类型,即使它内部嵌入了一个圆形。

func (c1 *Circle) DoesCollide(i Collidable) bool{
    switch c2 := value.(type) {
    case Circle:
    //doesn't fire, as it is of type Rock (unknown in this package)
    //Needed is something like
    //if i_embeds_Circle then c2 := i_to_Circle 
    }
}

这种情况是否可行?有更好的方法吗?


这个(https://play.golang.org/p/J5bvaEtLbM)是用来说明你的问题的吗? - VonC
有点儿。注释rock的Shape()函数(它使用了circles函数)。我的问题是不要进入select语句的默认部分,而是如果我将rock传递给DoesShape()函数,则也要进入circle部分。 - user2089648
所以 https://play.golang.org/p/rg_plLZMSM: default: 部分的存在是为了说明即使 Rock 作为匿名字段拥有 Circle,它也不会被检测为 CircleShaper - VonC
是的,那就是问题所在。如何让它将岩石识别为圆形(它嵌入其中)。 - user2089648
4个回答

5
您正在尝试在Go中使用继承的面向对象设计模式,这不是正确的做法。此外,在Java或其他面向对象语言中,接口名称以"able"结尾。在Go中,传统上接口名称以"er"结尾。
针对您关于“Rock”的问题,我建议所有可能发生碰撞的物体都实现CollisonShape()方法,并返回一个collison.Shaper (例如Circle),用于测试碰撞。这里的collison是您的包名称。
// This interface is defined in the collison package.
// Any object that may collide must implement that method.
type Collider interface {
    CollisonShape() Shaper
}

// This function defined in the collison package 
// test if two Collider collide into each other.
func Collide(c1, c2 Collider) bool {
    shape1, shape2 := c1.CollisonShape(), c2.CollisonShape()
    ...
}

// This is how you would define an object that can collide.
type Rock struct {
    shape *collison.Circle
    ...
}
// Implements the Collider interface.
// The return type must be the same as in the interface.
func (r *Rock) CollisonShape() collison.Shaper {
    return r.shape
}

如您所见,我们使用一种方法来访问岩石的碰撞形状。这使我们能够编写

if collison.Collide(rock, spaceCraft) {...}

这是关于如何获取Rock碰撞形状的问题的答案。
如果您想避免在Collide()函数中调用CollisonShape()方法,则需要直接传递collison.Shaper。
Collide方法将在collison包中定义。
func Collide(shape1, shape2 Shaper) bool {...}

你需要编写以下代码:

然后你需要编写

if collison.Collide(rock.shape, spacecraft.shape) {...}

这个设计会稍微更高效一些,但代价是代码可读性较差,这被经验丰富的Go程序员所反感。
如果你想让Circle成为rock中的一个嵌入结构体,你需要按照以下方式定义它。将shape嵌入其中可以节省Circle的分配时间和GC的一些工作。
type Rock struct {
    shape collison.Circle
    ....
}

if collison.Collide(&rock.shape, &spacecraft.shape) {...}

如果你想使用匿名嵌套结构体,那么你需要编写如下代码:
type Rock struct {
    Circle
    ....
}

if collison.Collide(&rock.Circle, &spacecraft.Rectangle) {...}

正如你所看到的,代码越来越难于阅读和使用,形状不再是抽象的了。应该仅在极少数确实有意义的情况下使用匿名嵌入式结构体。

通过使用最初建议的CollisonShape()方法,您可以轻松地将Rock结构更改为此结构,而不会破坏任何代码。

type Rock struct {
    shape collison.Circle
    ...
}


func (r *Rock) CollisonShape() collison.Shaper {
    return &r.shape
}

现在形成了形状和嵌入结构。使用一个方法来获取形状,将Rock的内部实现与访问形状解耦。您可以更改Rock的内部实现而不需要更改其他地方的代码。

这就是为什么Go不支持继承的原因之一。它会在基类和派生类之间创建非常强的依赖关系和耦合。经验表明,随着代码的发展,人们经常会后悔这样的耦合。对象组合是Go首选、推荐和得到良好支持的做法。

如果效率是您的目标,则每个Collider都应该有一个位置进行更改和一个边界框,其宽度和高度不会更改。您可以使用这些值来进行边界框重叠测试,从而节省一些操作。但这是另外一个故事。


我在想,如果Circle本身通过具有CollisionShape方法来实现Collider,那么嵌入一个匿名的Circle是否会使Rock也在你最后一个示例的扩展中实现Collider?这样,OP可以通过使Circle实现Collider并调用c2.CollisionShape().(type)而不是立即调用c2.(type)来简单地解决。我错过了什么吗?我觉得CollisionShape方法是使您的想法起作用的本质。 - scenia

0

如果你正在调用一个不可扩展的遗留库(无法检测到Rock,只有Circle),似乎只有一种解决方案,传递一个Circle

请参见此示例,其中我使用非匿名字段'c'来表示Rock
(这意味着Rock必须实现接口,这是对Circle.Shape()的简单委托)

type Shaper interface {
    Shape()
}

type Circle struct{}

func (c *Circle) Shape() {}

type Rock struct{ c *Circle }

func (r *Rock) Shape() { r.Shape() }

func DoesShape(s Shaper) {
    fmt.Println("type:", reflect.TypeOf(s))
    switch st := s.(type) {
    case *Circle:
        fmt.Println("Shaper Circle %+v", st)
    default:
        fmt.Println("Shaper unknown")
    }
}

func main() {
    c := &Circle{}
    DoesShape(c)
    r := &Rock{c}
    DoesShape(r.c)
}

然后输出为:

type: *main.Circle
Shaper Circle %+v &{}
type: *main.Circle
Shaper Circle %+v &{}

将接口命名为“Shaper”而不是“IShaper”可能更符合惯用语。只是说一下。 :) - 425nesp
在这种情况下,我无法重写Collide函数,该函数在两个对象碰撞时被调用,而我需要它针对嵌入圆形形状的对象进行自定义。 - user2089648
@user2089648 确实:通过在类型 B 中嵌入(通过匿名 字段)类型 A,并不会使 B 成为 A。这意味着需要采用不同的方法。如果 Rock 实现了 Collidable,那么您的库至少会调用 RockCollide() 版本吗? - VonC
Type Rock有自己的Collide()版本,但没有DoesCollide()版本。但是由于Rock嵌入了Circle,因此它可以作为可碰撞对象,并调用Circle的DoesCollide()版本。然而,DoesCollide()失败了,因为无法将参数"c2 Collidable"转换为Circle类型。 - user2089648
@user2089648 好的,所以全嵌入方法在这里并不合适。 - VonC

0

在你的例子中,圆知道石头的存在,因此我也可以将接口直接转换为正确的类型。问题是,碰撞是在它自己的库中,没有进一步了解稍后采取相同形状的对象的情况。 - user2089648

0

我曾通过轻微修改基类来解决类似的问题。不确定这是否是你要寻找的解决方案。

import "fmt"

type IShaper interface {
    Shape()
}

type Rect struct{}

func (r *Rect) Shape() {}

type Circle struct{}

func (c *Circle) GetCircle() *Circle { return c }

func (c *Circle) Shape() {}

type Rock struct{ *Circle }

type ShapeWithCircle interface {
    GetCircle() *Circle
}

func DoesShape(s IShaper) {
    if sc, ok := s.(ShapeWithCircle); ok {
        fmt.Printf("Shaper Circle %+v\n", sc.GetCircle())
    } else {
        fmt.Println("Shaper unknown")
    }
}

func main() {
    DoesShape(&Circle{})
    DoesShape(&Rock{&Circle{}})
    DoesShape(&Rect{})
}

也就是说,添加一个微不足道的GetCircle()函数,以便嵌入圆形的任何东西都具有获取(可能已嵌入)圆形的功能。然后,任何需要圆形的人都可以轻松地编写一个接口(这里是ShapeWithCircle),允许测试是否定义了GetCircle(),如果定义了,则调用它以获取嵌入的圆形。
https://play.golang.org/p/IDkjTPrG3Z5上进行实验。

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