《GO语言圣经》读书笔记(五):方法

本节读书笔记对应原书第六章。

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)
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值