教女友寫方法(續)

『就要學習 Go 語言』系列--第 24 篇分享文章html

以前的文章給你們總結過方法的一些基本用法,最近在學 Go 面向對象式編程,對方法又有一些新的認識,總結一下。
方法分爲值方法和指針方法,這篇文章主要來說講這二者的區別。二者的定義:接收者類型爲 T 的方法稱爲值方法;接收者類型爲 *T 的方法稱爲指針方法 其中 T 必須知足以下條件:golang

  1. T 必須是自定義類型;
  2. T 的定義必須與方法的聲明在同一個包內;
  3. T 不能是接口類型或者接口指針類型;

能夠認爲 T 是 *T 的基本類型。編程

方法的接收者是副本

以前也講過這個,值方法的接收者是原類型值的副本,指針方法的接收者是原類型值的指針副本。在值方法內對副本修改不會影響到原值,注意有例外,除非這個類型是引用類型的別名類型,例如切片、字典。而在指針方法內,對指針副本指向的值作的修改必定會體如今原值上。bash

type Book struct {
	pages int
}

type Books []Book

func (books Books)modify()  {
	// 原值已被修改
	books[0].pages = 188
	// 下面這行代碼不會修改原值
	books = append(books,Book{234})
}

func main() {
	books := Books{
		{123},
		{456},
	}
	books.modify()
	fmt.Println(books)    // 輸出:[{188} {456}]
}
複製代碼

輸出併發

[{188} {456}]
複製代碼

append() 調用不會影響到原值是由於該操做會從新申請一塊內存存放接收者 books,但不會影響到原值。咱們只將這兩行代碼換下順序,其餘代碼不變:app

func (books Books)modify()  {
	fmt.Println(books)     // [{123} {456}]
	books = append(books,Book{234})
	fmt.Println(books)     // [{123} {456} {234}]
	books[0].pages = 188	
	fmt.Println(books)	   // [{188} {456} {234}]
}

func main() {
	books := Books{
		{123},
		{456},
	}
	books.modify()
	fmt.Println(books)    // [{123} {456}]
}
複製代碼

輸出函數

[{123} {456}]
[{123} {456} {234}]
[{188} {456} {234}]
[{123} {456}]
複製代碼

從結果能夠看出,上面代碼段的第 5 行,這個賦值操做隻影響新建立的切片,不會影響到原來的值。
若是想讓方法內的修改體如今原值上,可使用指針接收者。post

func (books *Books) modify() {
	*books = append(*books, Book{234})
	(*books)[0].pages = 188
}

func main() {
	books := Books{
		{123},
		{456},
	}
	books.modify()
	fmt.Println(books)    // [{188} {456} {234}]
}
複製代碼

輸出:學習

[{188} {456} {234}]
複製代碼

每一個方法都對應一個隱式函數

咱們給結構體聲明兩個方法:ui

func (b Book) Pages() int {
	return b.pages
}
func (b *Book) SetPages(pages int) {
	b.pages = pages
}
複製代碼

上面代碼聲明瞭兩個方法,一個值方法,一個指針方法。
每一個方法聲明的時候,編譯器會各自聲明相對應的隱式函數。例如上面這兩個方法對應的隱式函數是:

func Book.Pages(b Book) int {
	return b.pages
}

func (*Book).SetPages(b *Book, pages int) {
	b.pages = pages
}
複製代碼

從代碼能夠看出,接收者被當作形參,函數體與方法體依然保持一致。看下函數名 Book.Pages 和 (*Book).SetPages,能夠看做是 Type.MethodName 的結果,但函數不能包含特殊字符,因此這兩個函數不能顯式聲明,但咱們能夠調用這兩個函數。

type Book struct {
	pages int
}
func (b Book) Pages() int {
	return b.pages
}
func (b *Book) SetPages(pages int) {
	b.pages = pages
}

func main() {
	var book Book
	(*Book).SetPages(&book, 188)
	fmt.Println(Book.Pages(book)) // 188
}
複製代碼

