天天看點

050-指針接收器(Pointer Receiver)

學會了 Method 的基本定義和使用後,我們讨論一點更加複雜的話題。

1. 問題引入

type Person struct {
    Name string
    Age  int
}

// 定義 Person 的一個方法 hello
func (p Person) hello() {
    fmt.Printf("Hello, my name is %s. I'm %d years old\n", p.Name, p.Age)
}

func (p Person) grow() {
    p.Age += 1      

我們繼續使用上一篇的結構體定義。不過這裡新增加了一個新的方法 grow,它的目的是讓 Person 對象的年齡增加 1 歲。

好了,接下來寫一個測試程式:

allen := Person{"allen", 19}
allen.hello() // Output: Hello, my name is allen. I'm 19 years old
allen.grow()
allen.hello() // Output: Hello, my name is allen. I'm 19 years old      

很遺憾,調用 grow 方法後,我們發現 allen 的年齡并沒有變為 20 歲。

2. 指針接收器

前面問題的主要原因在于 Golang 中的函數傳參都是值拷貝。當你調用 ​

​allen.grow()​

​ 時,實際上是把 allen 對象複制了一份,将其『副本』傳遞給了 grow 方法的接收器,是以在 grow 内部修改的對象也是『副本』。

要想真正達到修改原對象的目的,接收器類型必須為該類型的指針。

我們稍加修改 grow 方法:

// 接收器類型是 Person 指針類型
func (p *Person) grow() {
    p.Age += 1      

如何調用呢?你可以這樣:

allen.hello() // Output: Hello, my name is allen. I'm 19 years old
(&allen).grow()
allen.hello() // Output: Hello, my name is allen. I'm 20 years old      

不過上面的寫法太笨拙,go 語言允許更加簡潔的寫法:

allen.hello() // Output: Hello, my name is allen. I'm 19 years old
allen.grow()  // 直接使用 allen 對象調用指針接收器的方法
allen.hello() // Output: Hello, my name is allen. I'm 20 years old      

Go 會自動的将其轉換成 ​

​(&allen).grow()​

​,而不需要你顯式的這樣去寫。

3. 類型和指針類型混用

接收器的實參是類型 T 和 *T,而接收器參數的類型也可以是 T 和 *T,這一組合,會出現 4 種情況

3.1 普通對象調用普通接收器方法

這種情況是我們所熟知的。

  • 簡記為​

    ​T -> (T)​

    ​ (說明:左邊的 T 是接收器實參類型,右邊括号裡的 T 是接收器參數類型)
allen := Person{"allen", 19}
allen.hello()      

3.2 對象指針調用指針接收器方法

  • 簡記為​

    ​*T -> (*T)​

allenptr := new(Person) // 等價于:allenptr := &Person{}      

3.3 普通對象調用指針接收器方法

這種形式第 2 節已經講過了。

  • 簡記為​

    ​T -> (*T)​

allen := Person{"allen", 19}
(&allen).grow() // OK,這相當于 3.2 節
allen.grow() // OK,Go 會幫我們引式轉換成上面的形式      

3.4 指針對象調用普通接收器方法

  • 簡記為​

    ​*T -> (T)​

allenptr := &Person{"allen", 19}
(*allenptr).hello() // OK,這相當于 3.1 節,使用 T -> (T)
allenptr.hello() // OK,Go 會幫我們引式轉換成上面的形式      

3.5 小結

看這到裡是不是感覺有點繞?不用擔心,我簡單總結一下:

  1. 不用再關心接收器的實參類型了
  2. 隻關心接收器形參類型
  3. 實參無論是普通對象還是對象指針,都可以直接調用任何方法,Go 會自動幫你識别

這樣一來你會輕松很多。唯一需要注意就是,形參為指針類型的方法,操作的是原始對象。

4. 幾個特殊的例子

type Second int

func (s *Second) double() {
    (*s) *= 2 // 直接修改值也是可以的
}

func (s Second) show() {
    fmt.Printf("I'm %d\n", s)
}

s := Second(2)
s.show()            // Output: I'm 2
s.double()
s.show()            // Output: I'm 4
Second(5).show()    // Output: I'm 5
Second(10).double() // Not OK! 編譯錯誤。無法擷取字面量位址。
(&Second(10)).double() // Not OK! 編譯錯誤。無法擷取字面量位址。      

注意上面定義了一個底層類型為 int 的新類型 Second. 使用字面量可以調用普通類型接收器的方法 show,但是不可以調用指針接收器類型的方法 double,因為你無法擷取字面量的位址。字面量是一個臨時變量。

但是對于 Person 類型來說,下面的調用是合法的:

(&Person{"allen", 19}).grow() // OK,等價于 allen := new(Person); allen.grow()      

因為 golang 對于結構體類型使用 ​

​&T{}​

​​ 這種操作來說,相當于調用 ​

​new(T)​

​​. (很遺憾的是使用 new 沒辦法為結構體類型賦初始,new 隻能賦予零值。是以使用 ​

​&T{...}​

​ 文法更加常見)下面的調用是非法的:

(Person{"allen", 19}).grow() // Not OK! 無法擷取字段量位址      

5. nil 指針作為接收器參數

type LinkList struct {
    value int
    next *LinkList
}

// 計算連結清單長度
func (l *LinkList) Size() int {
     // 先判斷接收器是否為 nil
     if l == nil {
         return 0
     }
     size := 0
     for l != nil {
         size += 1
         l = l.next
     }
     return      
list := LinkList{1, &LinkList{2, &LinkList{3, &LinkList{4, nil}}}}
fmt.Printf("list size: %d\n", list.Size()) // Output: list size: 4

var emptyList *LinkList
fmt.Printf("emptyList size: %d\n", emptyList.Size()) // Output: emptyList size: 0      

6. 總結

  • 掌握指針接收器方法
  • 了解指針接收器方法和普通類型接收器方法的差別
  • 知道如何處理接收器為 nil 的特殊情況