Go 語言 Display遞歸打印

2023-03-14 17:00 更新

原文鏈接:https://gopl-zh.github.io/ch12/ch12-03.html


12.3. Display,一個(gè)遞歸的值打印器

接下來,讓我們看看如何改善聚合數(shù)據(jù)類型的顯示。我們并不想完全克隆一個(gè)fmt.Sprint函數(shù),我們只是構(gòu)建一個(gè)用于調(diào)試用的Display函數(shù):給定任意一個(gè)復(fù)雜類型 x,打印這個(gè)值對(duì)應(yīng)的完整結(jié)構(gòu),同時(shí)標(biāo)記每個(gè)元素的發(fā)現(xiàn)路徑。讓我們從一個(gè)例子開始。

e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)

在上面的調(diào)用中,傳入Display函數(shù)的參數(shù)是在7.9節(jié)一個(gè)表達(dá)式求值函數(shù)返回的語法樹。Display函數(shù)的輸出如下:

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"

你應(yīng)該盡量避免在一個(gè)包的API中暴露涉及反射的接口。我們將定義一個(gè)未導(dǎo)出的display函數(shù)用于遞歸處理工作,導(dǎo)出的是Display函數(shù),它只是display函數(shù)簡單的包裝以接受interface{}類型的參數(shù):

gopl.io/ch12/display

func Display(name string, x interface{}) {
    fmt.Printf("Display %s (%T):\n", name, x)
    display(name, reflect.ValueOf(x))
}

在display函數(shù)中,我們使用了前面定義的打印基礎(chǔ)類型——基本類型、函數(shù)和chan等——元素值的formatAtom函數(shù),但是我們會(huì)使用reflect.Value的方法來遞歸顯示復(fù)雜類型的每一個(gè)成員。在遞歸下降過程中,path字符串,從最開始傳入的起始值(這里是“e”),將逐步增長來表示是如何達(dá)到當(dāng)前值(例如“e.args[0].value”)的。

因?yàn)槲覀儾辉倌Mfmt.Sprint函數(shù),我們將直接使用fmt包來簡化我們的例子實(shí)現(xiàn)。

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))
    }
}

讓我們針對(duì)不同類型分別討論。

Slice和數(shù)組: 兩種的處理邏輯是一樣的。Len方法返回slice或數(shù)組值中的元素個(gè)數(shù),Index(i)獲得索引i對(duì)應(yīng)的元素,返回的也是一個(gè)reflect.Value;如果索引i超出范圍的話將導(dǎo)致panic異常,這與數(shù)組或slice類型內(nèi)建的len(a)和a[i]操作類似。display針對(duì)序列中的每個(gè)元素遞歸調(diào)用自身處理,我們通過在遞歸處理時(shí)向path附加“[i]”來表示訪問路徑。

雖然reflect.Value類型帶有很多方法,但是只有少數(shù)的方法能對(duì)任意值都安全調(diào)用。例如,Index方法只能對(duì)Slice、數(shù)組或字符串類型的值調(diào)用,如果對(duì)其它類型調(diào)用則會(huì)導(dǎo)致panic異常。

結(jié)構(gòu)體: NumField方法報(bào)告結(jié)構(gòu)體中成員的數(shù)量,F(xiàn)ield(i)以reflect.Value類型返回第i個(gè)成員的值。成員列表也包括通過匿名字段提升上來的成員。為了在path添加“.f”來表示成員路徑,我們必須獲得結(jié)構(gòu)體對(duì)應(yīng)的reflect.Type類型信息,然后訪問結(jié)構(gòu)體第i個(gè)成員的名字。

Maps: MapKeys方法返回一個(gè)reflect.Value類型的slice,每一個(gè)元素對(duì)應(yīng)map的一個(gè)key。和往常一樣,遍歷map時(shí)順序是隨機(jī)的。MapIndex(key)返回map中key對(duì)應(yīng)的value。我們向path添加“[key]”來表示訪問路徑。(我們這里有一個(gè)未完成的工作。其實(shí)map的key的類型并不局限于formatAtom能完美處理的類型;數(shù)組、結(jié)構(gòu)體和接口都可以作為map的key。針對(duì)這種類型,完善key的顯示信息是練習(xí)12.1的任務(wù)。)

指針: Elem方法返回指針指向的變量,依然是reflect.Value類型。即使指針是nil,這個(gè)操作也是安全的,在這種情況下指針是Invalid類型,但是我們可以用IsNil方法來顯式地測試一個(gè)空指針,這樣我們可以打印更合適的信息。我們?cè)趐ath前面添加“*”,并用括弧包含以避免歧義。

