golang 方法

golang語言中的方法是與對象實例綁定的特殊函數,用於維護和展示對象的自身狀態。

與函數的區別是方法有前置實例接收參數(receiver),編譯器根據receiver來判斷該方法屬於哪個實例。receiver可以是基礎類型,也可以是指針類型,這會關係到是否需要有可以修改對象實例的能力。

在調用方法時,可以使用對象實例值或指針,編譯器會根據receiver類型自動在基礎類型和指針類型之間轉換,比如:

type rect struct {
    width, height, area int
}

func (r *rect) pointer() {
    r.width += 2
    r.area = r.width * r.height
}
func (r rect) value() {
    r.width += 4
    r.area = r.width * r.height
}

func main() {
    r := rect{width: 10, height: 5}
    r.value()
    fmt.Println(r)
    r.pointer()
    fmt.Println(r)
/*
    r := &rect{width: 10, height: 5}
    r.value()
    fmt.Println(r)
    r.pointer()
    fmt.Println(r)
*/ 
}
輸出:
{10 5 0}
{12 5 60}

如果使用指針調用方法時,需要注意不能使用多級指針,且必須使用合法的指針(包括nil)或能取實例地址,比如:

type X struct{}

func (x *X)test(){
    fmt.Println("hello gopher")
}

func main(){
    var x *X
    fmt.Println(x)
    x.test()
    &X{}.test() //cannot take the address of X literal
}


如何選擇方法的receiver類型?

- 如果要修改實例狀態,用*T

- 如果不需要修改實例狀態的小對象或固定值,建議用T

- 如果是大對象,建議用*T,可以減少複製成本

- 引用類型、字符串、函數等指針包裝對象,用T

- 如果對象實例中包含Mutex等同步字段,用*T, 以免因復製造成鎖無效

- 無法確定需求的情況,都用*T

通過匿名字段的方法訪問:

可以像訪問匿名字段成員那樣調用其方法,由編譯器負責查找,比如:

type person  struct{}

type Man struct{
    person
}
func (p person)toWork()string{
    return "Tom"
}
func main(){
    var m Man 
    fmt.Println(m.toWork())
}
輸出:
Tom

如果Man結構體也有個同名的toWork方法,此時調用邏輯如下,比如:

type person  struct{}

type Man struct{
    person
}

func (p person)toWork()string{
    return "Tom to work"
}

func (m Man)toWork()string {
    return "me to work"
}
func main(){
    var m Man
    fmt.Println(m.toWork())         //me to work
    fmt.Println(m.person.toWork())  //Tom to work
}

方法集:

GoLang規範中提到了一個與類型相關的方法集(method set),這決定了它是否實現了某個接口。

- 類型T方法集包含所有receiver T方法

- 類型*T方法集包含所有receiver T + *T的方法

- 匿名嵌入S,T方法集包含所有receiver S方法

- 匿名嵌入*S,T方法集包含所有receiver S + receiver *S方法

- 匿名嵌入*S或匿名嵌入S,*T方法集包含所有receiver S + receiver *S方法

type S struct{}

type T struct{
    S   
}

func (S) SVal()  {}  
func (*S) SPtr() {}
func (T) TVal()  {}  
func (*T) TPtr() {}

func methodSet(a interface{}) {
    t := reflect.TypeOf(a)
    for i, n := 0, t.NumMethod(); i < n; i++ {
        m := t.Method(i)
        fmt.Println(m.Name, m.Type)
    }   
}
 
func main() {
    var t T 
    methodSet(t)
    println("--------------")
    methodSet(&t)
}
輸出:
SVal func(main.T)
TVal func(main.T)
--------------
SPtr func(*main.T)
SVal func(*main.T)
TPtr func(*main.T)
TVal func(*main.T)

很顯然, 匿名字段就是爲擴展方法集準備的。否則沒有必要少寫個字段。這種組合沒有父子依賴關係,

整體與局部鬆耦合,可以任意增加來實現擴展。各單元互無關聯,實現與維護更加簡單。


方法表達式:

方法是一種特殊的函數,除了可以直接調用之外,還可以進行賦值或當作參數傳遞,下面是Go語言的方法定義格式,比如:

func (p mytype) funcname(q type) (r,s type) { return 0,0}

本質上這就是一種語法糖,方法調用如下:

instance.method(args) -> (type).func(instance, args)

instance 就是Reciever,左邊的稱爲Method Value;右邊則是Method Expression,Go推薦使用左邊形式。

Method Value是包裝後的狀態對象,總是與特定的對象實例關聯在一起(類似閉包),

而Method Expression會被還原成普通的函數,將Receiver作爲第一個顯式參數,調用時需額外傳遞。

二者本質上沒有區別,只是Method Value 看起來更像面向對象的格式,且編譯器會自動進行類型轉換;

Method Expression更直觀,更底層,編譯器不會進行類型轉換,會按照實際表達意義去執行,更易於理解。

Method Expression:

type Person struct {
    Age int 
    Name string
}

func (p Person) GetAge() int{
    return  p.Age
}

func (p *Person) SetAge(i int){
    p.Age =  i
}

func main(){
    p := Person{20, "Tom"}
    setAge := (*Person).SetAge   //(\*Person)必須用括號,整體相當於func(p *Person)
    setAge(&p,50)                  //編譯器不會進行類型轉換
    getAge := Person.GetAge
    fmt.Println(getAge(p))
}
輸出:
50

Method Value:

func main(){
    p := Person{20, "Tom"}
    setAge := p.SetAge      //編譯器會自動進行類型轉換
    setAge(50) 
    getAge := p.GetAge
    fmt.Println(getAge())
}

只要receiver參數類型正確,使用nil同樣可以執行,比如:

type N int 

func (n N) value(){
    println(n)
}
func (n *N) pointer(){
    println(n)
}

func main(){
    var n *N
    n.pointer()
    (*N)(nil).pointer()
    (*N).pointer(nil)
}

這樣寫程序並沒有什麼意義,只是希望你能理解並安全使用Method Value和Method Expression

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