[翻译]反射的规则

第一次知道反射的时候还是许多年前在学校里玩 C# 的时候。那时总是弄不清楚这个复杂的玩意能有什么实际用途……然后发现 Java 有这个,后来发现 PHP 也有了,再后来 Objective-C、Python 什么的也都有……甚至连 Delphi 也有 TRttiContext……反射无处不在!!!

Go 作为一个集大成的现代系统级语言,当然也需要有,必须的!

大牛 Rob Pike 的这篇文章相对全面的介绍了 Go 语言中的反射的机制已经使用。觉得值得研读,于是翻译于此。

———-翻译分割线———-

反射的规则

在运行时反射是程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。它同时也是造成混淆的重要来源。

在这篇文章中将试图明确解释在 Go 中的反射是如何工作的。每个语言的反射模型都不同(同时许多语言根本不支持反射)。不过这篇文章是关于 Go 的,因此接下来的内容“反射”这一词表示“在 Go 中的反射”。

类型和接口

由于反射构建于类型系统之上,就从复习一下 Go 中的类型开始吧。

Go 是静态类型的。每一个变量有一个静态的类型,也就是说,有一个已知类型并且在编译时就确定下来了:int,float32,*MyType,[]byte 等等。如果定义

<br />
type MyInt int<br />
var i int<br />
var j MyInt<br />

那么 i 的类型为 int 而 j 的类型为 MyInt。即使变量 i 和 j 有相同的底层类型,它们仍然是有不同的静态类型的。未经转换是不能相互赋值的。

在类型中有一个重要的类别就是接口类型,表达了固定的一个方法集合。一个接口变量可以存储任意实际值(非接口),只要这个值直线了接口的方法。众所周知的一个例子就是 is io.Reader 和 io.Writer,来自 io 包的类型 Reader 和 Writer:

<br />
// Reader 是包裹了基础的 Read 方法的接口。.<br />
type Reader interface {<br />
    Read(p []byte) (n int, err os.Error)<br />
}</p>
<p>// Writer 是包裹了基础 Write 方法的接口。<br />
type Writer interface {<br />
    Write(p []byte) (n int, err os.Error)<br />
}<br />

任何用这个声明实现了 Read(或 Write)方法的类型,可以说它实现了 io.Reader(或 io.Writer)。基于本讨论来说,这意味着 io.Reader 类型的变量可以保存任意值,只要这个值的类型实现了 Read 方法:

<br />
var r io.Reader<br />
r = os.Stdin<br />
r = bufio.NewReader(r)<br />
r = new(bytes.Buffer)<br />
// 等等<br />

有一个事情是一定要明确的,不论 r 保存了什么值,r 的类型总是 io.Reader:Go 是静态类型,而 r 的静态类型是 io.Reader。

接口类型的一个极端重要的例子是空接口:

<br />
interface{}<br />

它表示空的方法集合,由于任何值都有另个或者多个方法,所以任何值都可以满足它。

也有人说 Go 的接口是动态类型的,不过这是一种误解。它们是静态类型的:接口类型的变量总是有着相同的静态类型,这个值总是满足空接口,只是存储在接口变量中的值运行时也有可能被改变类型。

对于所有这些都必须严谨的对待,因为反射和接口密切相关。

接口的特色

Russ Cox 已经写了一篇详细介绍 Go 中接口值的特点的博文。所以无需在这里重复整个故事了,不过简单的总结还是必要的。

接口类型的变量存储了两个内容:赋值给变量实际的值和这个值的类型描述。更准确的说,值是底层实现了接口的实际数据项目,而类型描述了这个项目完整的类型。例如下面,

<br />
var r io.Reader<br />
tty, err = os.OpenFile(&quot;/dev/tty&quot;, os.O_RDWR, 0)<br />
if err != nil { return nil, err }<br />
r = tty<br />

用模式的形式来表达 r 包含了的是 (value, type) 对,如 (tty, *os.File)。注意类型 *os.File 除了 Read 方法还实现了其他方法:尽管接口值仅仅提供了访问 Read 方法的可能,但是内部包含了这个值的完整的类型信息。这也就是为什么可以这样做:

<br />
var w io.Writer<br />
w = r.(io.Writer)<br />

在这个赋值中的断言是一个类型断言:它断言了 r 内部的条目同时也实现了 io.Writer,因此可以赋值它到 w。在赋值之后,w 将会包含 (tty, *os.File)。跟在 r 中保存的一致。接口的静态类型决定了哪个方法可以通过接口变量调用,即便内部实际的值可能有一个更大的方法集。

