问题描述
我正在用 Go 编写一个解释器,我正在寻找存储 AST 的惯用方法.我阅读了 Go 编译器源代码,似乎他们使用带有空方法的接口来表示 AST.例如,我们有以下层次结构,
对象--不动- - 建造- - 山- 活动- - 车- - 自行车
这就是上面的层次结构是如何以空方法"的方式实现的.
type 对象接口 {目的()}类型不可移动接口{目的不动()}类型构建结构{...}类型山结构{...}类型可移动接口{目的活动()}类型汽车结构{...}类型山结构{...}func (*Building) object() {}func (*Mountain) object() {}func (*Car) object() {}func (*Bike) object() {}func (*Building) immovable() {}func (*Mountain) immovable() {}func (*Car) 可移动 () {}func (*Bike) 可移动() {}
上面的代码是一个人为的例子,这是 Go 编译器如何实现 带有数十个空方法的 AST.但为什么?注意定义了多少空方法.随着层次结构深度的增加,它可能会变得非常复杂.
注释中指出空方法不允许分配不兼容的类型.例如,在我们的示例中,不能将 *Car
分配给 *Immovable
.
这在支持继承的 C++ 等其他语言中非常容易.我想不出任何其他方式来表示 AST.
Go 编译器 AST 的实现方式可能是惯用的,但不是不那么直接吗?
Go 是 不是(相当) 一种面向对象的语言:它没有类并且它没有类型继承一>;但它在 struct
级别和 interface
级别都支持称为 embedding 的类似结构,并且它确实具有 方法.
接口 在 Go 中只是固定的方法集.一个类型隐式实现一个接口,如果它的方法集是接口的超集(没有意图的声明).
如果您想记录或明确声明您的类型确实实现了一个接口(因为它没有明确声明),则空方法非常有用.官方 Go FAQ:如何保证我的类型满足接口?
type Fooer interface {富()实现Fooer()}
如果你想区分你的类型层次结构(例如,你不想让一个对象同时是Movable
和Immovable
),它们必须有不同的方法集合(Movable
和 Immovable
的每个方法集合中必须至少有 1 个方法集合中不存在其他方法集合),因为如果方法集合将包含相同的方法,一个实现也会自动实现另一个,因此您可以将 Movable
对象分配给 Immovable
类型的变量.
向具有相同名称的接口添加空方法将为您提供这种区别,假设您不会将此类方法添加到其他类型.
减少空方法的数量
我个人对空方法没有任何问题.不过有一种方法可以减少它们.
如果您还为层次结构中的每个类型创建了一个 struct
implementation 并且每个实现embeds struct
执行上一层,上一层的方法集会自动来,不用多说:
对象
Object
接口和 ObjectImpl
实现:
type 对象接口 {目的()}类型 ObjectImpl 结构 {}func (o *ObjectImpl) object() {}
不可移动
Immovable
接口和 ImmovableImpl
实现:
type Immovable interface {目的不动()}类型 ImmovableImpl 结构 {ObjectImpl//嵌入 ObjectImpl}func (o *Immovable) immovable() {}
注意ImmovableImpl
只增加了immovable()
方法,object()
是继承的".>
建筑
构建
实现:
type 构建结构 {ImmovableImpl//嵌入 ImmovableImpl 结构//建筑特定的其他字段可能会来这里}
注意Building
不添加任何新方法,但它自动成为一个Immovable
对象.
如果子类型"的数量增加或接口类型具有不止 1 个标记"方法(因为所有方法都是继承的"),则这种技术的优势会大大增加.
I am writing an interpreter in Go and I am looking for the idiomatic way to store the AST. I read the Go compiler source code and it seems they used interfaces with an empty method to represent the AST. For example, we have the following hierarchy,
Object
--Immovable
----Building
----Mountain
--Movable
----Car
----Bike
This is how the above hierarchy is implemented in the "empty method" way.
type Object interface {
object()
}
type Immovable interface {
Object
immovable()
}
type Building struct {
...
}
type Mountain struct {
...
}
type Movable interface {
Object
movable()
}
type Car struct {
...
}
type Mountain struct {
...
}
func (*Building) object() {}
func (*Mountain) object() {}
func (*Car) object() {}
func (*Bike) object() {}
func (*Building) immovable() {}
func (*Mountain) immovable() {}
func (*Car) movable() {}
func (*Bike) movable() {}
The above code is a contrived example and this is how the Go compiler implemented the AST with dozens of empty methods. But WHY? Note how many empty methods are defined. It may get very complicated with the increase of the depth of the hierarchy.
It is stated in the comments that the empty methods disallow the assignment of incompatible types. In our example, a *Car
can't be assigned to a *Immovable
for instance.
This is so easy in other languages like C++ that supports inheritance. I can't think of any other way of representing the AST.
The way how the Go compiler AST is implemented may be idiomatic but isn't it less straight forward?
Go is not (quite) an object oriented language: it does not have classes and it does not have type inheritance; but it supports a similar construct called embedding both on struct
level and on interface
level, and it does have methods.
Interfaces in Go are just fixed method sets. A type implicitly implements an interface if its method set is a superset of the interface (there is no declaration of the intent).
Empty methods are great if you want to document or state explicitly that your type does implement an interface (because it is not stated explicitly). Official Go FAQ: How can I guarantee my type satisfies an interface?
type Fooer interface {
Foo()
ImplementsFooer()
}
If you want a distinction in your type hierarchy (e.g. you don't want to allow an object to be both Movable
and Immovable
), they must have different method sets (there must be at least 1 method in each of the method sets of Movable
and Immovable
that is not present in the other's), because if the method sets would contain the same methods, an implementation of one would automatically implement the other too therefore you could assign a Movable
object to a variable of type Immovable
.
Adding an empty method to the interface with the same name will provide you this distinction, assuming that you will not add such methods to other types.
Reducing the number of empty methods
Personally I have no problem with empty methods whatsoever. There is a way to reduce them though.
If you also create a struct
implementation for each type in the hierarchy and each implementation embeds the struct
implementation one level higher, the method set of one level higher will automatically come without further ado:
Object
Object
interface and ObjectImpl
implementation:
type Object interface {
object()
}
type ObjectImpl struct {}
func (o *ObjectImpl) object() {}
Immovable
Immovable
interface and ImmovableImpl
implementation:
type Immovable interface {
Object
immovable()
}
type ImmovableImpl struct {
ObjectImpl // Embed ObjectImpl
}
func (o *Immovable) immovable() {}
Note ImmovableImpl
only adds immovable()
method, object()
is "inherited".
Building
Building
implementation:
type Building struct {
ImmovableImpl // Embed ImmovableImpl struct
// Building-specific other fields may come here
}
Note Building
does not add any new methods yet it is automatically an Immovable
object.
The advantage of this technic grows greatly if the number of "subtypes" increases or if the interface types have more than just 1 "marker" method (because all methods are "inherited").
这篇关于Go 中创建复杂结构层次结构的惯用方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!