事實上,編譯器不只隱式聲明瞭方法對應的函數,並且還重寫了方法,讓聲明的方法去調用隱式聲明的函數,就像下面這樣:

func (b Book) Pages() int {
	return Book.pages(b)
}
func (b *Book) SetPages(pages int) {
	(*Book).SetPages(b, pages)
}
複製代碼

方法集

理解方法集很是重要,來理一下,方法集是一組關聯到自定義類型的值或指針的方法。一個自定義類型 T 的方法集合僅包括它的值方法,該類型的指針類型 *T 的方法集包括全部的值方法和指針方法。例如:

type Dog struct {
	Name string
}
func (d Dog) getName() string {
	return d.Name
}
func (d *Dog) SetName(name string) {
	d.Name = name
}
複製代碼

自定義類型 Dog,聲明瞭值方法 getName() 和指針方法 SetName()。Dog 類型的方法集合只包括值方法,即 getName(),而 *Dog 類型的方法集合包含這兩個方法。

嚴格意義上來講,基本類型 Dog 只能調用它的值方法,但實際寫代碼的時候,也能夠經過 Dog 類型調用到指針方法,是由於編譯器爲自動爲咱們轉譯了。

func main() {
	var dog Dog
	dog.SetName("dog")     // 經過 Dog 類型調用指針方法 
	//(&dog).SetName("dog")
	fmt.Println(dog.Name)
}
複製代碼

咱們來看一個經典的例子,對比下:

// 定義接口 notifier
type notifier interface {
	notify()
}

type user struct {
	name string
	email string
}

func (u *user)notify()  {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

// 接收一個 notifier 接口類型的參數,併發送通知
func sendNotification(n notifier)  {
	n.notify()
}

func main() {
	u := user{"Seeklaod", "email@gmail.com"}
	sendNotification(u)
}
複製代碼

上面的代碼定義了接口類型 notifier,只包含一個方法 notify(),實現了該方法的類型就認爲實現了接口 notifier,*user 類型實現了該方法,即實現了接口。另外還定義了一個函數 sendNotification(),該函數接收一個接口類型的值。 編譯運行下程序,發現報錯:

cannot use u (type user) as type notifier in argument to sendNotification:
user does not implement notifier (notify method has pointer receiver)
複製代碼

兩個錯誤:

  1. 不能將 u ( type user) 做爲參數傳遞給參數類型爲 notifier 函數 sendNotification();
  2. user 類型沒有實現接口 notifier;

其實主要就是由於第 2 個錯誤引發的,上面已經講過是 *user 實現了接口 notifier。

使用 *user 類型實現接口時爲何 user 類型沒法實現接口呢?這就須要瞭解上面說過的方法集,關於方法集,Go 語言規範是這樣定義的:

Values Method Receivers
T (t T)
*T (t T) 或 (t *T)

是這個意思,自定義類型 T 的方法集合僅包括它的值方法,而該類型的指針類型 *T 的方法集包括全部的值方法和指針方法,上面也講過。對應到本例子,user 類型的方法集不包括方法 notify(),也就沒有實現接口 notifier。

如今知道問題出在哪了,修改下程序,讓程序跑起來:

func main() {
	u := user{"Seeklaod", "email@gmail.com"}
	sendNotification(&u)  // 傳入地址
}
複製代碼

上面的代碼編譯經過。由於使用指針接收者實現的接口,只有 user 類型的指針能夠傳給 sendNotification 函數。

如今的問題是,爲何這種狀況下,編譯器沒有爲咱們自動轉譯呢?事實上,編譯器並非總能自動得到一個值的地址,這就是其中一種。

由於不是總能獲取一個值的地址,因此值的方法集只包括了使用值接收者實現的方法。

但願這篇文章可以幫助你,Good day!

參考資料:
1.教女友寫方法
2.Methods in Go
3.Methods, Interfaces and Embedded Types in Go


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公衆號「Golang來啦」或者移步 seekload.net ,查看更多精彩文章。

給你準備了學習 Go 語言相關書籍,公號後臺回覆【電子書】領取!

公衆號二維碼
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息