GoLang之组合式继承

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)只有在能够拿到嵌入对象的地址时,才能继承指针接收者方法。
这就是组合式继承中,包装方法的生成规则~