mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2026-01-16 12:17:13 +08:00
回到简体
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
## 12.3. Display遞歸打印
|
||||
## 12.3. Display递归打印
|
||||
|
||||
接下來,讓我們看看如何改善聚合數據類型的顯示。我們併不想完全剋隆一個fmt.Sprint函數,我們隻是像構建一個用於調式用的Display函數,給定一個聚合類型x,打印這個值對應的完整的結構,同時記録每個發現的每個元素的路徑。讓我們從一個例子開始。
|
||||
接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是像构建一个用于调式用的Display函数,给定一个聚合类型x,打印这个值对应的完整的结构,同时记录每个发现的每个元素的路径。让我们从一个例子开始。
|
||||
|
||||
```Go
|
||||
e, _ := eval.Parse("sqrt(A / pi)")
|
||||
Display("e", e)
|
||||
```
|
||||
|
||||
在上面的調用中,傳入Display函數的參數是在7.9節一個表達式求值函數返迴的語法樹。Display函數的輸出如下:
|
||||
在上面的调用中,传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:
|
||||
|
||||
```Go
|
||||
Display e (eval.call):
|
||||
@@ -20,7 +20,7 @@ e.args[0].value.y.type = eval.Var
|
||||
e.args[0].value.y.value = "pi"
|
||||
```
|
||||
|
||||
在可能的情況下,你應該避免在一個包中暴露和反射相關的接口。我們將定義一個未導出的display函數用於遞歸處理工作,導出的是Display函數,它隻是display函數簡單的包裝以接受interface{}類型的參數:
|
||||
在可能的情况下,你应该避免在一个包中暴露和反射相关的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:
|
||||
|
||||
<u><i>gopl.io/ch12/display</i></u>
|
||||
```Go
|
||||
@@ -30,9 +30,9 @@ func Display(name string, x interface{}) {
|
||||
}
|
||||
```
|
||||
|
||||
在display函數中,我們使用了前面定義的打印基礎類型——基本類型、函數和chan等——元素值的formatAtom函數,但是我們會使用reflect.Value的方法來遞歸顯示聚合類型的每一個成員或元素。在遞歸下降過程中,path字符串,從最開始傳入的起始值(這里是“e”),將逐步增長以表示如何達到當前值(例如“e.args[0].value”)。
|
||||
在display函数中,我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数,但是我们会使用reflect.Value的方法来递归显示聚合类型的每一个成员或元素。在递归下降过程中,path字符串,从最开始传入的起始值(这里是“e”),将逐步增长以表示如何达到当前值(例如“e.args[0].value”)。
|
||||
|
||||
因爲我們不再模擬fmt.Sprint函數,我們將直接使用fmt包來簡化我們的例子實現。
|
||||
因为我们不再模拟fmt.Sprint函数,我们将直接使用fmt包来简化我们的例子实现。
|
||||
|
||||
```Go
|
||||
func display(path string, v reflect.Value) {
|
||||
@@ -72,21 +72,21 @@ func display(path string, v reflect.Value) {
|
||||
}
|
||||
```
|
||||
|
||||
讓我們針對不同類型分别討論。
|
||||
让我们针对不同类型分别讨论。
|
||||
|
||||
**Slice和數組:** 兩種的處理邏輯是一樣的。Len方法返迴slice或數組值中的元素個數,Index(i)活動索引i對應的元素,返迴的也是一個reflect.Value類型的值;如果索引i超出范圍的話將導致panic異常,這些行爲和數組或slice類型內建的len(a)和a[i]等操作類似。display針對序列中的每個元素遞歸調用自身處理,我們通過在遞歸處理時向path附加“[i]”來表示訪問路徑。
|
||||
**Slice和数组:** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数,Index(i)活动索引i对应的元素,返回的也是一个reflect.Value类型的值;如果索引i超出范围的话将导致panic异常,这些行为和数组或slice类型内建的len(a)和a[i]等操作类似。display针对序列中的每个元素递归调用自身处理,我们通过在递归处理时向path附加“[i]”来表示访问路径。
|
||||
|
||||
雖然reflect.Value類型帶有很多方法,但是隻有少數的方法對任意值都是可以安全調用的。例如,Index方法隻能對Slice、數組或字符串類型的值調用,其它類型如果調用將導致panic異常。
|
||||
虽然reflect.Value类型带有很多方法,但是只有少数的方法对任意值都是可以安全调用的。例如,Index方法只能对Slice、数组或字符串类型的值调用,其它类型如果调用将导致panic异常。
|
||||
|
||||
**結構體:** NumField方法報告結構體中成員的數量,Field(i)以reflect.Value類型返迴第i個成員的值。成員列表包含了匿名成員在內的全部成員。通過在path添加“.f”來表示成員路徑,我們必須獲得結構體對應的reflect.Type類型信息,包含結構體類型和第i個成員的名字。
|
||||
**结构体:** 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的任務。)
|
||||
**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前面添加“*”,併用括弧包含以避免歧義。
|
||||
**指针:** Elem方法返回指针指向的变量,还是reflect.Value类型。技术指针是nil,这个操作也是安全的,在这种情况下指针是Invalid无效类型,但是我们可以用IsNil方法来显式地测试一个空指针,这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。
|
||||
|
||||
**接口:** 再一次,我們使用IsNil方法來測試接口是否是nil,如果不是,我們可以調用v.Elem()來獲取接口對應的動態值,併且打印對應的類型和值。
|
||||
**接口:** 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
|
||||
|
||||
現在我們的Display函數總算完工了,讓我們看看它的表現吧。下面的Movie類型是在4.5節的電影類型上演變來的:
|
||||
现在我们的Display函数总算完工了,让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:
|
||||
|
||||
```Go
|
||||
type Movie struct {
|
||||
@@ -99,7 +99,7 @@ type Movie struct {
|
||||
}
|
||||
```
|
||||
|
||||
讓我們聲明一個該類型的變量,然後看看Display函數如何顯示它:
|
||||
让我们声明一个该类型的变量,然后看看Display函数如何显示它:
|
||||
|
||||
```Go
|
||||
strangelove := Movie{
|
||||
@@ -125,7 +125,7 @@ strangelove := Movie{
|
||||
}
|
||||
```
|
||||
|
||||
Display("strangelove", strangelove)調用將顯示(strangelove電影對應的中文名是《奇愛博士》):
|
||||
Display("strangelove", strangelove)调用将显示(strangelove电影对应的中文名是《奇爱博士》):
|
||||
|
||||
```Go
|
||||
Display strangelove (display.Movie):
|
||||
@@ -146,7 +146,7 @@ strangelove.Oscars[3] = "Best Picture (Nomin.)"
|
||||
strangelove.Sequel = nil
|
||||
```
|
||||
|
||||
我們也可以使用Display函數來顯示標準庫中類型的內部結構,例如`*os.File`類型:
|
||||
我们也可以使用Display函数来显示标准库中类型的内部结构,例如`*os.File`类型:
|
||||
|
||||
```Go
|
||||
Display("os.Stderr", os.Stderr)
|
||||
@@ -157,7 +157,7 @@ Display("os.Stderr", os.Stderr)
|
||||
// (*(*os.Stderr).file).nepipe = 0
|
||||
```
|
||||
|
||||
要註意的是,結構體中未導出的成員對反射也是可見的。需要當心的是這個例子的輸出在不同操作繫統上可能是不同的,併且隨着標準庫的發展也可能導致結果不同。(這也是將這些成員定義爲私有成員的原因之一!)我們深圳可以用Display函數來顯示reflect.Value,來査看`*os.File`類型的內部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`調用的輸出如下,當然不同環境得到的結果可能有差異:
|
||||
要注意的是,结构体中未导出的成员对反射也是可见的。需要当心的是这个例子的输出在不同操作系统上可能是不同的,并且随着标准库的发展也可能导致结果不同。(这也是将这些成员定义为私有成员的原因之一!)我们深圳可以用Display函数来显示reflect.Value,来查看`*os.File`类型的内部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异:
|
||||
|
||||
```Go
|
||||
Display rV (reflect.Value):
|
||||
@@ -174,7 +174,7 @@ Display rV (reflect.Value):
|
||||
...
|
||||
```
|
||||
|
||||
觀察下面兩個例子的區别:
|
||||
观察下面两个例子的区别:
|
||||
|
||||
```Go
|
||||
var i interface{} = 3
|
||||
@@ -191,11 +191,11 @@ Display("&i", &i)
|
||||
// (*&i).value = 3
|
||||
```
|
||||
|
||||
在第一個例子中,Display函數將調用reflect.ValueOf(i),它返迴一個Int類型的值。正如我們在12.2節中提到的,reflect.ValueOf總是返迴一個值的具體類型,因爲它是從一個接口值提取的內容。
|
||||
在第一个例子中,Display函数将调用reflect.ValueOf(i),它返回一个Int类型的值。正如我们在12.2节中提到的,reflect.ValueOf总是返回一个值的具体类型,因为它是从一个接口值提取的内容。
|
||||
|
||||
在第二個例子中,Display函數調用的是reflect.ValueOf(&i),它返迴一個指向i的指針,對應Ptr類型。在switch的Ptr分支中,通過調用Elem來返迴這個值,返迴一個Value來表示i,對應Interface類型。一個間接獲得的Value,就像這一個,可能代表任意類型的值,包括接口類型。內部的display函數遞歸調用自身,這次它將打印接口的動態類型和值。
|
||||
在第二个例子中,Display函数调用的是reflect.ValueOf(&i),它返回一个指向i的指针,对应Ptr类型。在switch的Ptr分支中,通过调用Elem来返回这个值,返回一个Value来表示i,对应Interface类型。一个间接获得的Value,就像这一个,可能代表任意类型的值,包括接口类型。内部的display函数递归调用自身,这次它将打印接口的动态类型和值。
|
||||
|
||||
目前的實現,Display如果顯示一個帶環的數據結構將會陷入死循環,例如首位項鏈的鏈表:
|
||||
目前的实现,Display如果显示一个带环的数据结构将会陷入死循环,例如首位项链的链表:
|
||||
|
||||
```Go
|
||||
// a struct that points to itself
|
||||
@@ -205,7 +205,7 @@ c = Cycle{42, &c}
|
||||
Display("c", c)
|
||||
```
|
||||
|
||||
Display會永遠不停地進行深度遞歸打印:
|
||||
Display会永远不停地进行深度递归打印:
|
||||
|
||||
```Go
|
||||
Display c (display.Cycle):
|
||||
@@ -216,10 +216,10 @@ c.Value = 42
|
||||
...ad infinitum...
|
||||
```
|
||||
|
||||
許多Go語言程序都包含了一些循環的數據結果。Display支持這類帶環的數據結構是比較棘手的,需要增加一個額外的記録訪問的路徑;代價是昂貴的。一般的解決方案是采用不安全的語言特性,我們將在13.3節看到具體的解決方案。
|
||||
许多Go语言程序都包含了一些循环的数据结果。Display支持这类带环的数据结构是比较棘手的,需要增加一个额外的记录访问的路径;代价是昂贵的。一般的解决方案是采用不安全的语言特性,我们将在13.3节看到具体的解决方案。
|
||||
|
||||
帶環的數據結構很少會對fmt.Sprint函數造成問題,因爲它很少嚐試打印完整的數據結構。例如,當它遇到一個指針的時候,它隻是簡單第打印指針的數值。雖然,在打印包含自身的slice或map時可能遇到睏難,但是不保證處理這種是罕見情況卻可以避免額外的麻煩。
|
||||
带环的数据结构很少会对fmt.Sprint函数造成问题,因为它很少尝试打印完整的数据结构。例如,当它遇到一个指针的时候,它只是简单第打印指针的数值。虽然,在打印包含自身的slice或map时可能遇到困难,但是不保证处理这种是罕见情况却可以避免额外的麻烦。
|
||||
|
||||
**練習 12.1:** 擴展Displayhans,以便它可以顯示包含以結構體或數組作爲map的key類型的值。
|
||||
**练习 12.1:** 扩展Displayhans,以便它可以显示包含以结构体或数组作为map的key类型的值。
|
||||
|
||||
**練習 12.2:** 增強display函數的穩健性,通過記録邊界的步數來確保在超出一定限製前放棄遞歸。(在13.3節,我們會看到另一種探測數據結構是否存在環的技術。)
|
||||
**练习 12.2:** 增强display函数的稳健性,通过记录边界的步数来确保在超出一定限制前放弃递归。(在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)
|
||||
|
||||
Reference in New Issue
Block a user