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

229 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{}類型的參數:
```Go
gopl.io/ch12/display
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節,我們會看到另一種探測數據結構是否存在環的技術。)