接下来,可以这样做:

<br />
var empty interface{}<br />
empty = w<br />

而空接口值 e 也将包含同样的 (tty, *os.File)。这很方便:空接口可以保存任何值同时保留关于那个值的所有信息。

(这里无需类型断言,因为 w 是肯定满足空接口的。在这个例子中,将一个值从 Reader 变为 Writer,由于 Writer 的方法不是 Reader 的子集,所以就必须明确使用类型断言。)

一个很重要的细节是接口内部的对总是 (value, 实际类型) 的格式,而不会有 (value, 接口类型) 的格式。接口不能保存接口值。

现在准备好来反射了。

反射的第一条规则

1. 从接口值到反射对象的反射。

在基本的层面上,反射只是一个检查存储在接口变量中的类型和值的算法。从头来讲,在 reflect 包中有两个类型需要了解:TypeValue。这两个类型使得可以访问接口变量的内容,还有两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,从接口值中分别获取 reflect.Type 和 reflect.Value。(同样,从 reflect.Value 也很容易能够获得 reflect.Type,不过这里让 Value 和 Type 在概念上分离了。)

从 TypeOf 开始:

<br />
package main</p>
<p>import (<br />
        &quot;fmt&quot;<br />
        &quot;reflect&quot;<br />
)</p>
<p>func main() {<br />
        var x float64 = 3.4<br />
        fmt.Println(&quot;type:&quot;, reflect.TypeOf(x))<br />
}<br />

这个程序打印

<br />
type: float64<br />

接口在哪里呢,读者可能会对此有疑虑,看起来程序传递了一个 float64 类型的变量 x,而不是一个接口值,到 reflect.TypeOf。但是,它确实就在那里:如同 godoc 报告的那样,reflect.TypeOf 的声明包含了空接口:

<br />
// TypeOf 返回 interface{} 中的值反射的类型。<br />
func TypeOf(i interface{}) Type<br />

当调用 reflect.TypeOf(x) 的时候,x 首先存储于一个作为参数传递的空接口中;reflect.TypeOf 解包这个空接口来还原类型信息。

reflect.ValueOf 函数,当然就是还原那个值(从这里开始将会略过那些概念示例,而聚焦于可执行的代码):

<br />
var x float64 = 3.4<br />
fmt.Println(&quot;value:&quot;, reflect.ValueOf(x))<br />

打印

<br />
value: &lt;float64 Value&gt;<br />

reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样):

<br />
var x float64 = 3.4<br />
v := reflect.ValueOf(x)<br />
fmt.Println(&quot;type:&quot;, v.Type())<br />
fmt.Println(&quot;kind is float64:&quot;, v.Kind() == reflect.Float64)<br />
fmt.Println(&quot;value:&quot;, v.Float())<br />

打印

<br />
type: float64<br />
kind is float64: true<br />
value: 3.4<br />

同时也有类似 SetInt 和 SetFloat 的方法,不过在使用它们之前需要理解可设置性,这部分的主题在下面的第三条军规中讨论。

反射库有着若干特性值得特别说明。首先,为了保持 API 的简洁,“获取者”和“设置者”用 Value 的最宽泛的类型来处理值:例如,int64 可用于所有带符号整数。也就是说 Value 的 Int 方法返回一个 int64,而 SetInt 值接受一个 int64;所以可能必须转换到实际的类型:

<br />
var x uint8 = 'x'<br />
v := reflect.ValueOf(x)<br />
fmt.Println(&quot;type:&quot;, v.Type()) // uint8.<br />
fmt.Println(&quot;kind is uint8: &quot;, v.Kind() == reflect.Uint8) // true.<br />
x = uint8(v.Uint()) // v.Uint 返回一个 uint64.<br />

第二个特性是反射对象的 Kind 描述了底层类型,而不是静态类型。如果一个反射对象包含了用户定义的整数类型的值,就像

<br />
type MyInt int<br />
var x MyInt = 7<br />
v := reflect.ValueOf(x)<br />

v 的 Kind 仍然是 reflect.Int,尽管 x 的静态类型是 MyInt,而不是 int。换句话说,Kind 无法从 MyInt 中区分 int,而 Type 可以。

反射的第二条规则

