Go語言 類型內(nèi)嵌 - 不同于繼承的類型擴展方式

2023-02-16 17:38 更新

結(jié)構(gòu)體一文中,我們得知一個結(jié)構(gòu)體類型可以擁有若干字段。 每個字段由一個字段名和一個字段類型組成。事實上,有時,一個字段可以僅由一個字段類型組成。 這樣的字段聲明方式稱為類型內(nèi)嵌(type embedding)。

此篇文章將解釋類型內(nèi)嵌的目的和各種和類型內(nèi)嵌相關(guān)的細節(jié)。

類型內(nèi)嵌語法

下面是一個使用了類型內(nèi)嵌的例子:

package main

import "net/http"

func main() {
	type P = *bool
	type M = map[int]int
	var x struct {
		string // 一個具名非指針類型
		error  // 一個具名接口類型
		*int   // 一個無名指針類型
		P      // 一個無名指針類型的別名
		M      // 一個無名類型的別名

		http.Header // 一個具名映射類型
	}
	x.string = "Go"
	x.error = nil
	x.int = new(int)
	x.P = new(bool)
	x.M = make(M)
	x.Header = http.Header{}
}

在上面這個例子中,有六個類型被內(nèi)嵌在了一個結(jié)構(gòu)體類型中。每個類型內(nèi)嵌形成了一個內(nèi)嵌字段(embedded field)。

因為歷史原因,內(nèi)嵌字段有時也稱為匿名字段。但是,事實上,每個內(nèi)嵌字段有一個(隱式的)名字。 此字段的非限定(unqualified)類型名即為此字段的名稱。 比如,上例中的六個內(nèi)嵌字段的名稱分別為string、errorint、PMHeader。

哪些類型可以被內(nèi)嵌?

當(dāng)前的Go白皮書(1.19)規(guī)定

An embedded field must be specified as a type name T or as a pointer to a non-interface type name *T, and T itself may not be a pointer type.

翻譯過來:

一個內(nèi)嵌字段必須被聲明為形式T或者一個基類型為非接口類型的指針類型*T,其中T為一個類型名但是T不能表示一個指針類型。

此規(guī)則描述在Go 1.9之前是精確的。但是隨著從Go 1.9引入的自定義類型別名概念,此描述有些過時和不太準(zhǔn)確了。 比如,此描述沒有包括上一節(jié)的例子中的P內(nèi)嵌字段的情形。

這里,本文試圖使用一個更精確的描述:

  • 一個類型名T只有在它既不表示一個具名指針類型也不表示一個基類型為指針類型或者接口類型的指針類型的情況下才可以被用做內(nèi)嵌字段。
  • 一個指針類型*T只有在T為一個類型名并且T既不表示一個指針類型也不表示一個接口類型的時候才能被用做內(nèi)嵌字段。

下面列出了一些可以被或不可以被內(nèi)嵌的類型或別名:

type Encoder interface {Encode([]byte) []byte}
type Person struct {name string; age int}
type Alias = struct {name string; age int}
type AliasPtr = *struct {name string; age int}
type IntPtr *int
type AliasPP = *IntPtr

// 這些類型或別名都可以被內(nèi)嵌。
Encoder
Person
*Person
Alias
*Alias
AliasPtr
int
*int

// 這些類型或別名都不能被內(nèi)嵌。
AliasPP          // 基類型為一個指針類型
*Encoder         // 基類型為一個接口類型
*AliasPtr        // 基類型為一個指針類型
IntPtr           // 具名指針類型
*IntPtr          // 基類型為一個指針類型
*chan int        // 基類型為一個無名類型
struct {age int} // 無名非指針類型
map[string]int   // 無名非指針類型
[]int64          // 無名非指針類型
func()           // 無名非指針類型

一個結(jié)構(gòu)體類型中不允許有兩個同名字段,此規(guī)則對匿名字段同樣適用。 根據(jù)上述內(nèi)嵌字段的隱含名稱規(guī)則,一個無名指針類型不能和它的基類型同時內(nèi)嵌在同一個結(jié)構(gòu)體類型中。 比如,int*int類型不能同時內(nèi)嵌在同一個結(jié)構(gòu)體類型中。

一個結(jié)構(gòu)體類型不能內(nèi)嵌(無論間接還是直接)它自己。

一般說來,只有內(nèi)嵌含有字段或者擁有方法的類型才有意義(后續(xù)幾節(jié)將闡述原因),盡管很多既沒有字段也沒有方法的類型也可以被內(nèi)嵌。

