gopl-zh.github.com/ch4/ch4-04-3.md

5.2 KiB
Raw Blame History

4.4.3. 結構體嵌入和匿名成員

在本節中我們將看到如何使用Go語言提供的不同尋常的結構體嵌入機製讓一個命名的結構體包含另一個結構體類型的匿名成員這樣就可以通過簡單的點運算符x.f來訪問匿名成員鏈中嵌套的x.d.e.f成員。

考慮一個二維的繪圖程序,提供了一個各種圖形的庫,例如矩形、橢圓形、星形和輪形等幾何形狀。這里是其中兩個的定義:

type Circle struct {
	X, Y, Radius int
}

type Wheel struct {
	X, Y, Radius, Spokes int
}

一個Circle代表的圓形類型包含了標準圓心的X和Y坐標信息和一個Radius表示的半徑信息。一個Wheel輪形除了包含Circle類型所有的全部成員外還增加了Spokes表示徑向輻條的數量。我們可以這樣創建一個wheel變量

var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

隨着庫中幾何形狀數量的增多,我們一定會註意到它們之間的相似和重複之處,所以我們可能爲了便於維護而將相同的屬性獨立出來:

type Point struct {
	X, Y int
}

type Circle struct {
	Center Point
	Radius int
}

type Wheel struct {
	Circle Circle
	Spokes int
}

這樣改動之後結構體類型變的清晰了,但是這種脩改同時也導致了訪問每個成員變得繁瑣:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字這類成員就叫匿名成員。匿名成員的數據類型必鬚是命名的類型或指向一個命名的類型的指針。下面的代碼中Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入到了Circle結構體同時Circle類型被嵌入到了Wheel結構體。

type Circle struct {
	Point
	Radius int
}

type Wheel struct {
	Circle
	Spokes int
}

得意於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑:

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

在右邊的註釋中給出的顯式形式訪問這些葉子成員的語法依然有效因此匿名成員併不是眞的無法訪問了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點操作符中是可選的。我們在訪問子成員的時候可以忽略任何匿名成員部分。

不幸的是,結構體字面值併沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過:

w = Wheel{8, 8, 5, 20}                       // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

結構體字面值必鬚遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的:

gopl.io/ch4/embed

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
	Circle: Circle{
		Point:  Point{X: 8, Y: 8},
		Radius: 5,
	},
	Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}

w.X = 42

fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}

需要註意的是Printf函數中%v參數包含的#副詞它表示用和Go語言類似的語法打印值。對於結構體類型來説將包含每個成員的名字。

因爲匿名成員也有一個隱式的名字因此不能同時包含兩個類型相同的匿名成員這會導致名字衝突。同時因爲成員的名字是由其類型隱式地決定的所有匿名成員也有可見性的規則約束。在上面的例子中Point和Circle匿名成員都是導出的。卽使它們不導出比如改成小寫字母開頭的point和circle我們依然可以用簡短形式訪問匿名成員嵌套的成員

w.X = 8 // equivalent to w.circle.point.X = 8

但是在包外部因爲circle和point沒有導出不能訪問它們的成員因此簡短的匿名成員訪問語法也是禁止的。

到目前未知,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法醣。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢?

答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員也可以用於訪問它們的方法。實際上外層的結構體不僅僅是獲得了匿名成員類型的所有成員而且也獲得了該類型導出的全部的方法。這個機製可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的核心我們將在6.3節中專門討論。