2. 从反射对象到接口值的反射。

如同物理中的反射,在 Go 中的反射也存在它自己的镜像。

从 reflect.Value 可以使用 Interface 方法还原接口值;方法高效的打包类型和值信息到接口表达中,并返回这个结果:

<br />
// Interface 以 interface{} 返回 v 的值。<br />
func (v Value) Interface() interface{}<br />

可以这样作为结果

<br />
y := v.Interface().(float64) // y 将为类型 float64。<br />
fmt.Println(y)<br />

通过反射对象 v 可以打印 float64 的表达值。

然而,还可以做得更好。fmt.Println,fmt.Printf 和其他所有传递一个空接口值作为参数的,由 fmt 包在内部解包的方式就像之前的例子这样。因此正确的打印 reflect.Value 的内容的方法就是将 Interface 方法的结果传递给格式化打印:formatted print routine:

<br />
fmt.Println(v.Interface())<br />

(为什么不是 fmt.Println(v)?因为 v 是一个 reflect.Value;这里希望是它保存的实际的值。)由于值是 float64,如果需要的话,甚至可以使用浮点格式化:

<br />
fmt.Printf(&quot;value is %7.1e\n&quot;, v.Interface())<br />

然后就得到这个

<br />
3.4e+00<br />

再次强调,对于 v.Interface() 无需类型断言其为 float64;空接口值在内部有实际值的类型信息,而 Printf 会发现它。

简单来说,Interface 方法是 ValueOf 函数的镜像,除了返回值总是静态类型 interface{}。

回顾:反射可以从接口值到反射对象,也可以反过来。

反射的第三条规则

3. 为了修改反射对象,其值必须可设置。

第三条军规是最为精细和迷惑的,但是如果从第一个规则开始,还是足以让人明白的。

这里有一些不能工作的代码,值得学习。

<br />
var x float64 = 3.4<br />
v := reflect.ValueOf(x)<br />
v.SetFloat(7.1) // Error: will panic.<br />

如果运行这个代码,它报出神秘的 panic 消息

<br />
panic: reflect.Value.SetFloat using unaddressable value<br />

问题不在于值 7.1 不能地址化;在于 v 不可设置。设置性是反射值的一个属性,并不是所有的反射值有它。

值的 CanSet 方法提供了值的设置性;在这个例子中,

<br />
var x float64 = 3.4<br />
v := reflect.ValueOf(x)<br />
fmt.Println(&quot;settability of v:&quot; , v.CanSet())<br />

打印

<br />
settability of v: false<br />

对不可设置值调用 Set 方法会有错误。但是什么是设置性?

设置性有一点点像地址化,但是更严格。这是用于创建反射对象的时候,能够修改实际存储的属性。设置性用于决定反射对象是否保存原始项目。当这样

<br />
var x float64 = 3.4<br />
v := reflect.ValueOf(x)<br />

就传递了一个 x 的副本到 reflect.ValueOf,所以接口值作为 reflect.ValueOf 参数创建了 x 的副本,而不是 x 本身。因此,如果语句

<br />
v.SetFloat(7.1)<br />

允许执行,虽然 v 看起来是从 x 创建的,它也无法更新 x。反之,如果在反射值内部允许更新 x 的副本,那么 x 本身不会收到影响。这会造成混淆,并且毫无意义,因此这是非法的,而设置性是用于解决这个问题的属性。

这很神奇?其实不是。这实际上是一个常见的非同寻常的情况。考虑传递 x 到函数:

<br />
f(x)<br />

由于传递的是 x 的值的副本,而不是 x 本身,所以并不期望 f 可以修改 x。如果想要 f 直接修改 x,必须向函数传递 x 的地址(也就是,指向 x 的指针):

<br />
f(&amp;x)<br />

这是清晰且熟悉的,而反射通过同样的途径工作。如果希望通过反射来修改 x,必须向反射库提供一个希望修改的值的指针。

来试试吧。首先像平常那样初始化 x,然后创建指向它的反射值,叫做 p。

<br />
var x float64 = 3.4<br />
p := reflect.ValueOf(&amp;x) // 注意:获取 X 的地址。<br />
fmt.Println(&quot;type of p:&quot;, p.Type())<br />
fmt.Println(&quot;settability of p:&quot; , p.CanSet())<br />

这样输出为

<br />
type of p: *float64<br />
settability of p: false<br />

