2016-08-11 09:08:38 +00:00
## 12.3. Display, 一个递归的值打印器
2015-12-09 07:45:11 +00:00
2016-08-11 09:08:38 +00:00
接下来, 让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数, 我们只是构建一个用于调试用的Display函数: 给定任意一个复杂类型 x, 打印这个值对应的完整结构, 同时标记每个元素的发现路径。让我们从一个例子开始。
2015-12-31 14:23:38 +00:00
```Go
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
```
2016-02-15 03:06:34 +00:00
在上面的调用中, 传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:
2015-12-31 14:23:38 +00:00
```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"
```
2016-08-11 09:08:38 +00:00
你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作, 导出的是Display函数, 它只是display函数简单的包装以接受interface{}类型的参数:
2015-12-31 14:23:38 +00:00
2016-01-20 14:56:40 +00:00
< u > < i > gopl.io/ch12/display< / i > < / u >
2015-12-31 14:23:38 +00:00
```Go
func Display(name string, x interface{}) {
fmt.Printf("Display %s (%T):\n", name, x)
display(name, reflect.ValueOf(x))
}
```
2016-08-11 09:08:38 +00:00
在display函数中, 我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数, 但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中, path字符串, 从最开始传入的起始值( 这里是“e”) , 将逐步增长来表示是如何达到当前值( 例如“e.args[0].value”) 的。
2015-12-31 14:23:38 +00:00
2016-02-15 03:06:34 +00:00
因为我们不再模拟fmt.Sprint函数, 我们将直接使用fmt包来简化我们的例子实现。
2015-12-31 14:23:38 +00:00
```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))
}
}
```
2016-02-15 03:06:34 +00:00
让我们针对不同类型分别讨论。
2015-12-31 14:23:38 +00:00
2017-08-24 14:31:28 +00:00
**Slice和数组: ** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数, Index(i)获得索引i对应的元素, 返回的也是一个reflect.Value; 如果索引i超出范围的话将导致panic异常, 这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理, 我们通过在递归处理时向path附加“[i]”来表示访问路径。
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
虽然reflect.Value类型带有很多方法, 但是只有少数的方法能对任意值都安全调用。例如, Index方法只能对Slice、数组或字符串类型的值调用, 如果对其它类型调用则会导致panic异常。
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
**结构体:** NumField方法报告结构体中成员的数量, Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径, 我们必须获得结构体对应的reflect.Type类型信息, 然后访问结构体第i个成员的名字。
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
**Maps:** MapKeys方法返回一个reflect.Value类型的slice, 每一个元素对应map的一个key。和往常一样, 遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。( 我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型; 数组、结构体和接口都可以作为map的key。针对这种类型, 完善key的显示信息是练习12.1的任务。)
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
**指针:** Elem方法返回指针指向的变量, 依然是reflect.Value类型。即使指针是nil, 这个操作也是安全的, 在这种情况下指针是Invalid类型, 但是我们可以用IsNil方法来显式地测试一个空指针, 这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。
2015-12-31 14:23:38 +00:00
2016-02-15 03:06:34 +00:00
**接口:** 再一次, 我们使用IsNil方法来测试接口是否是nil, 如果不是, 我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
2015-12-31 14:23:38 +00:00
2016-02-15 03:06:34 +00:00
现在我们的Display函数总算完工了, 让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:
2015-12-31 14:23:38 +00:00
```Go
type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
```
2016-02-15 03:06:34 +00:00
让我们声明一个该类型的变量, 然后看看Display函数如何显示它:
2015-12-31 14:23:38 +00:00
```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.)",
},
}
```
2016-02-15 03:06:34 +00:00
Display("strangelove", strangelove)调用将显示( strangelove电影对应的中文名是《奇爱博士》) :
2015-12-31 14:23:38 +00:00
```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
```
2016-02-15 03:06:34 +00:00
我们也可以使用Display函数来显示标准库中类型的内部结构, 例如`*os.File`类型:
2015-12-31 14:23:38 +00:00
```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
```
2018-05-27 20:51:15 +00:00
可以看出, 反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的, 并且随着标准库的发展也可能导致结果不同。( 这也是将这些成员定义为私有成员的原因之一! ) 我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为`*os.File`的类型描述体)。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异:
2015-12-31 14:23:38 +00:00
```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"
...
```
2016-02-15 03:06:34 +00:00
观察下面两个例子的区别:
2015-12-31 14:23:38 +00:00
```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
```
2016-08-11 09:08:38 +00:00
在第一个例子中, Display函数调用reflect.ValueOf(i), 它返回一个Int类型的值。正如我们在12.2节中提到的, reflect.ValueOf总是返回一个具体类型的 Value, 因为它是从一个接口值提取的内容。
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
在第二个例子中, Display函数调用的是reflect.ValueOf(& i), 它返回一个指向i的指针, 对应Ptr类型。在switch的Ptr分支中, 对这个值调用 Elem 方法, 返回一个Value来表示变量 i 本身, 对应Interface类型。像这样一个间接获得的Value, 可能代表任意类型的值, 包括接口类型。display函数递归调用自身, 这次它分别打印了这个接口的动态类型和值。
2015-12-31 14:23:38 +00:00
2016-08-11 09:08:38 +00:00
对于目前的实现, 如果遇到对象图中含有回环, Display将会陷入死循环, 例如下面这个首尾相连的链表:
2015-12-31 14:23:38 +00:00
```Go
// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, & c}
Display("c", c)
```
2016-02-15 03:06:34 +00:00
Display会永远不停地进行深度递归打印:
2015-12-31 14:23:38 +00:00
```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...
```
2016-08-11 09:08:38 +00:00
许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧, 需要额外记录迄今访问的路径; 相应会带来成本。通用的解决方案是采用 unsafe 的语言特性, 我们将在13.3节看到具体的解决方案。
2015-12-31 14:23:38 +00:00
2017-08-24 14:31:28 +00:00
带环的数据结构很少会对fmt.Sprint函数造成问题, 因为它很少尝试打印完整的数据结构。例如, 当它遇到一个指针的时候, 它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住, 但是这种情况很罕见, 不值得付出为了处理回环所需的开销。
2015-12-31 14:23:38 +00:00
2017-08-24 14:31:28 +00:00
**练习 12.1: ** 扩展Display函数, 使它可以显示包含以结构体或数组作为map的key类型的值。
2015-12-31 14:23:38 +00:00
2017-08-24 14:31:28 +00:00
**练习 12.2: ** 增强display函数的稳健性, 通过记录边界的步数来确保在超出一定限制后放弃递归。( 在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)
2016-08-11 09:08:38 +00:00