1.题目
这里定义了两个类型:People和Teacher。类型Teacher内部嵌入了类型People。并且定义了两个以*People为接收者的方法ShowA和ShowB,以及一个以*Teacher为接收者的方法ShowB。
package main
import "fmt"
type People struct{}
func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}
2.众说纷纭
有人认为Teacher从嵌入的类型People这里继承了两个方法showA和showB。所以t.ShowA()实际上会调用(*People).ShowA():
先输出字符串"showA",
再继续调用p.showB(),输出字符串"showB"。
但也有人不这样认为,他们同意showA是从People这里继承来的,但是*Teacher已经有showB()方法了,Go语言的编译器不允许T和*T定义同名方法。所以Teacher没有showB方法。
而且他们认为通过Teacher类型的变量调用showA,接收者已经是Teacher类型了,所以在输出showA之后,接下来应该调用t.ShowB(),在语法糖的作用下,它被转换为对(*Teacher).ShowB()的调用。所以应该输出字符串"teacher showB"。(你猜有多少人会这么想?)
还有一些人已经凌乱了,他们认为Teacher内嵌的类型是People,而People类型没有定义任何方法,能继承个啥?
3.答案
抛开前面所有的观点,实际运行输出结果:
showA
showB
4.解析
那么接下来,就只差一个合理的解释了~
首先要明确的是:T和*T是两种类型,分别对应自己的类型元数据,有着各自的方法集。
从代码中的明确定义来看,目前各个类型的方法集是这样的:
但不要忘了还有编译器生成的包装方法。通过如下命令只编译不链接,排除掉编译器内联优化造成的干扰,得到编译后的OBJ文件main.o。
go tool compile -l -p main main.go
再借助go tool nm命令就可以查看我们感兴趣的方法列表了:
go tool nm main.o | grep ' T '
上述命令输出结果如下:
c4f T main.(*People).ShowA
d1d T main.(*People).ShowB
ef6 T main.(*Teacher).ShowA
dd0 T main.(*Teacher).ShowB
e83 T main.main
People,*People以及Teacher的方法集同代码中的定义一致,而*Teacher相关的方法列表中多了一个main.(*Teacher).ShowA,这就是编译器自动生成的了。
所以编译器为*Teacher生成了一个类似这样的包装方法:
func (t *Teacher) ShowA() {
(*People).ShowA(&(t.People))
}
现在,我们已经知道这四种类型的方法列表,因此可以确定:t.showA()会在语法糖的作用下,转换为对(*Teacher).ShowA()方法的调用。而它又会取出People成员的地址作为接收者去执行*People这里的ShowA方法。所以会有这样的输出:
showA
showB
到这里我们依然无法解释一个问题:
明明是Teacher中内嵌了People,为什么编译器却只给*Teacher生成了包装方法?
为此我们来做一个小实验,探索一下组合式继承中编译器生成包装方法的规则。
5.实验
type A int
func (a A) Value() int {
return int(a)
}
func (a *A) Set(n int) {
*a = A(n)
}
type B struct {
A
b int
}
type C struct {
*A
c int
}
类型A有一个值接收者方法Value(),和一个指针接收者方法Set()。类型B中嵌入了A,类型C中嵌入了*A。
接下来看一下B和C以及*B和*C,分别会继承哪些方法。
首先编译得到OBJ文件:
go tool compile -l -p main main.go
然后通过go tool nm工具确认:
go tool nm main.o | grep ' T '
efd T main.(*A).Set
f38 T main.(*A).Value
fdb T main.(*B).Set
1002 T main.(*B).Value
1125 T main.(*C).Set
114d T main.(*C).Value
ee0 T main.A.Value
1096 T main.B.Value
11e4 T main.C.Set
1275 T main.C.Value
f25 T main.main
可以发现:
(1)A依然只有一个方法Value。
(2)在介绍包装方法时,我们提过为了支持接口,编译器会为值接收者方法生成指针接收者的包装方法。所以*A这里多了一个Value的包装方法。
(3)除去其它已定义的方法,可以看到编译器把逻辑合理的方法都生成出来了。*B,C,*C都继承了A和*A的所有方法,只有B只继承了A的方法,没有继承*A的方法。
这是因为以B为接收者调用方法时,方法操作的已经是B的副本,无法获取嵌入的A的原始地址。而*A的方法从语义上来讲,需要操作原始变量。
也就是说,对于B而言继承*A的方法是没有意义的,所以编译器并没有给B生成Set方法。
6.结论
(1)无论是嵌入值还是嵌入指针,值接收者方法始终能够被继承;
(2)只有在能够拿到嵌入对象的地址时,才能继承指针接收者方法。
这就是组合式继承中,包装方法的生成规则~