類型內(nèi)嵌的意義是什么?

類型內(nèi)嵌的主要目的是為了將被內(nèi)嵌類型的功能擴展到內(nèi)嵌它的結(jié)構(gòu)體類型中,從而我們不必再為此結(jié)構(gòu)體類型重復(fù)實現(xiàn)被內(nèi)嵌類型的功能。

很多其它流行面向?qū)ο蟮木幊陶Z言都是用繼承來實現(xiàn)上述目的。兩種實現(xiàn)方式有它們各自的利弊。 這里,此篇文章將不討論哪種方式更好一些,我們只需知道Go選擇了類型內(nèi)嵌這種方式。 這兩種方式有一個很大的不同點:

  • 如果類型T繼承了另外一個類型,則類型T獲取了另外一個類型的能力。 同時,一個T類型的值也可以被當(dāng)作另外一個類型的值來使用。
  • 如果一個類型T內(nèi)嵌了另外一個類型,則另外一個類型變成了類型T的一部分。 類型T獲取了另外一個類型的能力,但是T類型的任何值都不能被當(dāng)作另外一個類型的值來使用。

下面是一個展示了如何通過類型內(nèi)嵌來擴展類型功能的例子:

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}
func (p Person) PrintName() {
	fmt.Println("Name:", p.Name)
}
func (p *Person) SetAge(age int) {
	p.Age = age
}

type Singer struct {
	Person // 通過內(nèi)嵌Person類型來擴展之
	works  []string
}

func main() {
	var gaga = Singer{Person: Person{"Gaga", 30}}
	gaga.PrintName() // Name: Gaga
	gaga.Name = "Lady Gaga"
	(&gaga).SetAge(31)
	(&gaga).PrintName()   // Name: Lady Gaga
	fmt.Println(gaga.Age) // 31
}

從上例中,當(dāng)類型Singer內(nèi)嵌了類型Person之后,看上去類型Singer獲取了類型Person所有的字段和方法, 并且類型*Singer獲取了類型*Person所有的方法。此結(jié)論是否正確?隨后幾節(jié)將給出答案。

注意,類型Singer的一個值不能被當(dāng)作Person類型的值用。下面的代碼編譯不通過:

var gaga = Singer{}
var _ Person = gaga

當(dāng)一個結(jié)構(gòu)體類型內(nèi)嵌了另一個類型,此結(jié)構(gòu)體類型是否獲取了被內(nèi)嵌類型的字段和方法?

下面這個程序使用反射列出了上一節(jié)的例子中的Singer類型的字段和方法,以及*Singer類型的方法。

package main

import (
	"fmt"
	"reflect"
)

... // 為節(jié)省篇幅,上一個例子中聲明的類型在這里省略了。

func main() {
	t := reflect.TypeOf(Singer{}) // the Singer type
	fmt.Println(t, "has", t.NumField(), "fields:")
	for i := 0; i < t.NumField(); i++ {
		fmt.Print(" field#", i, ": ", t.Field(i).Name, "\n")
	}
	fmt.Println(t, "has", t.NumMethod(), "methods:")
	for i := 0; i < t.NumMethod(); i++ {
		fmt.Print(" method#", i, ": ", t.Method(i).Name, "\n")
	}

	pt := reflect.TypeOf(&Singer{}) // the *Singer type
	fmt.Println(pt, "has", pt.NumMethod(), "methods:")
	for i := 0; i < pt.NumMethod(); i++ {
		fmt.Print(" method#", i, ": ", pt.Method(i).Name, "\n")
	}
}

輸出結(jié)果:

main.Singer has 2 fields:
 field#0: Person
 field#1: works
main.Singer has 1 methods:
 method#0: PrintName
*main.Singer has 2 methods:
 method#0: PrintName
 method#1: SetAge

從此輸出結(jié)果中,我們可以看出類型Singer確實擁有一個PrintName方法,以及類型*Singer確實擁有兩個方法:PrintNameSetAge。 但是類型Singer并不擁有一個Name字段。那么為什么選擇器表達式gaga.Name是合法的呢? 畢竟gagaSinger類型的一個值。 請閱讀下一節(jié)以獲取原因。

選擇器的縮寫形式

從前面的結(jié)構(gòu)體方法兩篇文章中,我們得知,對于一個值x,x.y稱為一個選擇器,其中y可以是一個字段名或者方法名。 如果y是一個字段名,那么x必須為一個結(jié)構(gòu)體值或者結(jié)構(gòu)體指針值。 一個選擇器是一個表達式,它表示著一個值。 如果選擇器x.y表示一個字段,此字段也可能擁有自己的字段(如果此字段的類型為另一個結(jié)構(gòu)體類型)和方法,比如x.y.z,其中z可以是一個字段名,也可是一個方法名。

