gopl-zh.github.com/ch6/ch6-03.md

5.3 KiB
Raw Permalink Blame History

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

来看看ColoredPoint这个类型

gopl.io/ch6/coloredpoint

import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
	Point
	Color color.RGBA
}

我们完全可以将ColoredPoint定义为一个有三个字段的struct但是我们却将Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段。像我们在4.4节中看到的那样内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式并使其包含Point类型所具有的一切字段然后再定义一些自己的。如果我们想要的话我们可以直接认为通过嵌入的字段就是ColoredPoint自身的字段而完全不需要在调用时指出Point比如下面这样。

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

对于Point中的方法我们也有类似的用法我们可以把ColoredPoint类型当作接收器来调用Point里的方法即使ColoredPoint里没有声明这些方法

red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{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类的方法也被引入了ColoredPoint。用这种方式内嵌可以使我们定义字段特别多的复杂类型我们可以将字段先按小类型分组然后定义小类型的方法之后再把它们组合起来。

读者如果对基于类来实现面向对象的语言比较熟悉的话可能会倾向于将Point看作一个基类而ColoredPoint看作其子类或者继承类或者将ColoredPoint看作"is a" Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型但q并不是一个Point类所以尽管q有着Point这个内嵌类型我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误

p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point

一个ColoredPoint并不是一个Point但他"has a"Point并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法和下面的形式是等价的

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

func (p *ColoredPoint) ScaleBy(factor float64) {
	p.Point.ScaleBy(factor)
}

当Point.Distance被第一个包装方法调用时它的接收器值是p.Point而不是p当然了在Point类的方法里你是访问不到ColoredPoint的任何字段的。

在类型中内嵌的匿名字段也可能是一个命名类型的指针这种情况下字段和方法会被间接地引入到当前的类型中译注访问需要通过该指针指向的对象去取。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。

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}"

一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样

type ColoredPoint struct {
	Point
	color.RGBA
}

然后这种类型的值便会拥有Point和RGBA类型的所有方法以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时比如p.ScaleBy它会首先去找直接定义在这个类型里的ScaleBy方法然后找被ColoredPoint的内嵌字段们引入的方法然后去找Point和RGBA的内嵌字段引入的方法然后一直递归向下找。如果选择器有二义性的话编译器会报错比如你在同一级里有两个同名的方法。

方法只能在命名类型像Point或者指向类型的指针上定义但是多亏了内嵌有些时候我们给匿名struct类型来定义方法也有了手段。

下面是一个小trick。这个例子展示了简单的cache其使用两个包级别的变量来实现一个mutex互斥锁§9.2和它所操作的cache

var (
	mu sync.Mutex // guards mapping
	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方法也就都被引入到了这个匿名结构中了这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。