Go-面向对象
介绍了函数和 struct,那你是否想过函数当作 struct 的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为 method
method
现在假设有这么一个场景,你定义了一个 struct 叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}这段代码可以计算出来长方形的面积,但是 area () 不是作为 Rectangle 的方法实现的(类似面向对象里面的方法),而是将 Rectangle 的对象(如 r1, r2)作为参数传入函数计算面积的。
这样实现当然没有问题咯,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,你想计算他们的面积的时候怎么办啊?那就只能增加新的函数咯,但是函数名你就必须要跟着换了,变成 area_rectangle, area_circle, area_triangle...
像下图所表示的那样, 椭圆代表函数,而这些函数并不从属于 struct (或者以面向对象的术语来说,并不属于 class),他们是单独存在于 struct 外围,而非在概念上属于某个 struct 的。
图 2.8 方法和 struct 的关系图
很显然,这样的实现并不优雅,并且从概念上来说 "面积" 是 "形状" 的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。
基于上面的原因所以就有了 method 的概念,method 是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在 func 后面增加了一个 receiver (也就是 method 所依从的主体)。
用上面提到的形状的例子来说,method area() 是依赖于某个形状 (比如说 Rectangle) 来发生作用的。Rectangle.area () 的发出者是 Rectangle, area () 是属于 Rectangle 的方法,而非一个外围函数。
更具体地说,Rectangle 存在字段 height 和 width, 同时存在方法 area (), 这些字段和方法都属于 Rectangle。
用 Rob Pike 的话来说就是:
"A method is a function with an implicit first argument, called a receiver."
method 的语法如下:
下面我们用最开始的例子用 method 来实现:
在使用 method 的时候重要注意几点
虽然 method 的名字一模一样,但是如果接收者不一样,那么 method 就不一样
method 里面可以访问接收者的字段
调用 method 通过
.访问,就像 struct 里面访问字段一样
图示如下:
图 2.9 不同 struct 的 method 不同
在上例,method area () 分别属于 Rectangle 和 Circle, 于是他们的 Receiver 就变成了 Rectangle 和 Circle, 或者说,这个 area () 方法 是由 Rectangle/Circle 发出的。
值得说明的一点是,图示中 method 用虚线标出,意思是此处方法的 Receiver 是以值传递,而非引用传递,是的,Receiver 还可以是指针,两者的差别在于,指针作为 Receiver 会对实例对象的内容发生操作,而普通类型作为 Receiver 仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。
那是不是 method 只能作用在 struct 上面呢?当然不是咯,他可以定义在任何你自定义的类型、内置类型、struct 等各种类型上面。这里你是不是有点迷糊了,什么叫自定义类型,自定义类型不就是 struct 嘛,不是这样的哦,struct 只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。
请看下面这个申明自定义类型的代码
看到了吗?简单的很吧,这样你就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于 c 中的 typedef,例如上面 ages 替代了 int
好了,让我们回到 method
你可以在任何的自定义类型中定义任意多的 method,接下来让我们看一个复杂一点的例子
上面的代码通过 const 定义了一些常量,然后定义了一些自定义类型
Color 作为 byte 的别名
定义了一个 struct:Box,含有三个长宽高字段和一个颜色属性
定义了一个 slice:BoxList,含有 Box
然后以上面的自定义类型为接收者定义了一些 method
Volume () 定义了接收者为 Box,返回 Box 的容量
SetColor (c Color),把 Box 的颜色改为 c
BiggestColor () 定在 BoxList 上面,返回 list 里面容量最大的颜色
PaintItBlack () 把 BoxList 里面所有 Box 的颜色全部变成黑色
String () 定义在 Color 上面,返回 Color 的具体颜色 (字符串格式)
上面的代码通过文字描述出来之后是不是很简单?我们一般解决问题都是通过问题的描述,去写相应的代码实现。
指针作为 receiver
现在让我们回过头来看看 SetColor 这个 method,它的 receiver 是一个指向 Box 的指针,是的,你可以使用 *Box。想想为啥要使用指针而不是 Box 本身呢?
我们定义 SetColor 的真正目的是想改变这个 Box 的颜色,如果不传 Box 的指针,那么 SetColor 接受的其实是 Box 的一个 copy,也就是说 method 内对于颜色值的修改,其实只作用于 Box 的 copy,而不是真正的 Box。所以我们需要传入指针。
这里可以把 receiver 当作 method 的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解
这里你也许会问了那 SetColor 函数里面应该这样定义 *b.Color=c, 而不是 b.Color=c, 因为我们需要读取到指针相应的值。
你是对的,其实 Go 里面这两种方式都是正确的,当你用指针去访问相应的字段时 (虽然指针没有任何的字段),Go 知道你要通过指针去获取这个值,看到了吧,Go 的设计是不是越来越吸引你了。
也许细心的读者会问这样的问题,PaintItBlack 里面调用 SetColor 的时候是不是应该写成 (&bl[i]).SetColor(BLACK),因为 SetColor 的 receiver 是 * Box,而不是 Box。
你又说对的,这两种方式都可以,因为 Go 知道 receiver 是指针,他自动帮你转了。
也就是说:
如果一个 method 的 receiver 是 *T, 你可以在一个 T 类型的实例变量 V 上面调用这个 method,而不需要 &V 去调用这个 method
类似的
如果一个 method 的 receiver 是 T,你可以在一个 T 类型的变量 P 上面调用这个 method,而不需要 P 去调用这个 method
所以,你不用担心你是调用的指针的 method 还是不是指针的 method,Go 知道你要做的一切,这对于有多年 C/C++ 编程经验的同学来说,真是解决了一个很大的痛苦。
method 继承
前面一章我们学习了字段的继承,那么你也会发现 Go 的一个神奇之处,method 也是可以继承的。如果匿名字段实现了一个 method,那么包含这个匿名字段的 struct 也能调用该 method。让我们来看下面这个例子
method 重写
上面的例子中,如果 Employee 想要实现自己的 SayHi, 怎么办?简单,和匿名字段冲突一样的道理,我们可以在 Employee 上面定义一个 method,重写了匿名字段的方法。请看下面的例子
上面的代码设计的是如此的美妙,让人不自觉的为 Go 的设计惊叹!
通过这些内容,我们可以设计出基本的面向对象的程序了,但是 Go 里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现 (大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。
Last updated