在Go中,(不考慮下面將要介紹的選擇器碰撞和遮擋),如果一個選擇器中的中部某項對應(yīng)著一個內(nèi)嵌字段,則此項可被省略掉。 因此內(nèi)嵌字段又被稱為匿名字段。

一個例子:

package main

type A struct {
	x int
}

func (a A) MethodA() {}

type B struct {
	*A
}

type C struct {
	B
}

func main() {
	var c = &C{B: B{A: &A{FieldX: 5}}}

	// 這幾行是等價的。
	_ = c.B.A.FieldX
	_ = c.B.FieldX
	_ = c.A.FieldX // A是類型C的一個提升字段
	_ = c.FieldX   // FieldX也是一個提升字段

	// 這幾行是等價的。
	c.B.A.MethodA()
	c.B.MethodA()
	c.A.MethodA()
	c.MethodA() // MethodA是類型C的一個提升方法
}

這就是為什么在上一節(jié)的例子中選擇器表達式gaga.Name是合法的, 因為它只不過是gaga.Person.Name的一個縮寫形式。

類似的,選擇器gaga.PrintName可以被看作是gaga.Person.PrintName的縮寫形式。 但是,我們也可以不把它看作是一個縮寫。畢竟,類型Singer確實擁有一個PrintName方法, 盡管此方法是被隱式聲明的(請閱讀下下節(jié)以獲得詳情)。 同樣的原因,選擇器(&gaga).PrintName(&gaga).SetAge可以看作(也可以不看作)是(&gaga.Person).PrintName(&gaga.Person).SetAge的縮寫。

Name被稱為類型Singer的一個提升字段(promoted field)。 PrintName被稱為類型Singer的一個提升方法(promoted method)。

注意:我們也可以使用選擇器gaga.SetAge,但是只有在gaga是一個可尋址的類型為Singer的值的情況下。 它只不過是(&gaga).SetAge的一個語法糖。

在上面的例子中,c.B.A.FieldX稱為選擇器表達式c.FieldX、c.B.FieldXc.A.FieldX的完整形式。 類似的,c.B.A.MethodA可以稱為c.MethodAc.B.MethodAc.A.MethodA的完整形式。

如果一個選擇器的完整形式中的所有中部項均對應(yīng)著一個內(nèi)嵌字段,則中部項的數(shù)量稱為此選擇器的深度。 比如,上面的例子中的選擇器c.MethodA的深度為2,因為此選擇器的完整形式為c.B.A.MethodA,并且BA都對應(yīng)著一個內(nèi)嵌字段。

選擇器遮擋和碰撞

一個值x(這里我們總認為它是可尋址的)可能同時擁有多個最后一項相同的選擇器,并且這些選擇器的中間項均對應(yīng)著一個內(nèi)嵌字段。 對于這種情形(假設(shè)最后一項為y):

  • 只有深度最淺的一個完整形式的選擇器(并且最淺者只有一個)可以被縮寫為x.y。 換句話說,x.y表示深度最淺的一個選擇器。其它完整形式的選擇器被此最淺者所遮擋(壓制)。
  • 如果有多個完整形式的選擇器同時擁有最淺深度,則任何完整形式的選擇器都不能被縮寫為x.y。 我們稱這些同時擁有最淺深度的完整形式的選擇器發(fā)生了碰撞。

如果一個方法選擇器被另一個方法選擇器所遮擋,并且它們對應(yīng)的方法描述是一致的,那么我們可以說第一個方法被第二個覆蓋(overridden)了。

舉個例子,假設(shè)A、BC為三個定義類型

type A struct {
	x string
}
func (A) y(int) bool {
	return false
}

type B struct {
	y bool
}
func (B) x(string) {}

type C struct {
	B
}

下面這段代碼編譯不通過,原因是選擇器v1.A.xv1.B.x的深度一樣,所以它們發(fā)生了碰撞,結(jié)果導(dǎo)致它們都不能被縮寫為v1.x。 同樣的情況發(fā)生在選擇器v1.A.yv1.B.y身上。

var v1 struct {
	A
	B
}

func f1() {
	_ = v1.x // error: 模棱兩可的v1.x
	_ = v1.y // error: 模棱兩可的v1.y
}