接口: 再一次,我們使用IsNil方法來測試接口是否是nil,如果不是,我們可以調(diào)用v.Elem()來獲取接口對(duì)應(yīng)的動(dòng)態(tài)值,并且打印對(duì)應(yīng)的類型和值。

現(xiàn)在我們的Display函數(shù)總算完工了,讓我們看看它的表現(xiàn)吧。下面的Movie類型是在4.5節(jié)的電影類型上演變來的:

type Movie struct {
    Title, Subtitle string
    Year            int
    Color           bool
    Actor           map[string]string
    Oscars          []string
    Sequel          *string
}

讓我們聲明一個(gè)該類型的變量,然后看看Display函數(shù)如何顯示它:

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)調(diào)用將顯示(strangelove電影對(duì)應(yīng)的中文名是《奇愛博士》):

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函數(shù)來顯示標(biāo)準(zhǔn)庫中類型的內(nèi)部結(jié)構(gòu),例如*os.File類型:

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

可以看出,反射能夠訪問到結(jié)構(gòu)體中未導(dǎo)出的成員。需要當(dāng)心的是這個(gè)例子的輸出在不同操作系統(tǒng)上可能是不同的,并且隨著標(biāo)準(zhǔn)庫的發(fā)展也可能導(dǎo)致結(jié)果不同。(這也是將這些成員定義為私有成員的原因之一?。┪覀兩踔量梢杂肈isplay函數(shù)來顯示reflect.Value 的內(nèi)部構(gòu)造(在這里設(shè)置為*os.File的類型描述體)。Display("rV", reflect.ValueOf(os.Stderr))調(diào)用的輸出如下,當(dāng)然不同環(huán)境得到的結(jié)果可能有差異:

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"
...

觀察下面兩個(gè)例子的區(qū)別:

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

在第一個(gè)例子中,Display函數(shù)調(diào)用reflect.ValueOf(i),它返回一個(gè)Int類型的值。正如我們?cè)?2.2節(jié)中提到的,reflect.ValueOf總是返回一個(gè)具體類型的 Value,因?yàn)樗菑囊粋€(gè)接口值提取的內(nèi)容。

在第二個(gè)例子中,Display函數(shù)調(diào)用的是reflect.ValueOf(&i),它返回一個(gè)指向i的指針,對(duì)應(yīng)Ptr類型。在switch的Ptr分支中,對(duì)這個(gè)值調(diào)用 Elem 方法,返回一個(gè)Value來表示變量 i 本身,對(duì)應(yīng)Interface類型。像這樣一個(gè)間接獲得的Value,可能代表任意類型的值,包括接口類型。display函數(shù)遞歸調(diào)用自身,這次它分別打印了這個(gè)接口的動(dòng)態(tài)類型和值。

對(duì)于目前的實(shí)現(xiàn),如果遇到對(duì)象圖中含有回環(huán),Display將會(huì)陷入死循環(huán),例如下面這個(gè)首尾相連的鏈表:

// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &c}
Display("c", c)

Display會(huì)永遠(yuǎn)不停地進(jìn)行深度遞歸打?。?

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語言程序都包含了一些循環(huán)的數(shù)據(jù)。讓Display支持這類帶環(huán)的數(shù)據(jù)結(jié)構(gòu)需要些技巧,需要額外記錄迄今訪問的路徑;相應(yīng)會(huì)帶來成本。通用的解決方案是采用 unsafe 的語言特性,我們將在13.3節(jié)看到具體的解決方案。

帶環(huán)的數(shù)據(jù)結(jié)構(gòu)很少會(huì)對(duì)fmt.Sprint函數(shù)造成問題,因?yàn)樗苌賴L試打印完整的數(shù)據(jù)結(jié)構(gòu)。例如,當(dāng)它遇到一個(gè)指針的時(shí)候,它只是簡單地打印指針的數(shù)字值。在打印包含自身的slice或map時(shí)可能卡住,但是這種情況很罕見,不值得付出為了處理回環(huán)所需的開銷。

練習(xí) 12.1: 擴(kuò)展Display函數(shù),使它可以顯示包含以結(jié)構(gòu)體或數(shù)組作為map的key類型的值。

練習(xí) 12.2: 增強(qiáng)display函數(shù)的穩(wěn)健性,通過記錄邊界的步數(shù)來確保在超出一定限制后放棄遞歸。(在13.3節(jié),我們會(huì)看到另一種探測數(shù)據(jù)結(jié)構(gòu)是否存在環(huán)的技術(shù)。)



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)