本節內容 - 什麼是方法集 - 方法集區分基礎類型T和指針類型*T - 匿名嵌入對方法集的影響 - 方法集調用 ...
本節內容
- 什麼是方法集
- 方法集區分基礎類型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.go
package 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語言所謂的自動擁有方法集不是繼承而是語法糖層面上的代碼補全。
匿名嵌入對方法集的影響[付費閱讀]
當你匿名嵌入一個對象的時候,編譯器會幫我們自動生成間接代碼調用,所以看上去擁有了對象的方法,實際上你不是擁有,而是編譯器幫你做了代碼補全。這個語法糖實際上是代碼補全而不是動態行為是靜態行為。
$ cat embed.go
package main
import (
"strconv"
"reflect"
"fmt"
)
type N int
type X struct {
N
}
type Y struct {
*N
}
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 x X
listMethods(x)
listMethods(&x)
var y Y
listMethods(y)
listMethods(&y)
}
X嵌入了N,Y嵌入了*N。
$ go run embed.go
--- main.X ---------
String: func(main.X) string
--- *main.X ---------
Inc: func(*main.X)
String: func(*main.X) string
--- main.Y ---------
Inc: func(main.Y)
String: func(main.Y) string
--- *main.Y ---------
Inc: func(*main.Y)
String: func(*main.Y) string
我們註意到X只擁有X自身的方法,X指針擁有X和X指針的方法。Y因為嵌入了X,只能獲得對應值類型的方法,Y指針擁有對應的指針類型。
我們看看匿名嵌入的時候用哪些東西是自動生成的,我們註意到這裡面有很多自動生成的方法來實現方法集。
$ go build -gcflags "-N -l" -o test embed.go
$ nm test | grep "[^\.]main\."
$ go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
nm test | grep "[^\.]main\."
00000000004b1f60 T main.init
0000000000596204 B main.initdone.
00000000004b1a30 T main.listMethods
00000000004b1e20 T main.main
00000000004b1980 T main.(*N).Inc
00000000004b1fe0 T main.(*N).String
00000000004b19c0 T main.N.String
00000000004b2080 T main.(*X).Inc
00000000004b20a0 T main.(*X).String
00000000004b2130 T main.X.String
00000000004b21c0 T main.(*Y).Inc
00000000004b2270 T main.Y.Inc
00000000004b21e0 T main.(*Y).String
00000000004b22d0 T main.Y.String
go tool objdump -s "main\." test | grep "TEXT.*autogenerated"
TEXT main.init(SB) <autogenerated>
TEXT main.(*N).String(SB) <autogenerated>
TEXT main.(*X).Inc(SB) <autogenerated>
TEXT main.(*X).String(SB) <autogenerated>
TEXT main.X.String(SB) <autogenerated>
TEXT main.(*Y).Inc(SB) <autogenerated>
TEXT main.(*Y).String(SB) <autogenerated>
TEXT main.Y.Inc(SB) <autogenerated>
TEXT main.Y.String(SB) <autogenerated>
方法集調用[付費閱讀]
$ cat call.go
N類型,有兩個方法,當我們用N類型調用的時候,理論上只執行String,但是如果我們用N類型調用Inc是不是合法的呢?
package main
import (
"strconv"
)
type N int
func (n *N) Inc() {
*n++
}
func (n N) String() string {
return strconv.Itoa(int(n))
}
func main() {
var n N = 100
(*N).Inc(&n)
s := (*N).String(&n)
println(s)
}
$ go build -gcflags "-l" -o test call.go
反彙編
$ go tool objdump -s "main\.main" test
call.go:17 0x451600 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
call.go:17 0x451609 483b6110 CMPQ 0x10(CX), SP
call.go:17 0x45160d 0f868b000000 JBE 0x45169e
call.go:17 0x451613 4883ec38 SUBQ $0x38, SP
call.go:17 0x451617 48896c2430 MOVQ BP, 0x30(SP)
call.go:17 0x45161c 488d6c2430 LEAQ 0x30(SP), BP
call.go:17 0x451621 488d0538e40000 LEAQ 0xe438(IP), AX
call.go:18 0x451628 48890424 MOVQ AX, 0(SP)
call.go:18 0x45162c e84faefbff CALL runtime.newobject(SB)
call.go:18 0x451631 488b442408 MOVQ 0x8(SP), AX
call.go:18 0x451636 4889442428 MOVQ AX, 0x28(SP)
call.go:18 0x45163b 48c70064000000 MOVQ $0x64, 0(AX)
call.go:20 0x451642 48890424 MOVQ AX, 0(SP)
call.go:20 0x451646 e855ffffff CALL main.(*N).Inc(SB)
call.go:20 0x45164b 488b442428 MOVQ 0x28(SP), AX
call.go:22 0x451650 48890424 MOVQ AX, 0(SP)
call.go:22 0x451654 e8b7000000 CALL main.(*N).String(SB)
call.go:22 0x451659 488b442408 MOVQ 0x8(SP), AX
call.go:22 0x45165e 4889442420 MOVQ AX, 0x20(SP)
call.go:22 0x451663 488b c2410 MOVQ 0x10(SP), CX
call.go:22 0x451668 48894c2418 MOVQ CX, 0x18(SP)
call.go:23 0x45166d e81e22fdff CALL runtime.printlock(SB)
call.go:23 0x451672 488b442420 MOVQ 0x20(SP), AX
call.go:23 0x451677 48890424 MOVQ AX, 0(SP)
call.go:23 0x45167b 488b442418 MOVQ 0x18(SP), AX
call.go:23 0x451680 4889442408 MOVQ AX, 0x8(SP)
call.go:23 0x451685 e8a62bfdff CALL runtime.printstring(SB)
call.go:23 0x45168a e8b124fdff CALL runtime.printnl(SB)
call.go:23 0x45168f e88c22fdff CALL runtime.printunlock(SB)
call.go:24 0x451694 488b6c2430 MOVQ 0x30(SP), BP
call.go:24 0x451699 4883c438 ADDQ $0x38, SP
call.go:24 0x45169d c3 RET
call.go:17 0x45169e e8ed70ffff CALL runtime.morestack_noctxt(SB)
call.go:17 0x4516a3 e958ffffff JMP main.main(SB)
我們註意到CALL main.(*N).Inc(SB)
,它的調用方式並沒有運行期的行為,都是靜態綁定。就是很明確的你要調用哪些東西。因為我們知道你調用的哪些方法雖然源碼不存在,但是編譯器幫你生成了,這個方法實際上已經存在了,我們剛剛看到編譯器實際上替我們生成了這些間接調用,那j既然說這個函數或者方法已經存在了,那直接用call間接方法就可以了。
如果A嵌入B,B有個方法叫x,那麼編譯器會自動幫我們生成A.X方法,方法內部調用A.B.X方法,這實際上是編譯器自動生成的。如果我們源碼里寫A.X的話實際上會被編譯器翻譯成對自動包裝函數的調用,所以從語法上A.X看上去好像A繼承X方法,實際上是因為編譯器翻譯成A.B.X的調用。