mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 19:24:19 +08:00
回到简体
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
## 12.1. 爲何需要反射?
|
||||
## 12.1. 为何需要反射?
|
||||
|
||||
有時候我們需要編寫一個函數能夠處理一類併不滿足普通公共接口的類型的值,也可能是因爲它們併沒有確定的表示方式,或者是在我們設計該函數的時候還這些類型可能還不存在,各種情況都有可能。
|
||||
有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候还这些类型可能还不存在,各种情况都有可能。
|
||||
|
||||
一個大家熟悉的例子是fmt.Fprintf函數提供的字符串格式化處理邏輯,它可以用例對任意類型的值格式化併打印,甚至支持用戶自定義的類型。讓我們也來嚐試實現一個類似功能的函數。爲了簡單起見,我們的函數隻接收一個參數,然後返迴和fmt.Sprint類似的格式化後的字符串。我們實現的函數名也叫Sprint。
|
||||
一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用例对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。
|
||||
|
||||
我們使用了switch類型分支首先來測試輸入參數是否實現了String方法,如果是的話就使用該方法。然後繼續增加類型測試分支,檢査是否是每個基於string、int、bool等基礎類型的動態類型,併在每種情況下執行相應的格式化操作。
|
||||
我们使用了switch类型分支首先来测试输入参数是否实现了String方法,如果是的话就使用该方法。然后继续增加类型测试分支,检查是否是每个基于string、int、bool等基础类型的动态类型,并在每种情况下执行相应的格式化操作。
|
||||
|
||||
```Go
|
||||
func Sprint(x interface{}) string {
|
||||
@@ -31,6 +31,6 @@ func Sprint(x interface{}) string {
|
||||
}
|
||||
```
|
||||
|
||||
但是我們如何處理其它類似[]float64、map[string][]string等類型呢?我們當然可以添加更多的測試分支,但是這些組合類型的數目基本是無窮的。還有如何處理url.Values等命名的類型呢?雖然類型分支可以識别出底層的基礎類型是map[string][]string,但是它併不匹配url.Values類型,因爲它們是兩種不同的類型,而且switch類型分支也不可能包含每個類似url.Values的類型,這會導致對這些庫的循環依賴。
|
||||
但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理url.Values等命名的类型呢?虽然类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的循环依赖。
|
||||
|
||||
沒有一種方法來檢査未知類型的表示方式,我們被卡住了。這就是我們爲何需要反射的原因。
|
||||
没有一种方法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原因。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## 12.2. reflect.Type和reflect.Value
|
||||
|
||||
反射是由 reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個Go類型. 它是一個接口, 有許多方法來區分類型和檢査它們的組件, 例如一個結構體的成員或一個函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 同樣的實體標識了動態類型的接口值.
|
||||
反射是由 reflect 包提供支持. 它定义了两个重要的类型, Type 和 Value. 一个 Type 表示一个Go类型. 它是一个接口, 有许多方法来区分类型和检查它们的组件, 例如一个结构体的成员或一个函数的参数等. 唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5), 同样的实体标识了动态类型的接口值.
|
||||
|
||||
函數 reflect.TypeOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Type:
|
||||
函数 reflect.TypeOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Type:
|
||||
|
||||
```Go
|
||||
t := reflect.TypeOf(3) // a reflect.Type
|
||||
@@ -10,22 +10,22 @@ fmt.Println(t.String()) // "int"
|
||||
fmt.Println(t) // "int"
|
||||
```
|
||||
|
||||
其中 TypeOf(3) 調用將值 3 作爲 interface{} 類型參數傳入. 迴到 7.5節 的將一個具體的值轉爲接口類型會有一個隱式的接口轉換操作, 它會創建一個包含兩個信息的接口值: 操作數的動態類型(這里是int)和它的動態的值(這里是3).
|
||||
其中 TypeOf(3) 调用将值 3 作为 interface{} 类型参数传入. 回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作, 它会创建一个包含两个信息的接口值: 操作数的动态类型(这里是int)和它的动态的值(这里是3).
|
||||
|
||||
因爲 reflect.TypeOf 返迴的是一個動態類型的接口值, 它總是返迴具體的類型. 因此, 下面的代碼將打印 "*os.File" 而不是 "io.Writer". 稍後, 我們將看到 reflect.Type 是具有識别接口類型的表達方式功能的.
|
||||
因为 reflect.TypeOf 返回的是一个动态类型的接口值, 它总是返回具体的类型. 因此, 下面的代码将打印 "*os.File" 而不是 "io.Writer". 稍后, 我们将看到 reflect.Type 是具有识别接口类型的表达方式功能的.
|
||||
|
||||
```Go
|
||||
var w io.Writer = os.Stdout
|
||||
fmt.Println(reflect.TypeOf(w)) // "*os.File"
|
||||
```
|
||||
|
||||
要註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸出:
|
||||
要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的. 因为打印动态类型值对于调试和日志是有帮助的, fmt.Printf 提供了一个简短的 %T 标志参数, 内部使用 reflect.TypeOf 的结果输出:
|
||||
|
||||
```Go
|
||||
fmt.Printf("%T\n", 3) // "int"
|
||||
```
|
||||
|
||||
reflect 包中另一個重要的類型是 Value. 一個 reflect.Value 可以持有一個任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Value. 和 reflect.TypeOf 類似, reflect.ValueOf 返迴的結果也是對於具體的類型, 但是 reflect.Value 也可以持有一個接口值.
|
||||
reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以持有一个任意类型的值. 函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Value. 和 reflect.TypeOf 类似, reflect.ValueOf 返回的结果也是对于具体的类型, 但是 reflect.Value 也可以持有一个接口值.
|
||||
|
||||
```Go
|
||||
v := reflect.ValueOf(3) // a reflect.Value
|
||||
@@ -34,16 +34,16 @@ fmt.Printf("%v\n", v) // "3"
|
||||
fmt.Println(v.String()) // NOTE: "<int Value>"
|
||||
```
|
||||
|
||||
和 reflect.Type 類似, reflect.Value 也滿足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否則 String 隻是返迴具體的類型. 相同, 使用 fmt 包的 %v 標誌參數, 將使用 reflect.Values 的結果格式化.
|
||||
和 reflect.Type 类似, reflect.Value 也满足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否则 String 只是返回具体的类型. 相同, 使用 fmt 包的 %v 标志参数, 将使用 reflect.Values 的结果格式化.
|
||||
|
||||
調用 Value 的 Type 方法將返迴具體類型所對應的 reflect.Type:
|
||||
调用 Value 的 Type 方法将返回具体类型所对应的 reflect.Type:
|
||||
|
||||
```Go
|
||||
t := v.Type() // a reflect.Type
|
||||
fmt.Println(t.String()) // "int"
|
||||
```
|
||||
|
||||
逆操作是調用 reflect.ValueOf 對應的 reflect.Value.Interface 方法. 它返迴一個 interface{} 類型表示 reflect.Value 對應類型的具體值:
|
||||
逆操作是调用 reflect.ValueOf 对应的 reflect.Value.Interface 方法. 它返回一个 interface{} 类型表示 reflect.Value 对应类型的具体值:
|
||||
|
||||
```Go
|
||||
v := reflect.ValueOf(3) // a reflect.Value
|
||||
@@ -52,9 +52,9 @@ i := x.(int) // an int
|
||||
fmt.Printf("%d\n", i) // "3"
|
||||
```
|
||||
|
||||
一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型才能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值併沒有特别可做的事情. 相比之下, 一個 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麽. 讓我們再次嚐試實現我們的格式化函數 format.Any.
|
||||
一个 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一个空的接口隐藏了值对应的表示方式和所有的公开的方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样), 对于内部值并没有特别可做的事情. 相比之下, 一个 Value 则有很多方法来检查其内容, 无论它的具体类型是什么. 让我们再次尝试实现我们的格式化函数 format.Any.
|
||||
|
||||
我們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)
|
||||
我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch. 虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的: Bool, String 和 所有数字类型的基础类型; Array 和 Struct 对应的聚合类型; Chan, Func, Ptr, Slice, 和 Map 对应的引用类似; 接口类型; 还有表示空值的无效类型. (空的 reflect.Value 对应 Invalid 无效类型.)
|
||||
|
||||
<u><i>gopl.io/ch12/format</i></u>
|
||||
```Go
|
||||
@@ -95,7 +95,7 @@ func formatAtom(v reflect.Value) string {
|
||||
}
|
||||
```
|
||||
|
||||
到目前爲止, 我們的函數將每個值視作一個不可分割沒有內部結構的, 因此它叫 formatAtom. 對於聚合類型(結構體和數組)個接口隻是打印類型的值, 對於引用類型(channels, functions, pointers, slices, 和 maps), 它十六進製打印類型的引用地址. 雖然還不夠理想, 但是依然是一個重大的進步, 併且 Kind 隻關心底層表示, format.Any 也支持新命名的類型. 例如:
|
||||
到目前为止, 我们的函数将每个值视作一个不可分割没有内部结构的, 因此它叫 formatAtom. 对于聚合类型(结构体和数组)个接口只是打印类型的值, 对于引用类型(channels, functions, pointers, slices, 和 maps), 它十六进制打印类型的引用地址. 虽然还不够理想, 但是依然是一个重大的进步, 并且 Kind 只关心底层表示, format.Any 也支持新命名的类型. 例如:
|
||||
|
||||
```Go
|
||||
var x int64 = 1
|
||||
|
||||
@@ -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节,我们会看到另一种探测数据结构是否存在环的技术。)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 12.4. 示例: 編碼S表達式
|
||||
## 12.4. 示例: 编码S表达式
|
||||
|
||||
Display是一個用於顯示結構化數據的調試工具,但是它併不能將任意的Go語言對象編碼爲通用消息然後用於進程間通信。
|
||||
Display是一个用于显示结构化数据的调试工具,但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。
|
||||
|
||||
正如我們在4.5節中中看到的,Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式,采用類似Lisp語言的語法。但是和其他編碼格式不同的是,Go語言自帶的標準庫併不支持S表達式,主要是因爲它沒有一個公認的標準規范。
|
||||
正如我们在4.5节中中看到的,Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式,采用类似Lisp语言的语法。但是和其他编码格式不同的是,Go语言自带的标准库并不支持S表达式,主要是因为它没有一个公认的标准规范。
|
||||
|
||||
在本節中,我們將定義一個包用於將Go語言的對象編碼爲S表達式格式,它支持以下結構:
|
||||
在本节中,我们将定义一个包用于将Go语言的对象编码为S表达式格式,它支持以下结构:
|
||||
|
||||
```
|
||||
42 integer
|
||||
@@ -13,13 +13,13 @@ foo symbol (an unquoted name)
|
||||
(1 2 3) list (zero or more items enclosed in parentheses)
|
||||
```
|
||||
|
||||
布爾型習慣上使用t符號表示true,空列表或nil符號表示false,但是爲了簡單起見,我們暫時忽略布爾類型。同時忽略的還有chan管道和函數,因爲通過反射併無法知道它們的確切狀態。我們忽略的還浮點數、複數和interface。支持它們是練習12.3的任務。
|
||||
布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还浮点数、复数和interface。支持它们是练习12.3的任务。
|
||||
|
||||
我們將Go語言的類型編碼爲S表達式的方法如下。整數和字符串以自然的方式編碼。Nil值編碼爲nil符號。數組和slice被編碼爲一個列表。
|
||||
我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以自然的方式编码。Nil值编码为nil符号。数组和slice被编码为一个列表。
|
||||
|
||||
結構體被編碼爲成員對象的列表,每個成員對象對應一個個僅有兩個元素的子列表,其中子列表的第一個元素是成員的名字,子列表的第二個元素是成員的值。Map被編碼爲鍵值對的列表。傳統上,S表達式使用點狀符號列表(key . value)結構來表示key/value對,而不是用一個含雙元素的列表,不過爲了簡單我們忽略了點狀符號列表。
|
||||
结构体被编码为成员对象的列表,每个成员对象对应一个个仅有两个元素的子列表,其中子列表的第一个元素是成员的名字,子列表的第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。
|
||||
|
||||
編碼是由一個encode遞歸函數完成,如下所示。它的結構本質上和前面的Display函數類似:
|
||||
编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似:
|
||||
|
||||
<u><i>gopl.io/ch12/sexpr</i></u>
|
||||
```Go
|
||||
@@ -93,7 +93,7 @@ func encode(buf *bytes.Buffer, v reflect.Value) error {
|
||||
}
|
||||
```
|
||||
|
||||
Marshal函數是對encode的保證,以保持和encoding/...下其它包有着相似的API:
|
||||
Marshal函数是对encode的保证,以保持和encoding/...下其它包有着相似的API:
|
||||
|
||||
```Go
|
||||
// Marshal encodes a Go value in S-expression form.
|
||||
@@ -106,7 +106,7 @@ func Marshal(v interface{}) ([]byte, error) {
|
||||
}
|
||||
```
|
||||
|
||||
下面是Marshal對12.3節的strangelove變量編碼後的結果:
|
||||
下面是Marshal对12.3节的strangelove变量编码后的结果:
|
||||
|
||||
```
|
||||
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
|
||||
@@ -118,7 +118,7 @@ ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
|
||||
omin.)" "Best Picture (Nomin.)")) (Sequel nil))
|
||||
```
|
||||
|
||||
整個輸出編碼爲一行中以減少輸出的大小,但是也很難閲讀。這里有一個對S表達式格式化的約定。編寫一個S表達式的格式化函數將作爲一個具有挑戰性的練習任務;不過 http://gopl.io 也提供了一個簡單的版本。
|
||||
整个输出编码为一行中以减少输出的大小,但是也很难阅读。这里有一个对S表达式格式化的约定。编写一个S表达式的格式化函数将作为一个具有挑战性的练习任务;不过 http://gopl.io 也提供了一个简单的版本。
|
||||
|
||||
```
|
||||
((Title "Dr. Strangelove")
|
||||
@@ -137,16 +137,16 @@ omin.)" "Best Picture (Nomin.)")) (Sequel nil))
|
||||
(Sequel nil))
|
||||
```
|
||||
|
||||
和fmt.Print、json.Marshal、Display函數類似,sexpr.Marshal函數處理帶環的數據結構也會陷入死循環。
|
||||
和fmt.Print、json.Marshal、Display函数类似,sexpr.Marshal函数处理带环的数据结构也会陷入死循环。
|
||||
|
||||
在12.6節中,我們將給出S表達式解碼器的實現步驟,但是在那之前,我們還需要先了解如果通過反射技術來更新程序的變量。
|
||||
在12.6节中,我们将给出S表达式解码器的实现步骤,但是在那之前,我们还需要先了解如果通过反射技术来更新程序的变量。
|
||||
|
||||
**練習 12.3:** 實現encode函數缺少的分支。將布爾類型編碼爲t和nil,浮點數編碼爲Go語言的格式,複數1+2i編碼爲#C(1.0 2.0)格式。接口編碼爲類型名和值對,例如("[]int" (1 2 3)),但是這個形式可能會造成歧義:reflect.Type.String方法對於不同的類型可能返迴相同的結果。
|
||||
**练习 12.3:** 实现encode函数缺少的分支。将布尔类型编码为t和nil,浮点数编码为Go语言的格式,复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如("[]int" (1 2 3)),但是这个形式可能会造成歧义:reflect.Type.String方法对于不同的类型可能返回相同的结果。
|
||||
|
||||
**練習 12.4:** 脩改encode函數,以上面的格式化形式輸出S表達式。
|
||||
**练习 12.4:** 修改encode函数,以上面的格式化形式输出S表达式。
|
||||
|
||||
**練習 12.5:** 脩改encode函數,用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。
|
||||
**练习 12.5:** 修改encode函数,用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。
|
||||
|
||||
**練習 12.6:** 脩改encode,作爲一個優化,忽略對是零值對象的編碼。
|
||||
**练习 12.6:** 修改encode,作为一个优化,忽略对是零值对象的编码。
|
||||
|
||||
**練習 12.7:** 創建一個基於流式的API,用於S表達式的解碼,和json.Decoder(§4.5)函數功能類似。
|
||||
**练习 12.7:** 创建一个基于流式的API,用于S表达式的解码,和json.Decoder(§4.5)函数功能类似。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 12.5. 通過reflect.Value脩改值
|
||||
## 12.5. 通过reflect.Value修改值
|
||||
|
||||
到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。
|
||||
到目前为止,反射还只是程序中变量的另一种访问方式。然而,在本节中我们将重点讨论如果通过反射机制来修改变量。
|
||||
|
||||
迴想一下,Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量,但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。
|
||||
回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
|
||||
|
||||
對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的;其它一些則不可以。考慮以下的聲明語句:
|
||||
对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以。考虑以下的声明语句:
|
||||
|
||||
```Go
|
||||
x := 2 // value type variable?
|
||||
@@ -14,9 +14,9 @@ c := reflect.ValueOf(&x) // &x *int no
|
||||
d := c.Elem() // 2 int yes (x)
|
||||
```
|
||||
|
||||
其中a對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址,它隻是一個指針`&x`的拷貝。實際上,所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d,它是c的解引用方式生成的,指向另一個變量,因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem(),來獲取任意變量x對應的可取地址的Value。
|
||||
其中a对应的变量则不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针`&x`的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。
|
||||
|
||||
我們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址:
|
||||
我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:
|
||||
|
||||
```Go
|
||||
fmt.Println(a.CanAddr()) // "false"
|
||||
@@ -25,9 +25,9 @@ fmt.Println(c.CanAddr()) // "false"
|
||||
fmt.Println(d.CanAddr()) // "true"
|
||||
```
|
||||
|
||||
每當我們通過指針間接地獲取的reflect.Value都是可取地址的,卽使開始的是一個不可取地址的Value。在反射機製中,所有關於是否支持取地址的規則都是類似的。例如,slice的索引表達式e[i]將隱式地包含一個指針,它就是可取地址的,卽使開始的e表達式不支持也沒有關繫。以此類推,reflect.ValueOf(e).Index(i)對於的值也是可取地址的,卽使原始的reflect.ValueOf(e)不支持也沒有關繫。
|
||||
每当我们通过指针间接地获取的reflect.Value都是可取地址的,即使开始的是一个不可取地址的Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice的索引表达式e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。以此类推,reflect.ValueOf(e).Index(i)对于的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也没有关系。
|
||||
|
||||
要從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法,它返迴一個Value,里面保存了指向變量的指針。然後是在Value上調用Interface()方法,也就是返迴一個interface{},里面通用包含指向變量的指針。最後,如果我們知道變量的類型,我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了:
|
||||
要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针。然后是在Value上调用Interface()方法,也就是返回一个interface{},里面通用包含指向变量的指针。最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:
|
||||
|
||||
```Go
|
||||
x := 2
|
||||
@@ -37,20 +37,20 @@ px := d.Addr().Interface().(*int) // px := &x
|
||||
fmt.Println(x) // "3"
|
||||
```
|
||||
|
||||
或者,不使用指針,而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值:
|
||||
或者,不使用指针,而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对于的值:
|
||||
|
||||
```Go
|
||||
d.Set(reflect.ValueOf(4))
|
||||
fmt.Println(x) // "4"
|
||||
```
|
||||
|
||||
Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼,變量和值都是int類型,但是如果變量是int64類型,那麽程序將拋出一個panic異常,所以關鍵問題是要確保改類型的變量可以接受對應的值:
|
||||
Set方法将在运行时执行和编译时类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值:
|
||||
|
||||
```Go
|
||||
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
|
||||
```
|
||||
|
||||
通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常:
|
||||
通用对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:
|
||||
|
||||
```Go
|
||||
x := 2
|
||||
@@ -58,7 +58,7 @@ b := reflect.ValueOf(x)
|
||||
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
|
||||
```
|
||||
|
||||
這里有很多用於基本數據類型的Set方法:SetInt、SetUint、SetString和SetFloat等。
|
||||
这里有很多用于基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等。
|
||||
|
||||
```Go
|
||||
d := reflect.ValueOf(&x).Elem()
|
||||
@@ -66,7 +66,7 @@ d.SetInt(3)
|
||||
fmt.Println(x) // "3"
|
||||
```
|
||||
|
||||
從某種程度上説,這些Set方法總是盡可能地完成任務。以SetInt爲例,隻要變量是某種類型的有符號整數就可以工作,卽使是一些命名的類型,隻要底層數據類型是有符號整數就可以,而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是:對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常,卽使那個interface{}變量對於整數類型也不行。
|
||||
从某种程度上说,这些Set方法总是尽可能地完成任务。以SetInt为例,只要变量是某种类型的有符号整数就可以工作,即使是一些命名的类型,只要底层数据类型是有符号整数就可以,而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是:对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使那个interface{}变量对于整数类型也不行。
|
||||
|
||||
```Go
|
||||
x := 1
|
||||
@@ -84,7 +84,7 @@ ry.SetString("hello") // panic: SetString called on interface Value
|
||||
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
|
||||
```
|
||||
|
||||
當我們用Display顯示os.Stdout結構時,我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員,比如在類Unix繫統上os.File結構體中的fd int成員。然而,利用反射機製併不能脩改這些未導出的成員:
|
||||
当我们用Display显示os.Stdout结构时,我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,比如在类Unix系统上os.File结构体中的fd int成员。然而,利用反射机制并不能修改这些未导出的成员:
|
||||
|
||||
```Go
|
||||
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
|
||||
@@ -94,7 +94,7 @@ fmt.Println(fd.Int()) // "1"
|
||||
fd.SetInt(2) // panic: unexported field
|
||||
```
|
||||
|
||||
一個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員,如果是的話則拒絶脩改操作。因此,CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的:
|
||||
一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。因此,CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的:
|
||||
|
||||
```Go
|
||||
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 12.6. 示例: 解碼S表達式
|
||||
## 12.6. 示例: 解码S表达式
|
||||
|
||||
標準庫中encoding/...下每個包中提供的Marshal編碼函數都有一個對應的Unmarshal函數用於解碼。例如,我們在4.5節中看到的,要將包含JSON編碼格式的字節slice數據解碼爲我們自己的Movie類型(§12.3),我們可以這樣做:
|
||||
标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如,我们在4.5节中看到的,要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型(§12.3),我们可以这样做:
|
||||
|
||||
```Go
|
||||
data := []byte{/* ... */}
|
||||
@@ -8,13 +8,13 @@ var movie Movie
|
||||
err := json.Unmarshal(data, &movie)
|
||||
```
|
||||
|
||||
Unmarshal函數使用了反射機製類脩改movie變量的每個成員,根據輸入的內容爲Movie成員創建對應的map、結構體和slice。
|
||||
Unmarshal函数使用了反射机制类修改movie变量的每个成员,根据输入的内容为Movie成员创建对应的map、结构体和slice。
|
||||
|
||||
現在讓我們爲S表達式編碼實現一個簡易的Unmarshal,類似於前面的json.Unmarshal標準庫函數,對應我們之前實現的sexpr.Marshal函數的逆操作。我們必須提醒一下,一個健壯的和通用的實現通常需要比例子更多的代碼,爲了便於演示我們采用了精簡的實現。我們隻支持S表達式有限的子集,同時處理錯誤的方式也比較粗暴,代碼的目的是爲了演示反射的用法,而不是構造一個實用的S表達式的解碼器。
|
||||
现在让我们为S表达式编码实现一个简易的Unmarshal,类似于前面的json.Unmarshal标准库函数,对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下,一个健壮的和通用的实现通常需要比例子更多的代码,为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集,同时处理错误的方式也比较粗暴,代码的目的是为了演示反射的用法,而不是构造一个实用的S表达式的解码器。
|
||||
|
||||
詞法分析器lexer使用了標準庫中的text/scanner包將輸入流的字節數據解析爲一個個類似註釋、標識符、字符串面值和數字面值之類的標記。輸入掃描器scanner的Scan方法將提前掃描和返迴下一個記號,對於rune類型。大多數記號,比如“(”,對應一個單一rune可表示的Unicode字符,但是text/scanner也可以用小的負數表示記號標識符、字符串等由多個字符組成的記號。調用Scan方法將返迴這些記號的類型,接着調用TokenText方法將返迴記號對應的文本內容。
|
||||
词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号,对于rune类型。大多数记号,比如“(”,对应一个单一rune可表示的Unicode字符,但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型,接着调用TokenText方法将返回记号对应的文本内容。
|
||||
|
||||
因爲每個解析器可能需要多次使用當前的記號,但是Scan會一直向前掃描,所有我們包裝了一個lexer掃描器輔助類型,用於跟蹤最近由Scan方法返迴的記號。
|
||||
因为每个解析器可能需要多次使用当前的记号,但是Scan会一直向前扫描,所有我们包装了一个lexer扫描器辅助类型,用于跟踪最近由Scan方法返回的记号。
|
||||
|
||||
<u><i>gopl.io/ch12/sexpr</i></u>
|
||||
```Go
|
||||
@@ -34,7 +34,7 @@ func (lex *lexer) consume(want rune) {
|
||||
}
|
||||
```
|
||||
|
||||
現在讓我們轉到語法解析器。它主要包含兩個功能。第一個是read函數,用於讀取S表達式的當前標記,然後根據S表達式的當前標記更新可取地址的reflect.Value對應的變量v。
|
||||
现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数,用于读取S表达式的当前标记,然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。
|
||||
|
||||
```Go
|
||||
func read(lex *lexer, v reflect.Value) {
|
||||
@@ -67,13 +67,13 @@ func read(lex *lexer, v reflect.Value) {
|
||||
}
|
||||
```
|
||||
|
||||
我們的S表達式使用標識符區分兩個不同類型,結構體成員名和nil值的指針。read函數值處理nil類型的標識符。當遇到scanner.Ident爲“nil”是,使用reflect.Zero函數將變量v設置爲零值。而其它任何類型的標識符,我們都作爲錯誤處理。後面的readList函數將處理結構體的成員名。
|
||||
我们的S表达式使用标识符区分两个不同类型,结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”是,使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符,我们都作为错误处理。后面的readList函数将处理结构体的成员名。
|
||||
|
||||
一個“(”標記對應一個列表的開始。第二個函數readList,將一個列表解碼到一個聚合類型中(map、結構體、slice或數組),具體類型依然於傳入待填充變量的類型。每次遇到這種情況,循環繼續解析每個元素直到遇到於開始標記匹配的結束標記“)”,endList函數用於檢測結束標記。
|
||||
一个“(”标记对应一个列表的开始。第二个函数readList,将一个列表解码到一个聚合类型中(map、结构体、slice或数组),具体类型依然于传入待填充变量的类型。每次遇到这种情况,循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”,endList函数用于检测结束标记。
|
||||
|
||||
最有趣的部分是遞歸。最簡單的是對數組類型的處理。直到遇到“)”結束標記,我們使用Index函數來獲取數組每個元素的地址,然後遞歸調用read函數處理。和其它錯誤類似,如果輸入數據導致解碼器的引用超出了數組的范圍,解碼器將拋出panic異常。slice也采用類似方法解析,不同的是我們將爲每個元素創建新的變量,然後將元素添加到slice的末尾。
|
||||
最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记,我们使用Index函数来获取数组每个元素的地址,然后递归调用read函数处理。和其它错误类似,如果输入数据导致解码器的引用超出了数组的范围,解码器将抛出panic异常。slice也采用类似方法解析,不同的是我们将为每个元素创建新的变量,然后将元素添加到slice的末尾。
|
||||
|
||||
在循環處理結構體和map每個元素時必須解碼一個(key value)格式的對應子列表。對於結構體,key部分對於成員的名字。和數組類似,我們使用FieldByName找到結構體對應成員的變量,然後遞歸調用read函數處理。對於map,key可能是任意類型,對元素的處理方式和slice類似,我們創建一個新的變量,然後遞歸填充它,最後將新解析到的key/value對添加到map。
|
||||
在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体,key部分对于成员的名字。和数组类似,我们使用FieldByName找到结构体对应成员的变量,然后递归调用read函数处理。对于map,key可能是任意类型,对元素的处理方式和slice类似,我们创建一个新的变量,然后递归填充它,最后将新解析到的key/value对添加到map。
|
||||
|
||||
```Go
|
||||
func readList(lex *lexer, v reflect.Value) {
|
||||
@@ -130,7 +130,7 @@ func endList(lex *lexer) bool {
|
||||
}
|
||||
```
|
||||
|
||||
最後,我們將解析器包裝爲導出的Unmarshal解碼函數,隱藏了一些初始化和清理等邊緣處理。內部解析器以panic的方式拋出錯誤,但是Unmarshal函數通過在defer語句調用recover函數來捕獲內部panic(§5.10),然後返迴一個對panic對應的錯誤信息。
|
||||
最后,我们将解析器包装为导出的Unmarshal解码函数,隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误,但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic(§5.10),然后返回一个对panic对应的错误信息。
|
||||
|
||||
```Go
|
||||
// Unmarshal parses S-expression data and populates the variable
|
||||
@@ -150,10 +150,10 @@ func Unmarshal(data []byte, out interface{}) (err error) {
|
||||
}
|
||||
```
|
||||
|
||||
生産實現不應該對任何輸入問題都用panic形式報告,而且應該報告一些錯誤相關的信息,例如出現錯誤輸入的行號和位置等。盡管如此,我們希望通過這個例子來展示類似encoding/json等包底層代碼的實現思路,以及如何使用反射機製來填充數據結構。
|
||||
生产实现不应该对任何输入问题都用panic形式报告,而且应该报告一些错误相关的信息,例如出现错误输入的行号和位置等。尽管如此,我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路,以及如何使用反射机制来填充数据结构。
|
||||
|
||||
**練習 12.8:** sexpr.Unmarshal函數和json.Unmarshal一樣,都要求在解碼前輸入完整的字節slice。定義一個和json.Decoder類似的sexpr.Decoder類型,支持從一個io.Reader流解碼。脩改sexpr.Unmarshal函數,使用這個新的類型實現。
|
||||
**练习 12.8:** sexpr.Unmarshal函数和json.Unmarshal一样,都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型,支持从一个io.Reader流解码。修改sexpr.Unmarshal函数,使用这个新的类型实现。
|
||||
|
||||
**練習 12.9:** 編寫一個基於標記的API用於解碼S表達式,參考xml.Decoder(7.14)的風格。你將需要五種類型的標記:Symbol、String、Int、StartList和EndList。
|
||||
**练习 12.9:** 编写一个基于标记的API用于解码S表达式,参考xml.Decoder(7.14)的风格。你将需要五种类型的标记:Symbol、String、Int、StartList和EndList。
|
||||
|
||||
**練習 12.10:** 擴展sexpr.Unmarshal函數,支持布爾型、浮點數和interface類型的解碼,使用 **練習 12.3:** 的方案。(提示:要解碼接口,你需要將name映射到每個支持類型的reflect.Type。)
|
||||
**练习 12.10:** 扩展sexpr.Unmarshal函数,支持布尔型、浮点数和interface类型的解码,使用 **练习 12.3:** 的方案。(提示:要解码接口,你需要将name映射到每个支持类型的reflect.Type。)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 12.7. 獲取結構體字段標識
|
||||
## 12.7. 获取结构体字段标识
|
||||
|
||||
在4.5節我們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓我們可以選擇成員的名字和抑製零值成員的輸出。在本節,我們將看到如果通過反射機製類獲取成員標籤。
|
||||
在4.5节我们使用构体成员标签用于设置对应JSON对应的名字。其中json成员标签让我们可以选择成员的名字和抑制零值成员的输出。在本节,我们将看到如果通过反射机制类获取成员标签。
|
||||
|
||||
對於一個web服務,大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數,叫params.Unpack,通過使用結構體成員標籤機製來讓HTTP處理函數解析請求參數更方便。
|
||||
对于一个web服务,大部分HTTP处理函数要做的第一件事情就是展开请求中的参数到本地变量中。我们定义了一个工具函数,叫params.Unpack,通过使用结构体成员标签机制来让HTTP处理函数解析请求参数更方便。
|
||||
|
||||
首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每個成員表示HTTP請求的參數。其中結構體成員標籤指明了對於請求參數的名字,爲了減少URL的長度這些參數名通常都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中,這樣我們可以方便地通過合適的類型類來訪問這些參數。
|
||||
首先,我们看看如何使用它。下面的search函数是一个HTTP请求处理函数。它定义了一个匿名结构体类型的变量,用结构体的每个成员表示HTTP请求的参数。其中结构体成员标签指明了对于请求参数的名字,为了减少URL的长度这些参数名通常都是神秘的缩略词。Unpack将请求参数填充到合适的结构体成员中,这样我们可以方便地通过合适的类型类来访问这些参数。
|
||||
|
||||
<u><i>gopl.io/ch12/search</i></u>
|
||||
```Go
|
||||
@@ -28,9 +28,9 @@ func search(resp http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
下面的Unpack函數主要完成三件事情。第一,它調用req.ParseForm()來解析HTTP請求。然後,req.Form將包含所有的請求參數,不管HTTP客戶端使用的是GET還是POST請求方法。
|
||||
下面的Unpack函数主要完成三件事情。第一,它调用req.ParseForm()来解析HTTP请求。然后,req.Form将包含所有的请求参数,不管HTTP客户端使用的是GET还是POST请求方法。
|
||||
|
||||
下一步,Unpack函數將構建每個結構體成員有效參數名字到成員變量的映射。如果結構體成員有成員標籤的話,有效參數名字可能和實際的成員名字不相同。reflect.Type的Field方法將返迴一個reflect.StructField,里面含有每個成員的名字、類型和可選的成員標籤等信息。其中成員標籤信息對應reflect.StructTag類型的字符串,併且提供了Get方法用於解析和根據特定key提取的子串,例如這里的http:"..."形式的子串。
|
||||
下一步,Unpack函数将构建每个结构体成员有效参数名字到成员变量的映射。如果结构体成员有成员标签的话,有效参数名字可能和实际的成员名字不相同。reflect.Type的Field方法将返回一个reflect.StructField,里面含有每个成员的名字、类型和可选的成员标签等信息。其中成员标签信息对应reflect.StructTag类型的字符串,并且提供了Get方法用于解析和根据特定key提取的子串,例如这里的http:"..."形式的子串。
|
||||
|
||||
<u><i>gopl.io/ch12/params</i></u>
|
||||
```Go
|
||||
@@ -78,9 +78,9 @@ func Unpack(req *http.Request, ptr interface{}) error {
|
||||
}
|
||||
```
|
||||
|
||||
最後,Unpack遍歷HTTP請求的name/valu參數鍵值對,併且根據更新相應的結構體成員。迴想一下,同一個名字的參數可能出現多次。如果發生這種情況,併且對應的結構體成員是一個slice,那麽就將所有的參數添加到slice中。其它情況,對應的成員值將被覆蓋,隻有最後一次出現的參數值才是起作用的。
|
||||
最后,Unpack遍历HTTP请求的name/valu参数键值对,并且根据更新相应的结构体成员。回想一下,同一个名字的参数可能出现多次。如果发生这种情况,并且对应的结构体成员是一个slice,那么就将所有的参数添加到slice中。其它情况,对应的成员值将被覆盖,只有最后一次出现的参数值才是起作用的。
|
||||
|
||||
populate函數小心用請求的字符串類型參數值來填充單一的成員v(或者是slice類型成員中的單一的元素)。目前,它僅支持字符串、有符號整數和布爾型。其中其它的類型將留做練習任務。
|
||||
populate函数小心用请求的字符串类型参数值来填充单一的成员v(或者是slice类型成员中的单一的元素)。目前,它仅支持字符串、有符号整数和布尔型。其中其它的类型将留做练习任务。
|
||||
|
||||
```Go
|
||||
func populate(v reflect.Value, value string) error {
|
||||
@@ -109,7 +109,7 @@ func populate(v reflect.Value, value string) error {
|
||||
}
|
||||
```
|
||||
|
||||
如果我們上上面的處理程序添加到一個web服務器,則可以産生以下的會話:
|
||||
如果我们上上面的处理程序添加到一个web服务器,则可以产生以下的会话:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch12/search
|
||||
@@ -128,8 +128,8 @@ $ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
|
||||
max: strconv.ParseInt: parsing "lots": invalid syntax
|
||||
```
|
||||
|
||||
**練習 12.11:** 編寫相應的Pack函數,給定一個結構體值,Pack函數將返迴合併了所有結構體成員和值的URL。
|
||||
**练习 12.11:** 编写相应的Pack函数,给定一个结构体值,Pack函数将返回合并了所有结构体成员和值的URL。
|
||||
|
||||
**練習 12.12:** 擴展成員標籤以表示一個請求參數的有效值規則。例如,一個字符串可以是有效的email地址或一個信用卡號碼,還有一個整數可能需要是有效的郵政編碼。脩改Unpack函數以檢査這些規則。
|
||||
**练习 12.12:** 扩展成员标签以表示一个请求参数的有效值规则。例如,一个字符串可以是有效的email地址或一个信用卡号码,还有一个整数可能需要是有效的邮政编码。修改Unpack函数以检查这些规则。
|
||||
|
||||
**練習 12.13:** 脩改S表達式的編碼器(§12.4)和解碼器(§12.6),采用和encoding/json包(§4.5)類似的方式使用成員標籤中的sexpr:"..."字串。
|
||||
**练习 12.13:** 修改S表达式的编码器(§12.4)和解码器(§12.6),采用和encoding/json包(§4.5)类似的方式使用成员标签中的sexpr:"..."字串。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 12.8. 顯示一個類型的方法集
|
||||
## 12.8. 显示一个类型的方法集
|
||||
|
||||
我們的最後一個例子是使用reflect.Type來打印任意值的類型和枚舉它的方法:
|
||||
我们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法:
|
||||
|
||||
<u><i>gopl.io/ch12/methods</i></u>
|
||||
```Go
|
||||
@@ -18,9 +18,9 @@ func Print(x interface{}) {
|
||||
}
|
||||
```
|
||||
|
||||
reflect.Type和reflect.Value都提供了一個Method方法。每次t.Method(i)調用將一個reflect.Method的實例,對應一個用於描述一個方法的名稱和類型的結構體。每次v.Method(i)方法調用都返迴一個reflect.Value以表示對應的值(§6.4),也就是一個方法是幫到它的接收者的。使用reflect.Value.Call方法(我們之類沒有演示),將可以調用一個Func類型的Value,但是這個例子中隻用到了它的類型。
|
||||
reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例,对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值(§6.4),也就是一个方法是帮到它的接收者的。使用reflect.Value.Call方法(我们之类没有演示),将可以调用一个Func类型的Value,但是这个例子中只用到了它的类型。
|
||||
|
||||
這是屬於time.Duration和`*strings.Replacer`兩個類型的方法:
|
||||
这是属于time.Duration和`*strings.Replacer`两个类型的方法:
|
||||
|
||||
```Go
|
||||
methods.Print(time.Hour)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
## 12.9. 幾點忠告
|
||||
## 12.9. 几点忠告
|
||||
|
||||
雖然反射提供的API遠多於我們講到的,我們前面的例子主要是給出了一個方向,通過反射可以實現哪些功能。反射是一個強大併富有表達力的工具,但是它應該被小心地使用,原因有三。
|
||||
虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。
|
||||
|
||||
第一個原因是,基於反射的代碼是比較脆弱的。對於每一個會導致編譯器報告類型錯誤的問題,在反射中都有與之相對應的問題,不同的是編譯器會在構建時馬上報告錯誤,而反射則是在眞正運行到的時候才會拋出panic異常,可能是寫完代碼很久之後的時候了,而且程序也可能運行了很長的時間。
|
||||
第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后的时候了,而且程序也可能运行了很长的时间。
|
||||
|
||||
以前面的readList函數(§12.6)爲例,爲了從輸入讀取字符串併填充int類型的變量而調用的reflect.Value.SetString方法可能導致panic異常。絶大多數使用反射的程序都有類似的風險,需要非常小心地檢査每個reflect.Value的對於值的類型、是否可取地址,還有是否可以被脩改等。
|
||||
以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对于值的类型、是否可取地址,还有是否可以被修改等。
|
||||
|
||||
避免這種因反射而導致的脆弱性的問題的最好方法是將所有的反射相關的使用控製在包的內部,如果可能的話避免在包的API中直接暴露reflect.Value類型,這樣可以限製一些非法輸入。如果無法做到這一點,在每個有風險的操作前指向額外的類型檢査。以標準庫中的代碼爲例,當fmt.Printf收到一個非法的操作數是,它併不會拋出panic異常,而是打印相關的錯誤信息。程序雖然還有BUG,但是會更加容易診斷。
|
||||
避免这种因反射而导致的脆弱性的问题的最好方法是将所有的反射相关的使用控制在包的内部,如果可能的话避免在包的API中直接暴露reflect.Value类型,这样可以限制一些非法输入。如果无法做到这一点,在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例,当fmt.Printf收到一个非法的操作数是,它并不会抛出panic异常,而是打印相关的错误信息。程序虽然还有BUG,但是会更加容易诊断。
|
||||
|
||||
```Go
|
||||
fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"
|
||||
```
|
||||
|
||||
反射同樣降低了程序的安全性,還影響了自動化重構和分析工具的準確性,因爲它們無法識别運行時才能確認的類型信息。
|
||||
反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。
|
||||
|
||||
避免使用反射的第二個原因是,卽使對應類型提供了相同文檔,但是反射的操作不能做靜態類型檢査,而且大量反射的代碼通常難以理解。總是需要小心翼翼地爲每個導出的類型和其它接受interface{}或reflect.Value類型參數的函數維護説明文檔。
|
||||
避免使用反射的第二个原因是,即使对应类型提供了相同文档,但是反射的操作不能做静态类型检查,而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档。
|
||||
|
||||
第三個原因,基於反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對於一個典型的項目,大部分函數的性能和程序的整體性能關繫不大,所以使用反射可能會使程序更加清晰。測試是一個特别適合使用反射的場景,因爲每個測試的數據集都很小。但是對於性能關鍵路徑的函數,最好避免使用反射。
|
||||
第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以使用反射可能会使程序更加清晰。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第十二章 反射
|
||||
|
||||
Go語音提供了一種機製在運行時更新變量和檢査它們的值、調用它們的方法和它們支持的內在操作,但是在編譯時併不知道這些變量的具體類型。這種機製被稱爲反射。反射也可以讓我們將類型本身作爲第一類的值類型處理。
|
||||
Go语音提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
|
||||
|
||||
在本章,我們將探討Go語言的反射特性,看看它可以給語言增加哪些表達力,以及在兩個至關重要的API是如何用反射機製的:一個是fmt包提供的字符串格式功能,另一個是類似encoding/json和encoding/xml提供的針對特定協議的編解碼功能。對於我們在4.6節中看到過的text/template和html/template包,它們的實現也是依賴反射技術的。然後,反射是一個複雜的內省技術,不應該隨意使用,因此,盡管上面這些包內部都是用反射技術實現的,但是它們自己的API都沒有公開反射相關的接口。
|
||||
在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何用反射机制的:一个是fmt包提供的字符串格式功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然后,反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口。
|
||||
|
||||
Reference in New Issue
Block a user