Go入门系列(十) go中的面向对象编程——方法-张柏沛IT博客

正文内容

Go入门系列(十) go中的面向对象编程——方法

栏目:Go语言 系列:Go入门系列 发布时间:2021-01-08 09:39 浏览量:300

方法

方法涉及到面向对象编程。在php中,对象是作为一种PHP的数据类型而存在,必须要用类所实例化出来的一个变量才是一个对象。但是在python或者js中则秉承着一切皆对象的原则,一个字符串或者数字也是一个对象,一个函数也是一个对象,因此一个数字或者字符串也能调用方法。

Go中的对象和python/js的方式很像:“Go中的一个对象其实是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型(type自定义的类型)关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作”。

 

Go中,方法是和类型相关联而不是和某一个对象相关联。我们可以为某一个类型定义一个方法,那么使用该类型声明的变量就都可以调用这个方法,但是不能专门为一个变量定义方法(所以go的面向对象编程的方法不区分静态方法和实例方法)。

 

我们可以通过定义一个以结构体类型为底层类型的新类型来定义一个类(在Go中没有类的概念,但我们可以把这个新类型当成是一个类),结构体中的成员当成是对象的成员属性,为这个新类型定义的方法看成是类方法。

 

例如:

type Point struct {X, Y float64}	// 定义一个Point类

