《Go语言圣经》学习笔记:6. 方法

6. 方法

6.1 方法的声明

在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

示例:两种Distance实现的效果是一样的

type Point struct {
	X, Y float64
}

func Distance(p, q Point) float64 {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

// 下面的p称为方法的接受器
func (p Point) Distance(q Point) float64  {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

func main() {
	p := Point{1, 5}
	q := Point{5, 2}
	fmt.Println(Distance(p, q))
	fmt.Println(p.Distance(q))
}

在go语言中有一个好处就是:可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型。比如:数值、字符串、slice、map

示例,对一个切片定义方法

type Path []Point

func (p Path) Distance() float64 {
	sum := 0.0
	for i, _ := range p {
		if i>0 {
			sum += p[i].Distance(p[i-1])
		}
	}
	return sum
}

func main() {
	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(perim.Distance())    // 12
}

6.2 基于指针对象的方法

在函数中,如果我们要对一个对象进行修改,或者给函数传参的参数对象内容过大,会选用传递指针的方式。因为函数在传递的时候是进行值传递,也就是将一个参数里边的变量在另外一个内存地址重新拷贝一份,然后再在函数中使用,而指针变量的值是一个地址,所以本质上也是进行只拷贝,只不过拷贝了一个地址,编译器可以通过这个地址去访问参数对象。

同样对于对象的方法,也可以实现基于指针对象的方法。

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

使用的时候也是很简单,就和上一小节的使用方法是一样的。

但是细分起来有几种情况:

  • 接收器的实际参数和其接收器的形式参数相同,比如两者都是类型T或者都是类型*T
  • 接收器实参是类型T,但接收器形参是类型*T,这种情况下编译器会隐式地为我们取变量的地址
  • 接收器实参是类型*T,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量

一句话总结就是无论什么的变量是普通的类型还是指针类型,或者实现的方法的接收器是普通类型还是指针类型,都可以通过点.来访问定义的方法

可以结合下面例子理解下:

type MyInt int

// 接受器i是一个指针类型
func (i *MyInt) pAdd(x int) MyInt {
	// 因为x和i虽然底层变量是一样的,但是其实是两种不同的变量了,所以不能直接相加,详见2.5类型
	return *i + MyInt(x)
}

// 接收器i只是一个普通类型
func (i MyInt) Add(x int) MyInt {
	return i + MyInt(x)
}

func main() {
	i := MyInt(20)
	pi := &i
	// 以下都正确
	fmt.Println(i.Add(1))		// 21
	fmt.Println(i.pAdd(2))		// 22
	fmt.Println(pi.Add(3))		// 23
	fmt.Println(pi.pAdd(4))		// 24
}

6.3 通过嵌入结构体来扩展类型

go语言中,嵌入结构体能够达到像其他面向对象的编程语言一样的继承。

在下面代码中结构体Point称为基类,通过匿名嵌入可以将Point嵌入到新的结构中,并且Point实现的方法能够被ColoredPoint所继承。关于嵌入结构体的其他一些细节可以看4.4节的结构体部分。

type Point struct{ X, Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

Point的方法继承给了ColoredPoint,所以ColoredPoint类型能够使用Distance函数

func main()  {
	red := color.RGBA{255, 0,0,1}
	green := color.RGBA{0, 255,0,1}
	cp1 := ColoredPoint{Point{1, 1}, red}
	cp2 := ColoredPoint{Point{4, 5}, green}

	fmt.Println(cp1.Distance(cp2.Point))
	fmt.Println(cp1.Distance(cp2)) // 错误
}

Distance函数是Point方法,参数也是Point类型,所以必须接受一个Point参数,否者会报错。

当然,也可以为ColoredPoint定制自己的Distance方法。

func (p *ColoredPoint) Distance(q *ColoredPoint) float64 {
	return p.Point.Distance(q.Point)
}

func main()  {
	red := color.RGBA{255, 0,0,1}
	green := color.RGBA{0, 255,0,1}
	cp1 := ColoredPoint{Point{1, 1}, red}
	cp2 := ColoredPoint{Point{4, 5}, green}

	fmt.Println(cp1.Distance(&cp2))				// 5
	fmt.Println(cp1.Point.Distance(cp2.Point))	// 5
	fmt.Println(cp1.Distance(cp2.Point))   		// 错误
}

上述代码中,可以看到ColoredPoint拥有了自己的Distance方法,并且该方法可以接受*ColoredPoint的参数。但是原来的Point的Distance必须通过.Point.来访问了。

6.4 方法值和方法表达式

定义一个新的变量,这个变量是一个函数的值,可以看作函数的别名,使用的时候可以当做函数一样来使用。

方法值: 对一个已经声明的类型对象A,使用另外一个变量B来代替其方法。其接受器依然是A,通常表示为 B:=A.method

方法表达式: 当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:通常表示为 op:=T.f或者op:=(*T).f, 其中是(*T).f表示该方法的接收器是一个指针类型。

type Point struct {
	X, Y float64
}

// 下面的p称为方法的接受器
func (p *Point) Distance(q *Point) float64  {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

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 *Point, q* Point) Point
	// 判断要加还是减
	if add {
		// 方法表达式,会将第一个参数作为接收器
		op = (*Point).Add
	} else {
		op = (*Point).Sub
	}
	for i := range path {
		path[i] = op(&path[i], &offset)
	}
}

func main()  {
	p1 := Point{1,1}
	p2 := Point{4, 5}
	p1Distance := p1.Distance   // 使用方法值
	// p1Distance是p1.Distance方法返回的一个值
	fmt.Println(p1Distance(&p2))

	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}

	perim.TranslateBy(Point{1, 1}, true)
	fmt.Println(perim)
	perim.TranslateBy(Point{2, 2}, false)
	fmt.Println(perim)
}

6.5 封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。Go使用命名时首字母大小写来实现可见性。

封装的好处:

  1. 因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
  2. 隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
  3. 是阻止了外部调用方对对象内部的值任意地进行修改。

本文主要参考:《Go语言圣经》


撩我?
搜索我的公众号:Kyda
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章