教女朋友写方法(续)

901 阅读6分钟

『就要学习 Go 语言』系列--第 24 篇分享文章

之前的文章给大家总结过方法的一些基本用法,最近在学 Go 面向对象式编程,对方法又有一些新的认识,总结一下。
方法分为值方法和指针方法,这篇文章主要来讲讲这两者的区别。两者的定义:接收者类型为 T 的方法称为值方法;接收者类型为 *T 的方法称为指针方法 其中 T 必须满足如下条件:

  1. T 必须是自定义类型;
  2. T 的定义必须与方法的声明在同一个包内;
  3. T 不能是接口类型或者接口指针类型;

可以认为 T 是 *T 的基本类型。

方法的接收者是副本

之前也讲过这个,值方法的接收者是原类型值的副本,指针方法的接收者是原类型值的指针副本。在值方法内对副本修改不会影响到原值,注意有例外,除非这个类型是引用类型的别名类型,例如切片、字典。而在指针方法内,对指针副本指向的值做的修改一定会体现在原值上。

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,但不会影响到原值。我们只将这两行代码换下顺序,其他代码不变:

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 行,这个赋值操作只影响新创建的切片,不会影响到原来的值。
如果想让方法内的修改体现在原值上,可以使用指针接收者。

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}]

每个方法都对应一个隐式函数

我们给结构体声明两个方法:

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 语言相关书籍,公号后台回复【电子书】领取!

公众号二维码