下面的代碼編譯沒問題。選擇器v2.C.B.x被另一個選擇器v2.A.x遮擋了,所以v2.x實際上是選擇器v2.A.x的縮寫形式。 因為同樣的原因,v2.y是選擇器v2.A.y(而不是選擇器v2.C.B.y)的縮寫形式。

var v2 struct {
	A
	C
}

func f2() {
	fmt.Printf("%T \n", v2.x) // string
	fmt.Printf("%T \n", v2.y) // func(int) bool
}

一個被遮擋或者碰撞的選擇器并不妨礙更深層的選擇器被提升,如下例所示中的.M.z

package main

type x string
func (x) M() {}

type y struct {
	z byte
}

type A struct {
	x
}
func (A) y(int) bool {
	return false
}

type B struct {
	y
}
func (B) x(string) {}

func main() {
	var v struct {
		A
		B
	}
	//_ = v.x // error: 模棱兩可的v.x
	//_ = v.y // error: 模棱兩可的v.y
	_ = v.M // ok. <=> v.A.x.M
	_ = v.z // ok. <=> v.B.y.z
}

一個不尋常的但需要注意的細節(jié)是:來自不同庫包的兩個非導(dǎo)出方法(或者字段)將總是被認為是兩個不同的標(biāo)識符,即使它們的名字完全一致。 因此,當(dāng)它們的屬主類型被同時內(nèi)嵌在同一個結(jié)構(gòu)體類型中的時候,它們絕對不會相互碰撞或者遮擋。 舉個例子,下面這個含有兩個庫包的Go程序編譯和運行都沒問題。 但是,如果將其中所有出現(xiàn)的m()改為M(),則此程序?qū)⒕幾g不過。 原因是A.MB.M碰撞了,導(dǎo)致c.M為一個非法的選擇器。

package foo // import "x.y/foo"

import "fmt"

type A struct {
	n int
}

func (a A) m() {
	fmt.Println("A", a.n)
}

type I interface {
	m()
}

func Bar(i I) {
	i.m()
}
package main

import "fmt"
import "x.y/foo"

type B struct {
	n bool
}

func (b B) m() {
	fmt.Println("B", b.n)
}

type C struct{
	foo.A
	B
}

func main() {
	var c C
	c.m()      // B false
	foo.Bar(c) // A 0
}

為內(nèi)嵌了其它類型的結(jié)構(gòu)體類型聲明的隱式方法

上面已經(jīng)提到過,類型Singer*Singer都有一個PrintName方法,并且類型*Singer還有一個SetAge方法。 但是,我們從沒有為這兩個類型聲明過這幾個方法。這幾個方法從哪來的呢?

事實上,假設(shè)結(jié)構(gòu)體類型S內(nèi)嵌了一個類型(或者類型別名)T,并且此內(nèi)嵌是合法的,

  • 對內(nèi)嵌類型T的每一個方法,如果此方法對應(yīng)的選擇器既不和其它選擇器碰撞也未被其它選擇器遮擋,則編譯器將會隱式地為結(jié)構(gòu)體類型S聲明一個同樣描述的方法。 繼而,編譯器也將為指針類型*S隱式聲明一個相應(yīng)的方法。
  • 對類型*T的每一個方法,如果此方法對應(yīng)的選擇器既不和其它選擇器碰撞也未被其它選擇器遮擋,則編譯器將會隱式地為類型*S聲明一個同樣描述的方法。

簡單說來,

  • 類型struct{T}*struct{T}均將獲取類型T的所有方法。
  • 類型*struct{T}、struct{*T}*struct{*T}都將獲取類型*T的所有方法。

下面展示了編譯器為類型Singer*Singer隱式聲明的三個(提升)方法:

// 注意:這些聲明不是合法的Go語法。這里這樣表示只是為了
// 解釋目的。它們有助于解釋提升方法值是如何被估值的。
func (s Singer) PrintName = s.Person.PrintName
func (s *Singer) PrintName = s.Person.PrintName
func (s *Singer) SetAge = s.Person.SetAge

右邊的部分為各個提升方法相應(yīng)的完整形式選擇器形式。

方法一文中,我們得知我們不能為無名的結(jié)構(gòu)體類型(和基類型為無名結(jié)構(gòu)體類型的指針類型)聲明方法。 但是,通過類型內(nèi)嵌,這樣的類型也可以擁有方法。

如果一個結(jié)構(gòu)體類型內(nèi)嵌了一個實現(xiàn)了一個接口類型的類型(此內(nèi)嵌類型可以是此接口類型自己),則一般說來,此結(jié)構(gòu)體類型也實現(xiàn)了此接口類型,除非發(fā)生了選擇器碰撞和遮擋。 比如,上例中的結(jié)構(gòu)體類型和以它為基類型的指針類型均實現(xiàn)了接口類型I

