mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-09-04 03:12:31 +00:00
转为 mdbook
This commit is contained in:
227
ch4/ch4-04.md
227
ch4/ch4-04.md
@@ -134,8 +134,229 @@ if _, ok := seen[s]; !ok {
|
||||
}
|
||||
```
|
||||
|
||||
{% include "./ch4-04-1.md" %}
|
||||
### 4.4.1. 结构体字面值
|
||||
|
||||
{% include "./ch4-04-2.md" %}
|
||||
结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。
|
||||
|
||||
{% include "./ch4-04-3.md" %}
|
||||
```Go
|
||||
type Point struct{ X, Y int }
|
||||
|
||||
p := Point{1, 2}
|
||||
```
|
||||
|
||||
这里有两种形式的结构体字面值语法,上面的是第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序,不过结构体成员有细微的调整就可能导致上述代码不能编译。因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。
|
||||
|
||||
其实更常用的是第二种写法,以成员名字和相应的值来初始化,可以包含部分或全部的成员,如1.4节的Lissajous程序的写法:
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
```
|
||||
|
||||
在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。
|
||||
|
||||
两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。
|
||||
|
||||
```Go
|
||||
package p
|
||||
type T struct{ a, b int } // a and b are not exported
|
||||
|
||||
package q
|
||||
import "p"
|
||||
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
|
||||
var _ = p.T{1, 2} // compile error: can't reference a, b
|
||||
```
|
||||
|
||||
虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。
|
||||
|
||||
结构体可以作为函数的参数和返回值。例如,这个Scale函数将Point类型的值缩放后返回:
|
||||
|
||||
```Go
|
||||
func Scale(p Point, factor int) Point {
|
||||
return Point{p.X * factor, p.Y * factor}
|
||||
}
|
||||
|
||||
fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
|
||||
```
|
||||
|
||||
如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,
|
||||
|
||||
```Go
|
||||
func Bonus(e *Employee, percent int) int {
|
||||
return e.Salary * percent / 100
|
||||
}
|
||||
```
|
||||
|
||||
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
|
||||
|
||||
```Go
|
||||
func AwardAnnualRaise(e *Employee) {
|
||||
e.Salary = e.Salary * 105 / 100
|
||||
}
|
||||
```
|
||||
|
||||
因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:
|
||||
|
||||
```Go
|
||||
pp := &Point{1, 2}
|
||||
```
|
||||
|
||||
它和下面的语句是等价的
|
||||
|
||||
```Go
|
||||
pp := new(Point)
|
||||
*pp = Point{1, 2}
|
||||
```
|
||||
|
||||
不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。
|
||||
|
||||
### 4.4.2. 结构体比较
|
||||
|
||||
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:
|
||||
|
||||
```Go
|
||||
type Point struct{ X, Y int }
|
||||
|
||||
p := Point{1, 2}
|
||||
q := Point{2, 1}
|
||||
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。
|
||||
|
||||
```Go
|
||||
type address struct {
|
||||
hostname string
|
||||
port int
|
||||
}
|
||||
|
||||
hits := make(map[address]int)
|
||||
hits[address{"golang.org", 443}]++
|
||||
```
|
||||
|
||||
|
||||
### 4.4.3. 结构体嵌入和匿名成员
|
||||
|
||||
在本节中,我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。
|
||||
|
||||
考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义:
|
||||
|
||||
```Go
|
||||
type Circle struct {
|
||||
X, Y, Radius int
|
||||
}
|
||||
|
||||
type Wheel struct {
|
||||
X, Y, Radius, Spokes int
|
||||
}
|
||||
```
|
||||
|
||||
一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息,和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外,还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
w.X = 8
|
||||
w.Y = 8
|
||||
w.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来:
|
||||
|
||||
```Go
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
type Circle struct {
|
||||
Center Point
|
||||
Radius int
|
||||
}
|
||||
|
||||
type Wheel struct {
|
||||
Circle Circle
|
||||
Spokes int
|
||||
}
|
||||
```
|
||||
|
||||
这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
w.Circle.Center.X = 8
|
||||
w.Circle.Center.Y = 8
|
||||
w.Circle.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。
|
||||
|
||||
```Go
|
||||
type Circle struct {
|
||||
Point
|
||||
Radius int
|
||||
}
|
||||
|
||||
type Wheel struct {
|
||||
Circle
|
||||
Spokes int
|
||||
}
|
||||
```
|
||||
|
||||
得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
|
||||
|
||||
```Go
|
||||
var w Wheel
|
||||
w.X = 8 // equivalent to w.Circle.Point.X = 8
|
||||
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
|
||||
w.Radius = 5 // equivalent to w.Circle.Radius = 5
|
||||
w.Spokes = 20
|
||||
```
|
||||
|
||||
在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。
|
||||
|
||||
不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
|
||||
|
||||
```Go
|
||||
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
|
||||
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
|
||||
```
|
||||
|
||||
结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:
|
||||
|
||||
<u><i>gopl.io/ch4/embed</i></u>
|
||||
```Go
|
||||
w = Wheel{Circle{Point{8, 8}, 5}, 20}
|
||||
|
||||
w = Wheel{
|
||||
Circle: Circle{
|
||||
Point: Point{X: 8, Y: 8},
|
||||
Radius: 5,
|
||||
},
|
||||
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", w)
|
||||
// Output:
|
||||
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
|
||||
|
||||
w.X = 42
|
||||
|
||||
fmt.Printf("%#v\n", w)
|
||||
// Output:
|
||||
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
|
||||
```
|
||||
|
||||
需要注意的是Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。对于结构体类型来说,将包含每个成员的名字。
|
||||
|
||||
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。在上面的例子中,Point和Circle匿名成员都是导出的。即使它们不导出(比如改成小写字母开头的point和circle),我们依然可以用简短形式访问匿名成员嵌套的成员
|
||||
|
||||
```Go
|
||||
w.X = 8 // equivalent to w.circle.point.X = 8
|
||||
```
|
||||
|
||||
但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。
|
||||
|
||||
到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?
|
||||
|
||||
答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心,我们将在6.3节中专门讨论。
|
||||
|
Reference in New Issue
Block a user