猿问

尝试使用泛型在 Go 中实现访问者模式

我有以下简单的基于泛型的go包,它实现了 GoF 访客模式:


package patterns


type Social interface {

    AcceptVisitor(visitor *Visitor)

}


type Component struct {

}


func (c *Component) AcceptVisitor(visitor *Visitor) {

    visitor.VisitComponent(c)

}


type Collection[T Social] struct {

    Component

    items[]T

}


func (c *Collection[T]) AcceptVisitor(visitor *Visitor) {

    visitor.VisitCollection(c) // <- Error Here

}


type Visitor struct {

}


func (v *Visitor) VisitComponent(component *Component) {

}


func (v *Visitor) VisitCollection(collection *Collection[Social]) {

    for _, item := range collection.items {

        item.AcceptVisitor(v)

    }

}

编译器给出以下错误:


./patterns.go:20:26: cannot use c (variable of type *Collection[T]) as

  type *Collection[Social] in argument to visitor.VisitCollection

这对我来说似乎很奇怪,因为通用类型 T 被限制为 Social。

我尝试了几件事:

  • 用接口定义替换了访问者抽象类型。这导致了社交和访客界面之间的循环依赖。

  • 从修复问题的声明中删除了泛型,但我们非常需要 Collection 类型的泛型。

看起来go应该能够处理这段代码中的泛型。

可能的解决方案:在与@blackgreen 进行了非常有帮助的讨论之后,我们决定问题出现的原因有以下几点:

  1. Go 是(真正)严格类型化的,不允许将传递给函数的参数“缩小”为原始类型的子集,即使编译器仍然可以证明它是安全的。Go 是否应该允许缩小范围是有争议的。

  2. Go 不允许对方法进行泛型约束,因为约束可能会与与方法关联的结构上的泛型约束发生冲突。

  3. Go,没错,不允许循环依赖。我们可以将访问者模式的所有依赖项抽象为接口,但随后将具有该模式的“双重分派”方面所需的循环依赖项。

为了绕过这些项目,并仍然获得访问者模式的好处,我们可以将访问者结构中的 VisitXYZ() 方法更改为(可能是通用的)函数,每个函数都将 *Visitor 参数作为函数的第一个参数和被访问的对象作为第二个参数。

我在 Go Playground 中发布了这个解决方案:https ://go.dev/play/p/vV7v61teFbj

注意:即使这个可能的解决方案看起来确实可以解决问题,但实际上并没有。如果您考虑编写几种不同类型的访问者(一种用于漂亮打印,一种用于复制,一种用于排序等),您很快就会意识到,由于 VisitXYZ() 函数不是方法,因此您不能为每个函数拥有多个版本每个访客类型。最后,Visitor 模式确实需要 Social 接口和 Visitor 接口之间的循环依赖这一事实注定了 Go 的失败。我将关闭这篇文章,但会留下分析,这样其他人就不需要重复了。


潇湘沐
浏览 91回答 1
1回答

尚方宝剑之说

我得出的结论是泛型让这种模式变得更糟。通过参数化Collection结构,您强制items []T拥有相同的元素。使用普通接口,您可以进行动态分派,从而允许items包含不同的实现。仅此一项就应该是充分的理由。这是一个没有泛型的最小实现,改编自一些 Java 示例(可运行代码):主要接口:type Visitor func(Element)type Element interface {&nbsp; &nbsp; Accept(Visitor)}实施者:type Foo struct{}func (f Foo) Accept(visitor Visitor) {&nbsp; &nbsp; visitor(f)}容器:type List struct {&nbsp; &nbsp; elements []Element}func (l *List) Accept(visitor Visitor) {&nbsp; &nbsp; for _, e := range l.elements {&nbsp; &nbsp; &nbsp; &nbsp; e.Accept(visitor)&nbsp; &nbsp; }&nbsp; &nbsp; visitor(l)}访客本身,作为常规功能。在这里您可以自由定义任何功能。由于是无类型的,您可以直接将其作为Visitor参数传递:func doVisitor(v Element) {&nbsp; &nbsp; switch v.(type) {&nbsp; &nbsp; case *List:&nbsp; &nbsp; &nbsp; &nbsp; fmt.Println("visiting list")&nbsp; &nbsp; case Foo:&nbsp; &nbsp; &nbsp; &nbsp; fmt.Println("visiting foo")&nbsp; &nbsp; case Bar:&nbsp; &nbsp; &nbsp; &nbsp; fmt.Println("visiting bar")&nbsp; &nbsp; }}
随时随地看视频慕课网APP

相关分类

Go
我要回答