1. 程式人生 > >結構體及其方法的使用法門

結構體及其方法的使用法門

結構體表示的是實實在在的資料結構,一個結構體型別可以包含若干個欄位,每個欄位通常都需要有確切的名字和型別。結構體也可以不包含任何欄位,即使這樣的結構體也是有意義的,因為,我們可以為結構體關聯上一些方法。

函式和結構體的區別

方法可以看做是函式的特殊版本。

函式是獨立的程式實體。 我們可以宣告有名字的函式,也可以宣告沒有名字的函式,還可以把函式當做普通的值來傳遞。我們還可以把具有相同簽名的函式抽象成獨立的函式型別。

方法需要有名字,不能被當做值來看待,最重要的是:方法必須隸屬於某一個型別。 方法所述的型別會通過方法宣告中的接收者(receiver)宣告體現出來。

方法宣告

func (接收者名稱 接收者型別) methodName(引數列表)(結果列表)

舉個例子

// AnimalCategory 代表動物分類學中的基本分類法。
type AnimalCategory struct {
	kingdom string // 界。
	phylum string // 門。
	class  string // 綱。
	order  string // 目。
	family string // 科。
	genus  string // 屬。
	species string // 種。
}

func (ac AnimalCategory) String() string {
	return fmt.Sprintf("%s%s%s%s%s%s%s",ac.kingdom, ac.phylum, ac.class, ac.order,ac.family, ac.genus, ac.species)
}

func main {
	category := AnimalCategory{species: "cat"}
	fmt.Printf("The animal category: %s\n", category)
}

從String方法的宣告中可以得知,String方法隸屬於AnimalCategory型別。

在Go語言中,我們可以通過為一個型別編寫String方法,來自定義該型別的字串表示形式。

呼叫fmt.Printf函式時,使用佔位符%s和category值本身就可以打印出category的字串表示形式,無需顯示呼叫category的String方法。fmt.Printf函式會自動就尋找String函式。即使一個型別沒有實現String方法,fmt.Printf也能按照預設的格式打印出該型別的字串表示。

方法隸屬的型別其實並不侷限於結構體型別,但是必須是某個自定義的資料型別,並且不能是任何介面型別。

一個數據型別關聯的所有方法,共同組成了該型別的方法集合。同一個方法集合中的方法不能出現重名。並且,如果它們所屬的是一個結構體型別,那麼它們的名稱與該型別中任何欄位的名稱也不能重複。

我們可以把結構體型別中的一個欄位看作是它的一個屬性或者一項資料,再把隸屬於它的一個方法看作是附加在其中資料之上的一個能力或者一項操作。將屬性及其能力(或者說資料及其操作)封裝在一起,是面向物件程式設計(object-oriented programming)的一個主要原則。

Go 語言攝取了面向物件程式設計中的很多優秀特性,同時也推薦這種封裝的做法。從這方面看,Go語言其實是支援面向物件程式設計的,但它選擇摒棄了一些在實際運用過程中容易引起程式開發者困惑的特性和規則。

匿名欄位

type Animal struct {
	scientificName string // 學名。
	AnimalCategory    // 動物基本分類。
}

Go語言規範規定,如果一個欄位的宣告中只有欄位的型別名而沒有欄位的名稱,那麼它就是一個嵌入欄位,也可以被稱為匿名欄位。我們可以通過此型別變數的名稱後跟“.”,再後跟嵌入欄位型別的方式引用到該欄位。也就是說,嵌入欄位的型別既是型別也是名稱。

在某個代表變數的識別符號的右邊加“.”,再加上欄位名或方法名的表示式被稱為選擇表示式,它用來表示選擇了該變數的某個欄位或者方法。

嵌入欄位的方法集合會被無條件地合併進被嵌入型別的方法集合中。

只要名稱相同,無論這兩個方法的簽名是否一致,被嵌入型別的方法都會“遮蔽”掉嵌入欄位的同名方法。

類似的,由於我們同樣可以像訪問被嵌入型別的欄位那樣,直接訪問嵌入欄位的欄位,所以如果這兩個結構體型別裡存在同名的欄位,那麼嵌入欄位中的那個欄位一定會被“遮蔽”

多層嵌入的問題

在這種情況下,“遮蔽”現象會以嵌入的層級為依據,嵌入層級越深的欄位或方法越可能被“遮蔽”。

如果處於同一個層級的多個嵌入欄位擁有同名的欄位或方法,那麼從被嵌入型別的值那裡,選擇此名稱的時候就會引發一個編譯錯誤,因為編譯器無法確定被選擇的成員到底是哪一個

Go 語言是用嵌入欄位實現了繼承嗎?

強調一下,Go 語言中根本沒有繼承的概念,它所做的是通過嵌入欄位的方式實現了型別之間的組合。

