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

226 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 12.3. Display递归打印
接下来让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数我们只是像构建一个用于调式用的Display函数给定一个聚合类型x打印这个值对应的完整的结构同时记录每个发现的每个元素的路径。让我们从一个例子开始。
```Go
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
```
在上面的调用中传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下
```Go
Display e (eval.call):
e.fn = "sqrt"
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = "A"
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"
```
在可能的情况下你应该避免在一个包中暴露和反射相关的接口。我们将定义一个未导出的display函数用于递归处理工作导出的是Display函数它只是display函数简单的包装以接受interface{}类型的参数:
<u><i>gopl.io/ch12/display</i></u>
```Go
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
```
在display函数中我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数但是我们会使用reflect.Value的方法来递归显示聚合类型的每一个成员或元素。在递归下降过程中path字符串从最开始传入的起始值这里是“e”将逐步增长以表示如何达到当前值例如“e.args[0].value”
因为我们不再模拟fmt.Sprint函数我们将直接使用fmt包来简化我们的例子实现。
```Go
func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf("%s = invalid\n", path)
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
case reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf("%s[%s]", path,
formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
display(fmt.Sprintf("(*%s)", path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf("%s = nil\n", path)
} else {
fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
display(path+".value", v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf("%s = %s\n", path, formatAtom(v))
}
}
```
让我们针对不同类型分别讨论。
**Slice和数组** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数Index(i)活动索引i对应的元素返回的也是一个reflect.Value类型的值如果索引i超出范围的话将导致panic异常这些行为和数组或slice类型内建的len(a)和a[i]等操作类似。display针对序列中的每个元素递归调用自身处理我们通过在递归处理时向path附加“[i]”来表示访问路径。
虽然reflect.Value类型带有很多方法但是只有少数的方法对任意值都是可以安全调用的。例如Index方法只能对Slice、数组或字符串类型的值调用其它类型如果调用将导致panic异常。
**结构体:** NumField方法报告结构体中成员的数量Field(i)以reflect.Value类型返回第i个成员的值。成员列表包含了匿名成员在内的全部成员。通过在path添加“.f”来表示成员路径我们必须获得结构体对应的reflect.Type类型信息包含结构体类型和第i个成员的名字。
**Maps:** MapKeys方法返回一个reflect.Value类型的slice每一个都对应map的可以。和往常一样遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型数组、结构体和接口都可以作为map的key。针对这种类型完善key的显示信息是练习12.1的任务。)
**指针:** Elem方法返回指针指向的变量还是reflect.Value类型。技术指针是nil这个操作也是安全的在这种情况下指针是Invalid无效类型但是我们可以用IsNil方法来显式地测试一个空指针这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。
**接口:** 再一次我们使用IsNil方法来测试接口是否是nil如果不是我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
现在我们的Display函数总算完工了让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:
```Go
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
```
让我们声明一个该类型的变量然后看看Display函数如何显示它
```Go
strangelove := Movie{
Title: "Dr. Strangelove",
Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
Year: 1964,
Color: false,
Actor: map[string]string{
"Dr. Strangelove": "Peter Sellers",
"Grp. Capt. Lionel Mandrake": "Peter Sellers",
"Pres. Merkin Muffley": "Peter Sellers",
"Gen. Buck Turgidson": "George C. Scott",
"Brig. Gen. Jack D. Ripper": "Sterling Hayden",
`Maj. T.J. "King" Kong`: "Slim Pickens",
},
Oscars: []string{
"Best Actor (Nomin.)",
"Best Adapted Screenplay (Nomin.)",
"Best Director (Nomin.)",
"Best Picture (Nomin.)",
},
}
```
Display("strangelove", strangelove)调用将显示strangelove电影对应的中文名是《奇爱博士》
```Go
Display strangelove (display.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
strangelove.Oscars[0] = "Best Actor (Nomin.)"
strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
strangelove.Oscars[2] = "Best Director (Nomin.)"
strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil
```
我们也可以使用Display函数来显示标准库中类型的内部结构例如`*os.File`类型:
```Go
Display("os.Stderr", os.Stderr)
// Output:
// Display os.Stderr (*os.File):
// (*(*os.Stderr).file).fd = 2
// (*(*os.Stderr).file).name = "/dev/stderr"
// (*(*os.Stderr).file).nepipe = 0
```
要注意的是结构体中未导出的成员对反射也是可见的。需要当心的是这个例子的输出在不同操作系统上可能是不同的并且随着标准库的发展也可能导致结果不同。这也是将这些成员定义为私有成员的原因之一我们深圳可以用Display函数来显示reflect.Value来查看`*os.File`类型的内部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异:
```Go
Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = "*os.File"
(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir"
(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error"
(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error"
...
```
观察下面两个例子的区别:
```Go
var i interface{} = 3
Display("i", i)
// Output:
// Display i (int):
// i = 3
Display("&i", &i)
// Output:
// Display &i (*interface {}):
// (*&i).type = int
// (*&i).value = 3
```
在第一个例子中Display函数将调用reflect.ValueOf(i)它返回一个Int类型的值。正如我们在12.2节中提到的reflect.ValueOf总是返回一个值的具体类型因为它是从一个接口值提取的内容。
在第二个例子中Display函数调用的是reflect.ValueOf(&i)它返回一个指向i的指针对应Ptr类型。在switch的Ptr分支中通过调用Elem来返回这个值返回一个Value来表示i对应Interface类型。一个间接获得的Value就像这一个可能代表任意类型的值包括接口类型。内部的display函数递归调用自身这次它将打印接口的动态类型和值。
目前的实现Display如果显示一个带环的数据结构将会陷入死循环例如首位项链的链表
```Go
// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)
```
Display会永远不停地进行深度递归打印
```Go
Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
...ad infinitum...
```
许多Go语言程序都包含了一些循环的数据结果。Display支持这类带环的数据结构是比较棘手的需要增加一个额外的记录访问的路径代价是昂贵的。一般的解决方案是采用不安全的语言特性我们将在13.3节看到具体的解决方案。
带环的数据结构很少会对fmt.Sprint函数造成问题因为它很少尝试打印完整的数据结构。例如当它遇到一个指针的时候它只是简单第打印指针的数值。虽然在打印包含自身的slice或map时可能遇到困难但是不保证处理这种是罕见情况却可以避免额外的麻烦。
**练习 12.1** 扩展Displayhans以便它可以显示包含以结构体或数组作为map的key类型的值。
**练习 12.2** 增强display函数的稳健性通过记录边界的步数来确保在超出一定限制前放弃递归。在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)