// 为这个类(或者说Point类型)定义一个Distance方法用于计算点与点间的距离
func (point Point) Distance(anotherPoint Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

func main(){
	x := method.Point{ X:0, Y:0}
	y := method.Point{ X:3, Y:4}
	distance := x.Distance(y)
	fmt.Printf("%f", distance)
}

Distance方法中的point被称为接收器,它类似于oop编程中的self或者this,但是go中的接收器名词可以自定义,不一定非得定义为this或者self,一般会定义为类名的小写或者首字母。func (point Point) Distance(...) {...} 中的(point Point)是接收器参数,它也是一个参数,传入这个参数的时候也会进行值拷贝。

 

 

以指针作为接收器的方法

我们知道传入函数或者方法的参数会做一次值拷贝并赋给局部变量,现在我们假设Point是一个值很大的结构体,这个时候拿Point类型的变量的值作为接收器传入方法中的话就会对这个Point类型的变量进行只拷贝,这样无疑会消耗很多内存。

 

这时,我们就可以考虑使用Point类型的变量的指针作为接收器,例如上面的Distance方法就要改写成:

func (point *Point) Distance(anotherPoint *Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

我们使用point *Point 这个Point类型的指针作为接收器传入,并且Distance的参数anotherPoint也是一个指针。

 

这个时候我们可以通过以下方式调用Distance方法:

x := Point{ X:0, Y:0}

y := Point{ X:3, Y:4}

 

1.使用Point类型的变量指针调用:

distance := (&x).Distance(&y)         // 这里要用括号把&x包一下才行

 

这种方式是最正常的,因为我们定义Distance的时候也是指定用Point类型的指针作为接收器.

 

2.直接使用Point类型的变量调用:

distance := x.Distance(&y)

此时Go会将x隐式的转成用x的指针去调用Distance方法的。

 

但是下面这种直接用字面值方式的调用是错误的,原因是无法通过字面值找到其指针(指针是变量才有的,字面值没有指针):

distance := method.Point{ X:0, Y:0}.Distance(&y)

 

 

这里还要注意:Distance的参数 anotherPoint 必须只能传入指针,而不能传入实际值,Go是不会为非接收值的参数做指针转换的。

所以这样是错的:

distance := x.Distance(y)

 

现在我们反过来,形参接收器是实际值,实参使用指针去调用方法:

func (point Point) Distance(anotherPoint *Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

distance := (&x).Distance(&y)

 

GO中的原则是,一个新类型只要有一个方法使用指针作为接收器,那么这个新类型的其他所有方法也要用指针而不是变量值作为接收器。这个原则不是强制的,而是一种规范,我们尽可能的遵守这样的规范。

但是我们使用指针作为接收器的时候要考虑一点:由于传入的是指针,所以在方法中对接收器的操作会影响到方法之外的对应变量针的值(比如对x取两个指针x1x2,然后用x1调用某个方法从而修改了x的值,也会影响到x2)。因此如果不希望有这种情况发生,就还是用传值的接收器而非指针的接收器(但是大多数情况下。我们反而希望在方法对接收器的操作会影响到外面的对象的值同时也保存了对接收器的修改,因为在phppythonjs中对thisself的操作也都会被保存起来)。

 

总结起来考虑是否用指针作为接收器的因素就是两点:

  1. 对象本身是否很大
  2. 多个指针的情况下,某个指针调用方法时可能会影响对象值从而影响其他指针的值

我们需要权衡这两点来决定。

 

当我们用指针作为接收器的时候,我们甚至可以用一个nil来调用方法(不能用nil字面值调用哦),因为指针的零值就是nilnil可能是一个零值的指针。

 

nil也是一个合法的接收器类型

Go允许使用nil调用方法,前提是我们定义一个方法的时候,形参的接收器类型是一个指针或者是切片、哈希表这样的可引用类型。

比如:(下面这个例子的Point是模拟的链表的单向链接,和上面例子中的Point类没有半毛钱关系)

package method

import "fmt"

// 创建一个单向链表节点类
// 当该类型的对象指针是nil时,表示这是一个空链表 ****
type IntList struct {
	Value int
	Point *IntList
}

// 计算这个单项链表的元素值总和
func (intList *IntList) Sum() int {
	if intList == nil {
		return 0
	}

	return intList.Value + intList.Point.Sum()
}

// 往单项链表中添加一个元素,会影响原链表
func (intList *IntList) Add(i int) {
	// 获取链表最后一个节点的指针
	lastNode := intList
	for lastNode.Point != nil {
		lastNode = lastNode.Point
	}

	lastNode.Point = &IntList{
		Value: i,
		Point: nil,
	}
}

// 打印这个链表
func (IntList *IntList) IterPrint() {
	currentNode := IntList
	for currentNode != nil {
		fmt.Printf("%d\t", currentNode.Value)
		currentNode = currentNode.Point
	}
}


func main(){
	var int_list method.IntList
	int_list.Add(5)
	int_list.Add(4)
	int_list.Add(2)
	int_list.Add(8)
	int_list.Add(1)
	int_list.Add(10)

	int_list.IterPrint()	// 0   5   4   2   8   1   10
	fmt.Println(int_list.Sum())
}

这个是这个单向链表的正常示范。

 

接下来是使用nil指针调用其方法的示范。

func main(){
	int_list := new(IntList)
	fmt.Println(int_list)	// int_list不是一个nil指针,而是一个IntList类型的零值的指针 &{0 <nil>}
	int_list_next_node := int_list.Point	//  int_list_next_node才是一个nil指针
	fmt.Println(int_list_next_node)		// nil
	fmt.Println(int_list_next_node.Sum())	// 虽然int_list_next_node是nil,但是这个nil是IntList类型的nil,所以它也可以调用Sum方法
}

这个例子想告诉大家的是:值为nil的变量也可以调用类方法,前提是这个方法的接收器是指针类型或者切片/哈希表这样的引用类型。

 

 

 

通过嵌入其他类型来扩展

之前在讲结构体的时候有说过可以通过往一个新类型A(底层结构为结构体)嵌入其他类型B的方式(匿名成员)来使类型A拥有B类型的所有成员和方法。这种方式就类似于其他语言中类的继承。

 

例如:

type Point struct {X, Y float64}	// 定义一个Point类(表示一个点)
type ColorPoint struct {	// 定义一个有颜色的点类
	Point	// 嵌入Point这个类型,此时会隐式的为ColorPoint生成一个名字也叫Point的成员(这个成员就叫做匿名成员)
	Color color.RGBA	// 颜色成员,类型为color包的RGBA类型(底层其实是一个struct)
}

// 为Point类型定义一个Distance方法用于计算点与点间的距离
func (point *Point) Distance(anotherPoint *Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

func main(){
	r := color.RGBA{255, 0, 0, 255}
	g := color.RGBA{0, 255, 0, 255}
	p_r := method.ColorPoint{Point:method.Point{X:0, Y:0}, Color:r}
	p_g := method.ColorPoint{Point:method.Point{X:3, Y:4}, Color:g}
	fmt.Println(p_r.Distance(&p_g.Point))

	fmt.Println(p_r.X)		// 等价于 fmt.Println(p_r.Point.X)
}

这里要注意一点:

调用Distance的时候不能直接传入p_g,因为p_g是一个ColorPoint类型,而Distance方法要传入一个Point类型的指针类型才行。

 

 

下面我们再看看作者的说法:“读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将Point看作一个基类,而ColoredPoint看作其子类或者继承类,或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。一个ColoredPoint并不是一个Point,但他"has a"Point,并且它有从Point类里引入的Distance方法。

 

 

嵌入其他类型的时候,我们可以嵌入类型的指针类型:

嵌入其他类型的时候,我们可以嵌入类型的指针类型(这也是我们最常见的做法):

type ColoredPoint struct {

    *Point

    Color color.RGBA

}

 

不过这样的话,我们使用ColoredPoint的对象访问Point的方法和成员的时候就会通过指针访问而不是直接访问。而且*Point这个匿名成员让我们可以共享通用的结构并动态地改变对象之间的关系。

例如:

p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point                 // p和q共享1个Point成员
p.ScaleBy(2)          // 改变p的Point也会影响到q的Point
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

 

一个新类型可以嵌入多个其他类型(类似于其他语言的多继承)

比如我们这样定义:

type ColoredPoint struct {
    Point
    color.RGBA
}

这里就嵌入了两种新类型。ColoredPoint既可以使用Point的方法,也可以使用color.RGBA的方法。

但是嵌入多个类型的时候要求这些类型不能有同名的方法,否则会发生冲突。

 

匿名struct类型使用方法

我们知道要给一个类型定义方法的前提是:这个类型是用type声明出来的新类型。我们无法直接给go的原有类型直接定义方法。

 

比如,我们不用type声明Point这个类型,而直接给struct {X,Y int}创建一个方法:

func (point struct{X,Y int}) Distance() {

      // .....

}

 

会报错:Invalid receiver type 'struct {...}' ('struct {...}' is an unnamed type) 说这个类型是一个未命名的类型。

 

如何让一个不用type关键字声明的结构体类型使用方法呢?答案是通过嵌入其他结构体的方式。例如:

var cache = struct {
    sync.Mutex
    mapping map[string]string
}{
    mapping: make(map[string]string),
}


func Lookup(key string) string {
    cache.Lock()
    v := cache.mapping[key]
    cache.Unlock()
    return v
}

在这个例子中,我们初始化了一个cache的结构体类型的变量,在这个结构体类型中嵌入了一个Mutex互斥锁的类型。这么一来,cache就可以使用Mutex的方法,而且这个例子中也确实没有用type为这个struct类型声明新类型。这就是让一个匿名struct类型使用方法的技巧。当然了,实际上这个类使用的并不是自己定义的方法而是其成员的方法。因此匿名struct类型确实无法自定义方法。

 

方法值和方法表达式

方法值就是我们将一个已经指定了接收器的方法的引用赋给一个变量,例如:

func (point *Point) Distance(anotherPoint *Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

func main(){
	x := Point{ X:0, Y:0}
	y := Point{ X:3, Y:4}
	distance := (&x).Distance
	fmt.Println(distance(&y))
}

在这里我们直接将(&x).Distance这个函数引用赋值给distance变量,此时distance变量就是一个已经指定了&x为接收器的方法值,我们直接调用distance就会默认隐式的传入&x作为接收器去求xy之间的距离。

 

方法表达式是将一个未指定接收器的方法(即类型.方法)的引用赋给一个变量,这时这个变量方法的第一参会用来接收接收器,其他参数就是原方法的参数。例如:

func (point *Point) Distance(anotherPoint *Point) float64 {
	return math.Hypot(point.X-anotherPoint.X, point.Y-anotherPoint.Y)
}

func main(){
    x := Point{ X:0, Y:0}
    y := Point{ X:3, Y:4}
    distance := (*method.Point).Distance
    fmt.Println(distance(&x, &y))
}

这里的distance就是一个未指定接收器的方法,distance的第一参接收器,所以&x是接收器。其实distance(&x, &y) (&x).Distance(&y)等价,他们只是语法上有不同而已。

 

 

但是使用方法值和方法表达有时会给我们带来便利,看看作者给出的下面这个例子:

type Point struct {X, Y float64}	// 定义一个Point类(表示一个点)
type Points []*Point

// 向量相加
func (point *Point) Add(anotherPoint *Point) *Point {
	var res *Point
	res = &Point{X:point.X+anotherPoint.X, Y:point.Y+anotherPoint.Y}
	return res
}

// 向量相减
func (point *Point) Sub(anotherPoint *Point) *Point {
	var res *Point
	res = &Point{X:point.X-anotherPoint.X, Y:point.Y-anotherPoint.Y}
	return res
}

// 平移points中的多个点,平移的偏移量是offset,会影响原points得到平移后的点
// add为true表示做向量相加的平移,为false是向量相减的平移
func (points Points) TranslateBy(offset *Point, add bool) {
	var op func(point *Point, anotherPoint *Point) *Point	// 定义了一个没有函数体的闭包,所以这个op是一个零值函数,即nil

	// 为op赋值为方法表达式
	if add {
		op = (*Point).Add
	}else{
		op = (*Point).Sub
	}

	for i := range points{
		points[i] = op(points[i], offset)
	}
}


x1 := Point{ X:0, Y:0}
x2 := Point{ X:2, Y:3}
x3 := Point{ X:4, Y:4}
x4 := Point{ X:-2, Y:4}
x5 := Point{ X:-1, Y:-1}
offset_point := method.Point{ X:3, Y:4}
points := method.Points{&x1,&x2,&x3,&x4,&x5}
points.TranslateBy(&offset_point, true)
fmt.Printf("%v", points)

最后我们可以通过为一个类声明一个小写成员来作为私有成员。私有成员是非导出的。

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Go入门系列(十) go中的面向对象编程——方法

热门推荐
推荐新闻