反射对象 p 并不是可设置的,但是并不希望设置 p,(实际上)是 *p。为了获得 p 指向的内容,调用值上的 Elem 方法,从指针间接指向,然后保存反射值的结果叫做 v:

<br />
v := p.Elem()<br />
fmt.Println(&quot;settability of v:&quot; , v.CanSet())<br />

现在 v 是可设置的反射对象,如同示例的输出,

<br />
settability of v: true<br />

而由于它来自 x,最终可以使用 v.SetFloat 来修改 x 的值:

<br />
v.SetFloat(7.1)<br />
fmt.Println(v.Interface())<br />
fmt.Println(x)<br />

得到期望的输出

<br />
7.1<br />
7.1<br />

反射可能很难理解,但是语言做了它应该做的,尽管底层的实现被反射的 Type 和 Value 隐藏了。务必记得反射值需要某些内容的地址来修改它指向的东西。

结构体

在之前的例子中 v 本身不是指针,它只是从一个指针中获取的。这种情况更加常见的是当使用反射修改结构体的字段的时候。也就是当有结构体的地址的时候,可以修改它的字段。

这里有一个分析结构值 t 的简单例子。由于希望等下对结构体进行修改,所以从它的地址创建了反射对象。设置了 typeOfT 为其类型,然后用直白的方法调用来遍历其字段(参考 reflect 包了解更多信息)。注意从结构类型中解析了字段名字,但是字段本身是原始的 reflect.Value 对象。

<br />
type T struct {<br />
    A int<br />
    B string<br />
}<br />
t := T{23, &quot;skidoo&quot;}<br />
s := reflect.ValueOf(&amp;t).Elem()<br />
typeOfT := s.Type()<br />
for i := 0; i &lt; s.NumField(); i++ {<br />
    f := s.Field(i)<br />
    fmt.Printf(&quot;%d: %s %s = %v\n&quot;, i,<br />
        typeOfT.Field(i).Name, f.Type(), f.Interface())<br />
}<br />

这个程序的输出是

<br />
0: A int = 23<br />
1: B string = skidoo<br />

这里还有一个关于设置性的要点:T 的字段名要大写(可导出),因为只有可导出的字段是可设置的。

由于 s 包含可设置的反射对象,所以可以修改结构体的字段。

<br />
s.Field(0).SetInt(77)<br />
s.Field(1).SetString(&quot;Sunset Strip&quot;)<br />
fmt.Println(&quot;t is now&quot;, t)<br />

这里是结果:

<br />
t is now {77 Sunset Strip}<br />

如果修改程序使得 s 创建于 t,而不是 &t,调用 SetInt 和 SetString 会失败,因为 t 的字段不可设置。

总结

再次提示,反射的规则如下:

  • 从接口值到反射对象的反射。
  • 从反射对象到接口值的反射。
  • 为了修改反射对象,其值必须可设置。

一旦理解了 Go 中的反射的这些规则,就会变得容易使用了,虽然它仍然很微妙。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。

还有大量的关于反射的内容没有涉及到——channel 上的发送和接收、分配内存、使用 slice 和 map、调用方法和函数——但是这篇文章已经够长了。这些话题将会在以后的文章中逐一讲解。

Rob Pike 撰写,2011年9月

13 thoughts on “[翻译]反射的规则”

  1. var x float64 = 3.4
    fmt.Println(“value:”, reflect.ValueOf(x))
    ———————————————–
    value:
    ———————————————–
    这个Value反射出来的值为什么打印的不是具体的值?

  2. y := v.Interface().(float64) // y 将为类型 float64。
    fmt.Println(y)
    ————————————————————
    第一行不加.(float64) 也可以打印,加是因为?

  3. Value 的 Interface() 方法的说明如下:

    > Interface returns v’s current value as an interface{}.
    http://golang.org/pkg/reflect/#Value.Interface

    当前值是一个 float64 的话,显然会打印正确的数值。

    我觉得,你得把你遇到的问题拆分为以下两个问题:

    1. 反射的原理
    2. fmt.Print 相关函数的输出

    分别弄清除这两个问题,基本上就可以理解为什么一个 interface{} 的 float64 的值可以正确的被 fmt.Print 输出。

  4. 如果知道一个struct的名称:
    MainController,如果得到这个struct的实例?
    输入:
    字符串 “MainController”
    输出:
    &MainController{}

Leave a Reply

Your email address will not be published. Required fields are marked *