型別之間的組合採用的是非宣告的方式,我們不需要顯式地宣告某個型別實現了某個介面,或者一個型別繼承了另一個型別。

型別組合也是非侵入式的,它不會破壞型別的封裝或加重型別之間的耦合。我們要做的只是把型別當做欄位嵌入進來,然後坐享其成地使用嵌入欄位所擁有的一切。如果嵌入欄位有哪裡不合心意,我們還可以用“包裝”或“遮蔽”的方式去調整和優化。

型別間的組合也是靈活的,我們總是可以通過嵌入欄位的方式把一個型別的屬性和能力“嫁接”給另一個型別。這時候,被嵌入型別也就自然而然地實現了嵌入欄位所實現的介面。

組合要比繼承更加簡潔和清晰,Go 語言可以輕而易舉地通過嵌入多個欄位來實現功能強大的型別,卻不會有多重繼承那樣複雜的層次結構和可觀的管理成本。

介面型別之間也可以組合。在 Go 語言中,介面型別之間的組合甚至更加常見,我們常常以此來擴充套件介面定義的行為或者標記介面的特徵。

值方法和指標方法都是什麼意思?有什麼區別?

方法的接收者型別必須是某個自定義的資料型別,而且不能是介面型別或介面的指標型別。所謂的值方法,就是接收者型別是非指標的自定義資料型別的方法。

func (cat *Cat) SetName(name string) {
	cat.name = name
}

func (cat Cat) SetNameOfCopy(name string) {
	cat.name = name
}

方法setName的接收者型別是Cat。表示的Cat型別的指標型別。這時,Cat可以叫做Cat的基本型別。指標型別的值表示的是指向某個基本型別值的指標。

值方法和指標方法之間有什麼不同點呢?

值方法的接收者是該方法所屬的那個型別值的一個副本。我們在該方法內對該副本的修改一般都不會體現在原值上,除非這個型別本身是某個引用型別(比如切片或字典)的別名型別。

而指標方法的接收者,是該方法所屬的那個基本型別值的指標值的一個副本。我們在這樣的方法內對該副本指向的值進行修改,卻一定會體現在原值上。

一個自定義資料型別的方法集合中僅會包含它的所有值方法,而該型別的指標型別的方法集合卻囊括了前者的所有方法,包括所有值方法和所有指標方法

我們在這樣的基本型別的值上只能呼叫到它的值方法。但是,Go 語言會適時地為我們進行自動地轉譯,使得我們在這樣的值上也能呼叫到它的指標方法。

如果一個基本型別和它的指標型別的方法集合是不同的,那麼它們具體實現的介面型別的數量就也會有差異,除非這兩個數量都是零。

以上結論可以結合下面的例子進行體會。

package main

import "fmt"

type Cat struct {
	name           string // 名字。
	scientificName string // 學名。
	category       string // 動物學基本分類。
}

func New(name, scientificName, category string) Cat {
	return Cat{
		name:           name,
		scientificName: scientificName,
		category:       category,
	}
}

func (cat *Cat) SetName(name string) {
	cat.name = name
}

func (cat Cat) SetNameOfCopy(name string) {
	cat.name = name
}

func (cat Cat) Name() string {
	return cat.name
}

func (cat Cat) ScientificName() string {
	return cat.scientificName
}

func (cat Cat) Category() string {
	return cat.category
}

func (cat Cat) String() string {
	return fmt.Sprintf("%s (category: %s, name: %q)",
		cat.scientificName, cat.category, cat.name)
}

func main() {
	cat := New("little pig", "American Shorthair", "cat")
	cat.SetName("monster") // (&cat).SetName("monster")
	fmt.Printf("The cat: %s\n", cat)

	cat.SetNameOfCopy("little pig")
	fmt.Printf("The cat: %s\n", cat)

	type Pet interface {
		SetName(name string)
		Name() string
		Category() string
		ScientificName() string
	}

	_, ok := interface{}(cat).(Pet)
	fmt.Printf("Cat implements interface Pet: %v\n", ok)
	_, ok = interface{}(&cat).(Pet)
	fmt.Printf("*Cat implements interface Pet: %v\n", ok)
}

思考題

  1. 我們可以在結構體型別中嵌入某個型別的指標型別嗎?如果可以,有哪些注意事項?

我們可以在結構體中嵌入某個型別的指標型別, 它和普通指標類似,預設初始化為nil,因此在用之前需要人為初始化,否則可能引起錯誤

  1. 字面量struct{}代表了什麼?又有什麼用處?

空結構體不佔用記憶體空間,但是具有結構體的一切屬性,如可以擁有方法,可以寫入channel。所以當我們需要使用結構體而又不需要具體屬性時可以使用它。