第6章 方法
从90年代开始,面向对象编程(OOP)就成了称霸工程界和教育界的编程范式,所以之后几乎所有大规模被应用的语言都包含了对OOP的支持,Go语言也不例外
关于OOP,其实没有明确定义,但是大概的意思是,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型相关联的函数,一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情
在早些章节,其实我们已经使用过标准库提供的一些方法,比如time.Duration这个类型的Seconds方法
const day = 24*time.Hour
fmt.Println(day.Seconds())//86400
在2.5节中,我们定义了一个方法,Celsius类型的String方法
func (c Celsius) String() string{
return fmt.Sprintf("%g˚C",c)
}
在本章中,OOP编程的第一方面,我们会展示如何有效的定义和使用方法,我们会覆盖到OOP编程的两个关键点,封装和组合
6.3 通过嵌入结构体来扩展类型
type Point struct {
X,Y float64
}
type ColorPoint struct {
Point
Color color.RGBA
}
var cp ColorPoint
cp.X = 1
cp.Point.Y = 2
fmt.Println(cp) //{{1 2} {0 0 0 0}}
我们再来回顾一下结构体的嵌套以及嵌套结构体成员的访问,如上
那其实这种嵌套在方法中也同样适用,我们可以把ColorPoint类型当作接收器来调用Point里面的方法,即使ColorPoint里没有声明这些方法:
red := color.RGBA{255,0,0,255}
blue := color.RGBA{0,0,255,255}
var p = ColorPoint{Point{1,1},red} //定义变量
var q = ColorPoint{Point{5,4},blue}
fmt.Println(p.Distance(q.Point))//5
p.ScaleBy(2) //通过变量调用方法
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) //10
Point类的方法也被引入了ColorPoint。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型方法,再把它们组合起来
类比面向对象的语言,可以把Point看作一个基类,而ColorPoint看作其子类或者继承类,或者将ColorPoint 看作is a Point类型,但是这是错误的理解,只是类比罢了。注意我们对于Distance的调用,Distance有一个参数是Point类型,但q并不是一个Point类,所以尽管q有着Point这个内嵌类型,我们也必须要显式的选择它。尝试直接传q的话,会出现如下错误:
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
一个ColorPoin并不是一个Point,但它has a Point,并且它有从Point类里引入的Diatance和ScaleBy方法,如果从实现的角度考虑,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:
func (p ColorPoint) Distance(q Point)float64{
return p.Point.Distance(q)
}
func (p *ColorPoint) ScaleBy(factor float64){
p.Point.ScaleBy(factor)
}
当Point.Distance被第一个包装方法调用时,它的接收器的值是p.Point,而不是p,当然了,再Point类的方法里,你是访问不到ColorPoint的任何字段的
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这时,字段和方法会被间接的引入到当前的类型中,添加这一层间接关系让我们通过共享公用的结构动态的改变对象之间的关系,下面这个ColorPoint的声明内嵌了一个*Point的指针
type ColorPoint struct {
*Point
Color color.RGBA
var p = ColorPoint{&Point{1,1},red}
var q = ColorPoint{&Point{5,4},blue}
fmt.Println(p.Distance(*q.Point))//5
p.Point = q.Point
p.ScaleBy(2)
fmt.Println(*q.Point,*p.Point) //10
一个struct可能有多个匿名字段,我们将ColorPointd定一个为多个字段
type ColorPoint struct {
Point
color.RGBA
}
然后这种类型的值便会拥有Point和RGBA类型的所有方法,以及直接定义在ColorPoint中的方法。当编译器解析一个选择器到方法时,比如p.ScaleBy,它首先会去找直接定义在这个类型里的ScaleBy方法,然后找到被ColorPoint的内嵌字段们引入的方法,然后去找Point和RGBA的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话,编译器会报错,比如你在同一级里有两个同名的方法
方法只能在命名类型(像Point)或者指针类型的指针定义,但是多亏了内嵌,有些时候我们会给匿名struct类型来定义方法也有了手段,下面是一个小trick,这个例子展示了简单的cache,并适用两个包级别的变量来实现,一个mutex互斥量和它所操作的cache
var (
mu sync.Mutex
mapping = make(map[string]string)
)
func Lookup(key string)string{
mu.Lock()
v:= mapping[key]
mu.Unlock()
return v
}
下面这两个版本在功能上是一样的,但将两个包级别的变量放在了cache这个struct一组内:
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因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名的结构中,这让我们能够以一个简单明了的语法来对其进行加锁和解锁操作