Go 学习笔记7-Go继承:类型嵌入
目录
类型嵌入指的就是在一个类型的定义中嵌入了其他类型。Go 语言支持两种类型嵌入:
- 接口类型的类型嵌入
- 结构体类型的类型嵌入
接口类型只能嵌入接口类型,而结构体类型对嵌入类型的要求就比较宽泛了,可以是任意自定义类型或接口类型。
1,接口类型的类型嵌入
接口类型声明了由一个方法集合代表的接口,比如下面接口类型 E:
type E interface {
M1()
M2()
}
如果某个类型实现了方法 M1 和 M2,我们就说这个类型实现了 E 所代表的接口。
再定义另外一个接口类型 I,它的方法集合中包含了三个方法 M1、M2 和 M3,如下面代码:
type I interface {
M1()
M2()
M3()
}
我们看到接口类型 I 方法集合中的 M1 和 M2,与接口类型 E 的方法集合中的方法完全相同。在这种情况下,我们可以用接口类型 E 替代上面接口类型 I 定义中 M1 和 M2,如下面代码:
type I interface {
E
M3()
}
像这种在一个接口类型(I)定义中,嵌入另外一个接口类型(E)的方式,就是我们说的接口类型的类型嵌入。
而且,这个带有类型嵌入的接口类型 I 的定义与上面那个包含 M1、M2 和 M3 的接口类型 I 的定义,是等价的。
通过在接口类型中嵌入其他接口类型可以实现接口的组合,这也是 Go 语言中基于已有接口类型构建新接口类型的惯用法。
接口类型的类型嵌入比较简单,我们只要把握好它的语义,也就是“方法集合并入”就可以了。
2,结构体类型的类型嵌入
常规的结构体类型:
type S struct {
A int
b string
c T
p *P
_ [10]int8
F func()
}
结构体类型 S 中的每个字段(field)都有唯一的名字与对应的类型,即便是使用空标识符占位的字段,它的类型也是明确的。
带有嵌入字段的结构体定义,看下面代码中的 S1:
type T1 int
type t2 struct{
n int
m int
}
type I interface {
M1()
}
type S1 struct {
T1
*t2
I
a int
b string
}
- 标识符 T1 表示字段名为 T1,它的类型为自定义类型 T1;
- 标识符 t2 表示字段名为 t2,它的类型为自定义结构体类型 t2 的指针类型;
- 标识符 I 表示字段名为 I,它的类型为接口类型 I。
- 如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。
- 如果结构体使用从其他包导入的类型作为嵌入字段,比如 pkg.T,那么这个嵌入字段的字段名就是 T,代表的类型为 pkg.T。
这种以某个类型名、类型的指针类型名或接口类型名,直接作为结构体字段的方式就叫做结构体的类型嵌入,这些字段也被叫做嵌入字段。
嵌入字段的使用的确可以帮我们在 Go 中实现方法的“继承”。
类型嵌入这种看似“继承”的机制,实际上是一种组合的思想。更具体点,它是一种组合中的代理(delegate)模式,如下图所示:
结构体类型的方法集合,包含嵌入的接口类型的方法集合。 也就是说,当结构体类型中嵌入了接口类型时,接口类型中的方法都会并入到结构体类型中。
在结构体类型中嵌入结构体类型,为 Gopher 们提供了一种“实现继承”的手段,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。
当通过结构体类型 S 的变量 s 调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法。这个时候,Reader 字段就被找了出来,之后 s.Read 的调用就被转换为 s.Reader.Read 调用。
这样一来,嵌入字段 Reader 的 Read 方法就被提升为 S 的方法,放入了类型 S 的方法集合。
当结构体嵌入的多个接口类型的方法集合存在交集时,Go 编译器会报错。
嵌入了其他类型的结构体类型本身是一个代理,在调用其实例所代理的方法时,Go 会首先查看结构体自身是否实现了该方法。
- 如果实现了,Go 就会优先使用结构体自己实现的方法。
- 如果没有实现,那么 Go 就会查找结构体中的嵌入字段的方法集合中,是否包含了这个方法。
- 如果多个嵌入字段的方法集合中都包含这个方法,那么我们就说方法集合存在交集。
- 这个时候,Go 编译器就会因无法确定究竟使用哪个方法而报错。(有点类似 C++ 多重继承出现的问题)
一个示例:
type E1 interface {
M1()
M2()
M3()
}
type E2 interface {
M1()
M2()
M4()
}
type T struct {
E1
E2
}
func main() {
t := T{}
t.M1()
t.M2()
}
输出如下:
main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2
这个问题有两种解决办法
- 一是,我们可以消除 E1 和 E2 方法集合存在交集的情况。
- 二是为 T 增加 M1 和 M2 方法的实现,这样的话,编译器便会直接选择 T 自己实现的 M1 和 M2,不会陷入两难境地。
如下:
... ...
type T struct {
E1
E2
}
func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }
func main() {
t := T{}
t.M1() // T's M1
t.M2() // T's M2
}
3,type 定义新类型时的方法集合
分两种情况:
- type NewT OldT:定义新的类型
- 如果 OldT 是接口类型,NewT 的方法集合与 OldT 的方法集合是一致的
- 如果 OldT 是非接口类型,那么 NewT 不会继承 OldT 的任意一个方法,NewT 的方法集合是空集合
- type NewT = OldT:类型别名
- 无论 OldT 是接口类型还是非接口类型,NewT 都与 OldT 拥有完全相同的方法集合
- 实际上 NewT 与 OldT 是完全等价的
4,一个思考题
下面带有类型嵌入的结构体 S1 与不带类型嵌入的结构体 S2 是否是等价的,如不等价,区别在哪里?
type T1 int
type t2 struct{
n int
m int
}
type I interface {
M1()
}
type S1 struct {
T1
*t2
I
a int
b string
}
type S2 struct {
T1 T1
t2 *t2
I I
a int
b string
}
- S1 是类型嵌入的,拥有“继承”能力
- S2 并不是类型嵌入的,只是普通的结构体组合,所以没有对应接口的方法集合(并不存在“继承”的能力)
(完。)
文章作者 @码农加油站
上次更改 2022-06-13