Go語言學習 二十一 內嵌

本文最初發表在我的個人博客,查看原文,獲得更好的閱讀體驗


在像Java這種語言中,有子類(或者繼承)的概念,通過繼承複用已有的功能或屬性,與繼承不同,Go中使用組合的方式來完成已有實現的複用,這種做法稱爲內嵌。具體來說,就是將已定義類型內嵌到結構體或接口中完成組合。

一 接口內嵌

接口內嵌非常簡單。例如標準庫中的io.Readerio.Writer接口,它們的定義如下:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

我們當然可以聲明一個新的接口來顯示的包含上述的ReadWrite方法:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

但是,更好的做法是將已有的接口內嵌到新的接口中,就像標準庫io.ReadWriter所做的這樣:

// ReadWriter 接口整合了 Reader 和 Writer 接口。
type ReadWriter interface {
    Reader
    Writer
}

現在,ReadWriter同時擁有了ReaderWriter的功能。

需要注意的是,內嵌的接口中不能包含重複的方法,而且接口中只能嵌套接口
例如以下錯誤示例

package main

import "fmt"

func main() {
	fmt.Println("test")
}

type A interface {
	M1()
	M2()
}

type B interface {
	M2()
	M3()
}

type C interface {
	A
	B       // 編譯錯誤:duplicate method M2
	Counter // 編譯錯誤:interface contains embedded non-interface Counter
}

type Counter uint

上述接口C中包含的兩個子接口AB中都有方法M2,而Counter本身不是接口,所以編譯會報錯。

接口不能直接或間接嵌套自身。

二 結構內嵌

結構中,同樣可以內嵌其他類型。例如標準庫bufio包中有bufio.Readerbufio.Writer兩個結構體類型,分別實現了io.Readerio.Writer接口,而結構bufio.ReadWriter則通過整合bufio.Readerbufio.Writer到自身,實現了帶緩衝的reader/writer

// ReadWriter 存儲了指向 Reader 和 Writer 的指針。
// 它實現了 io.ReadWriter。
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

注意,上述的bufio.ReadWriter只列出了結構類型,並未給予它們字段名。內嵌字段是結構指針的形式,所以使用前需要初始化爲有效的指針。當然,我們也可以這麼來定義ReadWriter

// 一個不好的示例
type ReadWriter struct {
    reader *Reader
    writer *Writer
}

但是如果這麼寫的話,我們就得提供轉發的方法來滿足io中的接口:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

而通過直接內嵌結構體,我們就能避免這種繁瑣。內嵌類型的方法可以直接引用,這意味着bufio.ReadWriter不僅包括bufio.Readerbufio.Writer的方法,它還同時滿足下列三個接口:io.Readerio.Writer 以及io.ReadWriter,在Go中,這種做法被稱爲晉升promoted,詳見下文)。

還有種區分內嵌與子類(或者繼承)的重要手段。當內嵌一個類型時,該類型的方法會成爲外部類型的方法,但當它們被調用時,該方法的接收者仍然是內部類型,而非外部的。上述示例中,當bufio.ReadWriterRead方法被調用時,它與之前寫的轉發方法具有同樣的效果;接收者是ReadWriterreader字段,而非ReadWriter本身。

若我們需要直接引用內嵌字段,可以忽略包限定名,直接將該字段的類型名作爲字段名。

2.1 內嵌字段

前面已經看到將其他類型內嵌到結構中的示例。這種使用類型聲明但沒有顯式字段名稱的字段稱爲內嵌字段。必須將內嵌字段指定爲類型名稱T或指向非接口類型名稱*T的指針,並且T本身不是指針類型。非限定類型名稱充當字段名稱。

// 類型M有四個內嵌字段:T1, *T2, P.T3 還有 *P.T4
type M struct {
	T1        // 字段名爲 T1
	*T2       // 字段名爲 T2
	P.T3      // 字段名爲 T3
	*P.T4     // 字段名爲 T4
	x, y int  // 字段名爲 x 和 y
}

以下字段聲明不合法,因爲結構中的字段名稱需要唯一:

struct {
	T     // 與內嵌字段 *T 和 *P.T 衝突
	*T    // 與內嵌字段 T 和 *P.T 衝突
	*P.T  // 與內嵌字段 T 和 *T 衝突
}

2.2 晉升(Promoted)

如前文所講,對於結構x中的內嵌字段或方法f,如果x.f是一個可以表示字段或方法的合法選擇器,則稱其爲晉升。晉升的字段除了不能在結構的複合字面量中表示字段名之外,與普通字段無異。

給定一個結構類型S和一個已定義類型T,晉升的方法會包含在結構的方法集中,只要滿足以下條件:

  • 如果S包含內嵌字段T,則S*S的方法集均包含含有接收器T的晉升方法。*S的方法集還包含含有接收器*T的晉升方法。
  • 如果S包含內嵌字段*T,則S*S的方法集均包含含有接收器T*T的晉升方法。

三 命名衝突問題

內嵌類型會引入命名衝突的問題,但解決規則卻很簡單。
首先,字段或方法X會隱藏該類型中更深層嵌套的其它項X。如下面示例中類型C與類型B中的字段F3;類型C中的字段F5與類型B中的方法F5

