mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-17 11:14:20 +08:00
fix typo and optimize.
Change-Id: I7b6938936231fd722814984678ffa30402539fd9
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
## 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 对应的引用类型; interface 类型; 还有表示空值的 Invalid 类型. (空的 reflect.Value 的 kind 即为 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,6 +1,6 @@
|
||||
## 12.3. Display递归打印
|
||||
## 12.3. Display,一个递归的值打印器
|
||||
|
||||
接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是像构建一个用于调式用的Display函数,给定一个聚合类型x,打印这个值对应的完整的结构,同时记录每个发现的每个元素的路径。让我们从一个例子开始。
|
||||
接下来,让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数,我们只是构建一个用于调试用的Display函数:给定任意一个复杂类型 x,打印这个值对应的完整结构,同时标记每个元素的发现路径。让我们从一个例子开始。
|
||||
|
||||
```Go
|
||||
e, _ := eval.Parse("sqrt(A / pi)")
|
||||
@@ -20,7 +20,7 @@ e.args[0].value.y.type = eval.Var
|
||||
e.args[0].value.y.value = "pi"
|
||||
```
|
||||
|
||||
在可能的情况下,你应该避免在一个包中暴露和反射相关的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:
|
||||
你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作,导出的是Display函数,它只是display函数简单的包装以接受interface{}类型的参数:
|
||||
|
||||
<u><i>gopl.io/ch12/display</i></u>
|
||||
```Go
|
||||
@@ -30,7 +30,7 @@ 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包来简化我们的例子实现。
|
||||
|
||||
@@ -74,15 +74,15 @@ 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的一个key。和往常一样,遍历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()来获取接口对应的动态值,并且打印对应的类型和值。
|
||||
|
||||
@@ -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):
|
||||
@@ -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总是返回一个具体类型的 Value,因为它是从一个接口值提取的内容。
|
||||
|
||||
在第二个例子中,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
|
||||
@@ -216,10 +216,11 @@ c.Value = 42
|
||||
...ad infinitum...
|
||||
```
|
||||
|
||||
许多Go语言程序都包含了一些循环的数据结果。Display支持这类带环的数据结构是比较棘手的,需要增加一个额外的记录访问的路径;代价是昂贵的。一般的解决方案是采用不安全的语言特性,我们将在13.3节看到具体的解决方案。
|
||||
许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧,需要额外记录迄今访问的路径;相应会带来成本。通用的解决方案是采用 unsafe 的语言特性,我们将在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节,我们会看到另一种探测数据结构是否存在环的技术。)
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
## 12.4. 示例: 编码S表达式
|
||||
## 12.4. 示例: 编码为S表达式
|
||||
|
||||
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
|
||||
"hello" string (with Go-style quotation)
|
||||
foo symbol (an unquoted name)
|
||||
(1 2 3) list (zero or more items enclosed in parentheses)
|
||||
"hello" string (带有Go风格的引号)
|
||||
foo symbol (未用引号括起来的名字)
|
||||
(1 2 3) list (括号包起来的0个或多个元素)
|
||||
```
|
||||
|
||||
布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还浮点数、复数和interface。支持它们是练习12.3的任务。
|
||||
布尔型习惯上使用t符号表示true,空列表或nil符号表示false,但是为了简单起见,我们暂时忽略布尔类型。同时忽略的还有chan管道和函数,因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。
|
||||
|
||||
我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以自然的方式编码。Nil值编码为nil符号。数组和slice被编码为一个列表。
|
||||
我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。
|
||||
|
||||
结构体被编码为成员对象的列表,每个成员对象对应一个个仅有两个元素的子列表,其中子列表的第一个元素是成员的名字,子列表的第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。
|
||||
结构体被编码为成员对象的列表,每个成员对象对应一个有两个元素的子列表,子列表的第一个元素是成员的名字,第二个元素是成员的值。Map被编码为键值对的列表。传统上,S表达式使用点状符号列表(key . value)结构来表示key/value对,而不是用一个含双元素的列表,不过为了简单我们忽略了点状符号列表。
|
||||
|
||||
编码是由一个encode递归函数完成,如下所示。它的结构本质上和前面的Display函数类似:
|
||||
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
@@ -139,7 +139,7 @@ omin.)" "Best Picture (Nomin.)")) (Sequel nil))
|
||||
|
||||
和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方法对于不同的类型可能返回相同的结果。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 12.5. 通过reflect.Value修改值
|
||||
|
||||
到目前为止,反射还只是程序中变量的另一种访问方式。然而,在本节中我们将重点讨论如果通过反射机制来修改变量。
|
||||
到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量。
|
||||
|
||||
回想一下,Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量,但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
|
||||
|
||||
@@ -14,7 +14,7 @@ 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方法来判断其是否可以被取地址:
|
||||
|
||||
@@ -27,7 +27,7 @@ fmt.Println(d.CanAddr()) // "true"
|
||||
|
||||
每当我们通过指针间接地获取的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
|
||||
@@ -44,13 +44,13 @@ 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
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
虽然反射提供的API远多于我们讲到的,我们前面的例子主要是给出了一个方向,通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具,但是它应该被小心地使用,原因有三。
|
||||
|
||||
第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后的时候了,而且程序也可能运行了很长的时间。
|
||||
第一个原因是,基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题,在反射中都有与之相对应的误用问题,不同的是编译器会在构建时马上报告错误,而反射则是在真正运行到的时候才会抛出panic异常,可能是写完代码很久之后了,而且程序也可能运行了很长的时间。
|
||||
|
||||
以前面的readList函数(§12.6)为例,为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险,需要非常小心地检查每个reflect.Value的对于值的类型、是否可取地址,还有是否可以被修改等。
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 第十二章 反射
|
||||
|
||||
Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
|
||||
Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
|
||||
|
||||
在本章,我们将探讨Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何用反射机制的:一个是fmt包提供的字符串格式功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包,它们的实现也是依赖反射技术的。然后,反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些包内部都是用反射技术实现的,但是它们自己的API都没有公开反射相关的接口。
|
||||
|
||||
Reference in New Issue
Block a user