diff --git a/ch12/ch12-03.md b/ch12/ch12-03.md index cd19a4d..464fb39 100644 --- a/ch12/ch12-03.md +++ b/ch12/ch12-03.md @@ -1,3 +1,228 @@ ## 12.3. Display遞歸打印 -TODO +接下來,讓我們看看如何改善聚合數據類型的顯示。我們併不想完全剋隆一個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節,我們會看到另一種探測數據結構是否存在環的技術。) + +