其次,若相同的嵌套層級上出現同名衝突,通常會產生一個錯誤。例如類型C中已經有一個字段B,則試圖再爲類型C定義一個方法B就會出錯。然而,若重名永遠不會在該類型定義之外的程序中使用,那就不會出錯,例如CAB間接引入兩個同名字段F2,但只要不調用v.F2,就不會產生錯誤。這種限定能夠在外部嵌套類型發生修改時提供某種保護。因此,就算添加的字段與另一個子類型中的字段相沖突,只要這兩個相同的字段永遠不會被使用就沒問題。

注:重名定義發生在最外層嵌套時,定義即會出錯,其他更深入的嵌套在定義時不會出錯,但使用時會出錯。參考示例中的方法B(最外層重名)與字段F2(第二層重名)。

命名衝突示例:

package main

import "fmt"

func main() {
	v := C{A{1, 2}, B{"test", 3, 4}, 5, 6}

	fmt.Printf("%v,%T\n", v, v)
	fmt.Println(v.B.F3, v.F3) // 結果:3 5。C中的字段F3覆蓋了B中的字段F3
	fmt.Println(v.F5)         // 結果:6。C中的F5會覆蓋內嵌類型B中的F5,故F5是一個字段,不是方法
	// v.F5()            // 編譯錯誤,方法被覆蓋掉了: cannot call non-function v.F5 (type int),
	v.F6()   // 可以直接調用內嵌類型B中的方法
	v.F7()   // C中的方法F7覆蓋了B中的方法F7
	v.B.F7() // 由於被覆蓋,需要通過字段B間接調用B的方法
	// fmt.Println(v.F2) // 編譯錯誤,分不清F2到底是指的A中的還是B中的:ambiguous selector v.F2
	fmt.Println(v.A.F2) // 間接調用沒問題
	fmt.Println(v.B.F2) // 間接調用沒問題
}

type A struct {
	F1 int
	F2 int
}

type B struct {
	F2 string
	F3 int
	F4 int
}

type C struct {
	A
	B
	F3 int
	F5 int
}

func (b B) F5() {
	fmt.Println("printed from B's method F5().")
}

func (b B) F6() {
	fmt.Println("printed from B's method F6().")
}

func (b B) F7() {
	fmt.Println("printed from B's method F7().")
}

func (c C) F7() {
	fmt.Println("printed from C's method F7().")
}

// 編譯錯誤:type C has both field and method named B
// func (c C) B() {
// 	fmt.Println("printed from C's method B().")
// }

四 方法的繼承性

我們已經知道,已定義的類型可以有對應的方法。但是,新定義的類型並不會從創建它的非接口類型中繼承任何方法。接口類型或複合類型(例如內嵌)元素的方法集則保持不變:

// Mutex 是一個有兩個方法的數據類型:Lock 和 Unlock
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex和Mutex具有相同的結構(注意是不同的類型),但它的方法集是空的。
type NewMutex Mutex

// PtrMutex的底層類型*Mutex的方法集保持不變,但PtrMutex的方法集是空的。
type PtrMutex *Mutex

// *PrintableMutex 的方法集包含它的內嵌字段Mutex的方法集Lock 和 Unlock
type PrintableMutex struct {
	Mutex
}

// crypto.cipher.Block
type Block interface {
        // BlockSize returns the cipher's block size.
        BlockSize() int

        // Encrypt encrypts the first block in src into dst.
        // Dst and src must overlap entirely or not at all.
        Encrypt(dst, src []byte)

        // Decrypt decrypts the first block in src into dst.
        // Dst and src must overlap entirely or not at all.
        Decrypt(dst, src []byte)
}

// MyBlock是一個接口類型,具有與Block一樣的方法集
// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block

五 選擇器

對於表達式x(不是包名),如下選擇器表達式:
x.f
表示值x(或者*x)的方法或字段。標識符f被稱爲選擇器,選擇器不能爲空白標識符。f的類型即選擇器表達式的類型。

選擇器f可以表示爲類型T的字段或方法,或者它可以指代嵌套的嵌入字段T的字段或方法f。遍歷到f的嵌入字段的數量在T中稱爲其深度。在T中聲明的字段或方法f的深度爲零。在T中的嵌入字段A中聲明的字段或方法f的深度是A中的f的深度加1

以下規則適用於選擇器:

  1. 對於類型爲T*T的值x,其中T不是指針或接口類型,x.f表示在T中最淺的深度處的字段或方法f:如果沒有一個具有最淺深度的f,則選擇器表達式是非法的。
  2. 對於接口類型I的值xx.f表示動態值x的實際方法f。如果在I的方法集中沒有名稱爲f的方法,則選擇器表達式是非法的。
  3. 一個例外情況是,如果x的類型是已定義的指針類型,而且(*x).f是表示字段(而不是方法)的有效選擇器表達式,則x.f(*x).f的簡寫。
  4. 除此之外,所有其他情況下,x.f都是非法的。
  5. 如果x是指針類型且值爲nil,且x.f表示結構字段,則爲x.f賦值或對其求值會導致運行時panic。
  6. 如果x是接口類型且值爲nil,則調用方法x.f或對其求值會導致運行時panic。

參考:
https://golang.org/doc/effective_go.html#embedding
https://golang.org/ref/spec#Struct_types
https://golang.org/ref/spec#Type_declarations
https://golang.org/ref/spec#Selectors

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章