10 KiB
10.6 方法
10.6.1 方法是什么
在Go中,结构体就像是类的一种简化形式,那么OO程序员可能会问:类的方法在哪里呢?在Go中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go方法是作用在接受者(receiver)上的一个函数,接受者是某种类型的变量。因此方法是一种特殊类型的函数。
接受者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是int、bool、string或数组的alias类型。但是接受者不能是一个接口类型(参考 第11章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type…
最后接受者不能是一个指针类型,但是它可以是任何其他允许类型的指针。
一个类型加上它的方法等价于OO中的一个类。一个重要的区别是:在Go中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
类型T(或T)上的所有方法的集合叫做类型T(或T)的方法集。
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接受者类型,是有重载的:具有同样名字的方法可以在2个或多个不同的接受者类型上存在,比如在同一个包里这么做是允许的:
func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix
alias类型不能有它原始类型上已经定义过的方法。
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func关键字之后的括号中指定receiver。
如果recv是receiver的实例,Method1是它的方法名,那么方法调用遵循传统的object.name选择器符号:recv.Method1()
如果recv一个指针,Go会自动解引用。
如果方法不需要使用recv的值,可以用*_*替换它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
recv就像是OO语言中的this或self,但是Go中并没有这两个关键字。随个人喜好,你可以使用this或self作为receiver的名字。下面是一个结构体上的简单方法的例子:
Listing 10.10—method .go
package main
import "fmt"
type TwoInts struct {
a int
b int
}
func main() {
two1 := new(TwoInts)
two1.a = 12
two1.b = 10
fmt.Printf("The sum is: %d\n", two1.AddThem())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))
two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}
输出:
The sum is: 22
Add them to the param: 42
The sum is: 7
下面是非结构体类型上方法的例子:
Listing 10.11—method2.go:
package main
import "fmt"
type IntVector []int
func (v IntVector) Sum() (s int) {
for _, x := range v {
s += x
}
return
}
func main() {
fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
}
- 练习 10.6:* employee_salary.go
定义结构体employee,它有一个salary字段,给这个结构体定义一个方法giveRaise来按照指定的百分比增加薪水。
- 练习 10.7:* iteration_list.go
下面这段代码有什么错?
package main
import "container/list"
func (p *list.List) Iter() {
// ...
}
func main() {
lst := new(list.List)
for _= range list.Iter() {
}
}
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在int、float或类似这些的类型上定义方法。试图在int类型上定义方法会得到一个编译错误:
cannot define new methods on non-local type int
比如想在time.Time上定义如下方法:
func (t time.Time) first3Chars() string {
return time.LocalTime().String()[0:3]
}
类型在在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。
但是有一个绕点的方式:可以先定义该类型(比如int,float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
Listing 10.12—method_on_time.go:
package main
import (
"fmt"
"time"
)
type myTime struct {
time.Time //anonymous field
}
func (t myTime) first3Chars() string {
return t.Time.String()[0:3]
}
func main() {
m := myTime{time.Now()}
// 调用匿名Time上的String方法
fmt.Println("Full time now:", m.String())
// 调用myTime.first3Chars
fmt.Println("First 3 chars:", m.first3Chars())
}
/* Output:
Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011
First 3 chars: Mon
*/
10.6.2 函数和方法的区别
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
在接受者是指针时,方法可以改变接受者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
!!不要忘记Method1后边的括号(),否则会引发编译器错误:*method recv.Method1 is not an expression, must be called *!!
接受者必须有一个显式的名字,这个名字必须在方法中被使用。
receiver_type叫做*(接受者)基本类型*,这个类型必须在和方法同样的包中被声明。
在Go中,(接受者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接受者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
10.6.2 指针或值作为接受者
鉴于性能的原因,recv最常见的是一个指向receiver_type的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在receiver类型是结构体时,就更这样了。
如果想要方法改变接受者的数据,就在接受者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
下面的例子pointer_value.go作了说明:change()接受一个指向B的指针,并改变它内部的成员;write()接受通过拷贝接受B的值并只输出B的内容。注意Go为我们做了探测工作,我们自己并没有指出是是否在指针上调用方法,Go替我们做了这些事情。b1是值而b2是指针,方法都支持运行了。
Listing 10.13—pointer_value.go:
package main
import (
"fmt"
)
type B struct {
thing int
}
func (b *B) change() { b.thing = 1 }
func (b B) write() string { return fmt.Sprint((b)) }
func main() {
var b1 B // b1是值
b1.change()
fmt.Println(b1.write())
b2 := new(B) // b2是指针
b2.change()
fmt.Println(b2.write())
}
/* 输出:
{1}
{1}
*/
试着在write()中改变接受者b的值:将会看到它可以正常编译,但是开始的b没有被改变。
我们知道方法不需要指针作为接受者,如下面的例子,我们只是需要Point3的值来做计算:
type Point3 struct { x, y, z float }
// A method on Point3
func (p Point3) Abs float {
return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}
这样做稍微有点昂贵,因为Point3是作为值传递给方法的,因此传递的是它的拷贝,这在Go中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。
假设p3定义为一个指针:* p3 := &Point{ 3, 4, 5}*
可以这样写: * p3.Abs() 来替代 (*p3).Abs() *
像例子10.11(method1.go)中接受者类型是*TwoInts的方法AddThem(),它能在类型TwoInts的值上被调用,这是自动间接发生的。
因此two2.AddThem可以替代(&two2).AddThem()。
在值和指针上调用方法:
可以有连接到类型的方法,也可以有连接到类型指针的方法。
但是这没关系:对于类型T,如果在T上存在方法Meth(),并且t是这个类型的变量,那么t.Meth()会被自动转换为(&t).Meth().*
指针方法和值方法都可以在指针或非指针上被调用,如下面程序所示,类型List在值上有一个方法Len(),在指针上有一个方法Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。
Listing 10.14—methodset1.go:
package main
import (
"fmt"
)
type List []int
func (l List) Len() int { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }
func main() {
// 值
var lst List
lst.Append(1)
fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1)
// 指针
plst := new(List)
plst.Append(2)
fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1)
}
** 10.6.4 方法和未导出字段
考虑person2.go中的person包:类型Person被明确的导出了,但是它的字段没有被导出。例如在use_person2.go中p.firsetname就是错误的。该如何在另一个程序中修改或者只是读取一个Person的名字呢?
这可以通过OO语言一个众所周知的技术来完成:提供getter和setter方法。对于setter方法使用Set前缀,对于getter方法只适用成员名。
Listing 10.15—person2.go:
package person
type Person struct {
firstName string
lastName string
}
func (p *Person) FirstName() string {
return p.firstName
}
func (p *Person) SetFirstName(newName string) {
p.firstName = newName
}
Listing 10.16—use_person2.go:
package main
import (
"./person"
"fmt"
)
func main() {
p := new(person.Person)
// p.firstName undefined
// (cannot refer to unexported field or method firstName)
// p.firstName = "Eric"
p.SetFirstName("Eric")
fmt.Println(p.FirstName()) // Output: Eric
}
并发访问对象:
对象的字段(属性)不应该由2个或2个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包sync(参考9.3)中的方法。在14.17我们会通过goroutines和channels探索另一种方式。
** 嵌入类型上的方法和继承 // TODO
链接
- 目录
- 上一节:10.5 匿名字段和内嵌结构体
- 下一节:10.7 TODO