請注意:一個類型將只會獲取它(直接或者間接)內(nèi)嵌了的類型的方法。 換句話說,一個類型的方法集由為類型直接(顯式或者隱式)聲明的方法和此類型的底層類型的方法集組成。 比如,在下面的例子中,

  • 類型Age沒有方法,因為代碼中既沒有為它聲明任何方法,它也沒有內(nèi)嵌任何類型,。
  • 類型X有兩個方法:IsOddDouble。 其中IsOdd方法是通過內(nèi)嵌類型MyInt而得來的。
  • 類型Y沒有方法,因為它所內(nèi)嵌的類型Age沒有方法,另外代碼中也沒有為它聲明任何方法。
  • 類型Z只有一個方法:IsOdd。 此方法是通過內(nèi)嵌類型MyInt而得來的。 它沒有獲取到類型XDouble方法,因為它并沒有內(nèi)嵌類型X
type MyInt int
func (mi MyInt) IsOdd() bool {
	return mi%2 == 1
}

type Age MyInt

type X struct {
	MyInt
}
func (x X) Double() MyInt {
	return x.MyInt + x.MyInt
}

type Y struct {
	Age
}

type Z X

提升方法值的正規(guī)化和估值

假設(shè)v.m是一個合法的提升方法表達式,在編譯時刻,編譯器將把此提升方法表達式正規(guī)化。 正規(guī)化過程分為兩步:首先找出此提升方法表達式的完整形式;然后將此完整形式中的隱式取地址和解引用操作均轉(zhuǎn)換為顯式操作。

和其它方法值估值的規(guī)則一樣,對于一個已經(jīng)正規(guī)化的方法值表達式v.m,在運行時刻,當(dāng)v.m被估值的時候,屬主實參v的估值結(jié)果的一個副本將被存儲下來以供后面調(diào)用此方法值的時候使用。

以下面的代碼為例:

  • 提升方法表達式s.M1的完整形式為s.T.X.M1。 將此完整形式中的隱式取地址和解引用操作轉(zhuǎn)換為顯式操作之后的結(jié)果為(*s.T).X.M1。 在運行時刻,屬主實參(*s.T).X被估值并且估值結(jié)果的一個副本被存儲下來以供后用。 此估值結(jié)果為1,這就是為什么調(diào)用f()總是打印出1
  • 提升方法表達式s.M2的完整形式為s.T.X.M2。 將此完整形式中的隱式取地址和解引用操作轉(zhuǎn)換為顯式操作之后的結(jié)果為(&(*s.T).X).M2。 在運行時刻,屬主實參&(*s.T).X被估值并且估值結(jié)果的一個副本被存儲下來以供后用。 此估值結(jié)果為提升字段s.X(也就是(*s.T).X)的地址。 任何對s.X的修改都可以通過解引用此地址而反映出來,但是對s.T的修改是不會通過此地址反映出來的。 這就是為什么兩個g()調(diào)用都打印出了2。
package main

import "fmt"

type X int

func (x X) M1() {
	fmt.Println(x)
}

func (x *X) M2() {
	fmt.Println(*x)
}

type T struct { X }

type S struct { *T }

func main() {
	var t = &T{X: 1}
	var s = S{T: t}
	var f = s.M1 // <=> (*s.T).X.M1
	var g = s.M2 // <=> (&(*s.T).X).M2
	s.X = 2
	f() // 1
	g() // 2
	s.T = &T{X: 3}
	f() // 1
	g() // 2
}

接口類型內(nèi)嵌接口類型

不但結(jié)構(gòu)體類型可以內(nèi)嵌類型,接口類型也可以內(nèi)嵌類型。但是接口類型只能內(nèi)嵌接口類型。 詳情請閱讀接口一文。

一個有趣的類型內(nèi)嵌的例子

在本文的最后,讓我們來看一個有趣的例子。 此例子程序?qū)⑾萑胨姥h(huán)并會因堆棧溢出而崩潰退出。 如果你已經(jīng)理解了多態(tài)和類型內(nèi)嵌,那么就不難理解為什么此程序?qū)⑺姥h(huán)。

package main

type I interface {
	m()
}

type T struct {
	I
}

func main() {
	var t T
	var i = &t
	t.I = i
	i.m() // 將調(diào)用t.m(),然后再次調(diào)用i.m(),......
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號