基础技能树-26 方法集
2017-11-28 10:51 by 李永京, ... 阅读, ... 评论, 收藏, 编辑本节内容
- 什么是方法集
- 方法集区分基础类型T和指针类型*T
- 匿名嵌入对方法集的影响[付费阅读]
- 方法集调用[付费阅读]
重点理解接口和接口相关的概念,接口在不同语言里有不同的做法,静态语言往往会有显式的接口,就是声明一个接口类型,至于哪些类型是否实现了这个接口类型,不同语言有不同处理方法,C#或者Java必须指定类型实现了接口,go语言只要符合条件就可以了,不需要显式的说实现。动态语言很多时候没有接口这样的概念,只要你有对应的名字就可以了,它们把接口称之为协议,概念都是类似的,今天是要了解静态语言接口是怎么实现的,在了解接口之前,需要准备一些相关的概念。
什么是方法集
go语言里的方法集,个人认为实现起来不是特别优雅,但是并不影响我们理解概念。什么是方法集?假如说类型A实现了a1方法,B继承自A,B实现了b1方法,C继承B,C实现了c1方法。那么A的方法集是什么?什么是方法集,就是说A能调用的方法集合。A的方法集是a1,B的方法集是a1,b1,就是还包含父类的方法,C的方法集是a1,b1,c1。问题是go语言很大的问题是没有继承的概念,它用的是组合的概念,这时候变得麻烦了。
假如说C里面包含了B和A,C的方法集包含哪些呢?go语言编译器就做了比较投机取巧的事情,它认为包含了某个东西,就除了访问它的字段以外还可以访问它的方法,就是C.c1,C.B.b1,C.A.a1,按照正常访问是访问3个方法,在语法糖上把C.B.b1做了一次缩写C.b1,C.A.a1缩写成C.a1,编译器负责查找,最后的方法集变成了c1,b1,a1。很显然这个是编译器替我们完成的这种东西。
正常情况下我们自己写伪码:
struct A{
a1()
}
struct B{
b1()
}
struct C{
A
B
c1()
}
C::a1{
C.A.a1()
}
C::b1{
C.B.b1()
}如果编译器不帮我们做,我们实际上需要自己去写C.a1调用C.A.a1,这样一来,c的方法集就包含a1,b1,c1,因为理论上组合是没有办法继承它内部字段成员的,必须是显式实现,区别在于是我们自己写还是编译器替我们写。
方法集区分基础类型T和指针类型*T
看看编译器怎么做这事情的?
$ cat test.gopackage main
import (
"fmt"
"reflect"
"strconv"
)
type N int
func (n *N) Inc() {
*n++
}
func (n N) String() string {
return strconv.Itoa(int(n))
}
func listMethods(a interface{}) {
t := reflect.TypeOf(a)
fmt.Printf("\n--- %v ---------\n", t)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}
func main() {
var n N
listMethods(n)
listMethods(&n)
}listMethods方法是利用反射把当前方法的方法集全部列出来。方法集具体的区别在于go语言很大的不同在于可以显式的提供参数,提供参数可以指定类型的*N或者N。这地方就会形成这样一个概念。
比如声明类型N,假如说有两个方法A、B,但是它们的this参数可以是N也可以是指针*N,这样的话A和B就会属于不同的类型,一个是N,一个是N的指针。我们知道一个类型和一个类型的指针属于两种类型,*N长度是8,N长度是32,64之类的,虽然类型不是一回事,一个是指针一个是普通类型,这就造成实现方法的时候,是针对N实现的还是针对*N实现的,这就造成了N的方法集和*N的方法集是不一样的。
上面代码定义类型N,实现了两个方法,一个方法针对N本身实现,一个方法针对*N实现。我们分别列出它们的方法集究竟有哪些。在main函数中定义了N实例,分别列出N和*N有哪些方法集。
$ go run test.go输出:
--- main.N ---------
String: func(main.N) string
--- *main.N ---------
Inc: func(*main.N)
String: func(*main.N) string
我们可以看到N方法集就有1个N基础类型的方法,*N方法集除了*N类型的方法以外还包含N类型的方法。func(*main.N) string做了类型转换。
简单的来说,我们有个类型T,T的方法集只包含T自身的,但是*T方法集等于T+*T的方法,这就是差别。
不同的语言在这块的做法会有一些细微的差别。Java和C#为什么没有这东西,因为它们默认的话this就有一种引用方式,没有说把this分为引用和值两种方式。就是你引用实例,说白了就相当于只有*T没有T,指针类型和指针的基础类型不是一回事。
当我们拿到一个对象指针*T的时候,调用对象T的方法是不是安全的呢?因为我们可以把指针里面数据取出来,然后作为T参数。但是我们拥有T,未必就能获得T的指针*T,因为它有可能是个临时对象,我们知道临时对象是没有办法取得它的指针的,你有指针也就意味着这个对象肯定是在栈上或者堆上分配过的,但是你拥有临时对象的实例未必能拿到临时对象的指针,不见得是合法的。我们假如访问字典里面一个元素,如果编译器对字典元素本身做了不允许访问地址,那你访问元素的时候拿不到指针的,这时候获取到它的指针没有意义,还有跨栈帧获取指针也没有意义。所以说用指针获取指针目标是安全的,用目标未必能获得它的指针。这是因为内存安全模型决定的,因为go语言并不完全区分值类型和引用类型,它是由编译器决定对象到底分配到哪。
String: func(*main.N) string方法哪里来的?
编译
$ go build -gcflags "-N -l" -o test test.go输出符号
$ nm test | grep "[^\.]main\."输出
00000000004b1ee0 T main.init
0000000000595204 B main.initdone.
00000000004b1a30 T main.listMethods
00000000004b1e20 T main.main
00000000004b1980 T main.(*N).Inc
00000000004b1f60 T main.(*N).String
00000000004b19c0 T main.N.String
我们注意到String有两个,main.(*N).String和main.N.String,main.N.String是我们自己定义的,main.(*N).String是程序执行时候输出的,两个地址都不一样,这表明最终生成机器代码的时候是存在两个这样函数,很显然main.(*N).String是编译器生成的。
反汇编看看到底什么样的:
$ go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
TEXT main.(*N).String(SB) <autogenerated>
main.(*N).String(SB)是机器生成的,地址是00000000004b1f60和符号表里面一致,实际上在符号表里面已经打上了<autogenerated>标记。为什么打上这个标记,因为我们自己写的代码在符号表里面有信息可以对应到哪一行,但是很显然有些东西不是我们写的,所以从源码上没有办法对应关系,所以符号表标记这些信息由编译器生成的。
现在知道,当我们想实现一个方法集的时候,源码层面和机器码层面其实是不一样的,因为源码层面当我嵌入一个类型的时候,我会自动拥有它的方法。对于机器码来说,你想调用函数,必须给一个合法的地址,这个合法的地址必须生成对应的代码,这个代码高级语言称之为规则,规则就是编译器支持这种理论,编译器替你完成这种东西。
所谓的方法集就是当你嵌入一个类型的时候,你拥有它的方法,准确的说,编译器自动生成嵌入类型的方法。
go语言虽然没有继承的概念,编译器替我们补全了这种间接调用。这样一来有点类似于A继承B的方法,但是这不是继承。因为是继承的话就不会有自动代码生成,直接通过类型表去调用。go语言所谓的自动拥有方法集不是继承而是语法糖层面上的代码补全。