本节读书笔记对应原书第六章。
6.1方法声明
在函数声明时,在其名字之前放上一个变量,就是一个方法。
package geometry
import "math"
type Point struct{X,Y float64}
func (p Point)Distance(q Point) float64{
return math.Hypot(q.X-p.X,q.Y-p.Y)
}
func Distance(q,p Point) float64{
return math.Hypot(q.X-p.X,q.Y-p.Y)
}
这里提供了两个Distance
,第一个Distance
就是一个方法,附加的参数p
是方法的接收器,表示Distance
属于Point
这种类型的独有方法,第二个Distance
就是一个传统的函数。早期的说法:调用一个方法称为向一个对象发送消息。
我们可以任意选择接收器的名字,但建议接收器的名字要统一和简短,此外,一般是不适用this
或者self
作为接收器的。
下面看一下这两种方式如何被使用的。
p:=Point{1,2}
q:=Point{4,6}
fmt.Println(Distance(p,q))//使用包级别的函数Distance
fmt.Println(p.Distance(q))//使用Point类下生命的Point.Distance方法
这里多说一句,如果在包外调用的时候,使用方法会比函数更简短,因为调用包级别的函数需要写包名,举个例子。
import "gopl.io/ch6/geometry"
perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
fmt.Println(geometry.Path.Distance(perim)) // "12", standalone function
fmt.Println(perim.Distance()) // "12", method of geometry.Path
总结:
- 方法可以被声明到任意类型,只要不是一个指针或者一个interface就行。
- 对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名。
6.2 基于指针对象的方法
当调用一个函数的时候,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大,那么应使用指针来声明方法。
type Point2 struct{X,Y float64}
func (p *Point2) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
该方法的名字就是(*Point).ScaleBy。如果Point2
这个类有一个指针作为接收器的方法,那么所有Point2
的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。如果一个类型本身就是一个指针的话,那是不能出现在接收器中的,就像下面这样会出现compile error
。
type P *int
func (P)f(){
//do something....
}
调用指针类型方法有3种方法,以调用指针类型方法(*Point2).ScaleBy为例:
//first
r := &Point2{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
//second
p := Point2{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p) // "{2, 4}"
//third
p := Point2{1, 2}
(&p).ScaleBy(2)
fmt.Println(p) // "{2, 4}"
其实还有简短的写法:
p:= Point2{1, 2}
p.ScaleBy(2)
编译器会隐式用&p
调用ScaleBy
方法,不过这种简写只适合变量。不能通过一个无法取到地址的接收器(比如临时变量的内存地址)来调用指针方法。
Point{1, 2}.ScaleBy(2) // compile error
注意:
- 不管方法的接收者是指针类型还是非指针类型,都可以通过指针/非指针类型调用,编译器会帮助做类型转换;
- 在声明一个method的接收者应该是指针还是非指针的时候,需要考虑对象本身是不是特别大,如果声明为非指针变量的时候,调用会产生一次拷贝,如果使用指针作为接收者,那么指针指向的始终是一块内存地址,即使进行了拷贝。
6.3 通过嵌入结构体来扩展类型
import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
func (p Point)Distance(q Point) float64{
return math.Hypot(q.X-p.X,q.Y-p.Y)
}
Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段 ,可以把ColoredPoint类型当作接收器来调用Point里的方法,Point类的方法也被引入了ColoredPoint 。
Distance有一个参数是Point类型, 但q虽然有Point这个内嵌类型,但它并不是Point
类型,使用时要显式地选择。
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
p.Distance(q.Point)//right
结构体中也可以内嵌一个类型的指针,这种情况下字段和方法会被间接引入到当前类型中,访问的时候通过这个指针指向的对象去取。
type ColoredPoint struct {
*Point
Color color.RGBA
}
p:= ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
-
通过嵌入的字段就是当前类自身的字段
-
把当前类型当作接收器来调用嵌入结构体里的方法,即使当前类型没有声明这些方法也就可以的。
6.4 方法值和方法表达式
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q))
fmt.Println(p.Distance(q))
为了说明这两个概念,还是先看个例子。我们调用一个方法一般采用p.Distance(q)
形式一步到位,但实际上等价于两步,首先将选择器返回的方法值保存到一个变量中distanceFromP := p.Distance
,这其实是将方法绑定到特定接收器变量的函数,然后调用时直接传入参数就可以了distanceFromP(q)
。和之前的方式比,似乎少了一个接收器p
,其实我们之前已经指定过了。
下面说方法表达式,方法表达式就是刚刚的p.distance
,方法表达式返回的是方法值。如果T是一个类型,方法表达式可能写作T.f或者(*T).f
,如何使用方法表达式最合适呢?
使用方法表达式,可以根据选择来调用接收器各不相同的方法。如下所示,我们可以为Path数组中的每一个Point来调用对应的Sub或者Add方法。
type Point struct{ X, Y float64 }
func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }
type Path []Point
func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
// Call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}