Go 博客

反射三法则

2011/09/06

引言

在计算过程中,反射是程序检查其自身结构,特别是类型的一种能力。 它是元编程的一种形式,同时也是最容易让人误解的部分。

在本文中,我们试图解释反射在Go中如何工作,以澄清某些事情。每一种语言的反射模型都不同 (甚至许多语言根本不支持反射),不过这篇文章是关于Go的,因此在接下来的内容中, “反射”一词应看做“Go中的反射”。

类型与接口

由于反射建立在类型系统之上,就让我们先来复习一下 Go 中的类型吧。。

Go是静态类型的语言。每个变量都有一种静态类型。换言之,它们都有一种已知的类型, 并且在编译时就确定下来了。比如 intfloat32*MyType[]byte 等等。如果我们定义了

type MyInt int

var i int
var j MyInt

那么 i 的类型为 int ,而 j 的类型为 MyInt 。尽管变量 ij 拥有相同的底层类型, 但它们的静态类型仍然不同,因此它们在未经转换前是无法相互赋值的。

有一类重要的类型称作接口类型,它表示一个确定的方法集。只要某个具体值 (非接口)实现了某个接口中的方法,该接口类型的变量就能存储它。一个众所周知的例子就是 io.Readerio.Writer ,即 io 包中的 ReaderWriter 类型:

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

任何实现了 Read (或 Write )方法及其签名的类型, 同时也就实现了 io.Reader (或 io.Writer )接口。 就此而言,若某个值的类型拥有 Read 方法, io.Reader 类型的变量就能保存它:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

有件事一定要明白,即无论 r 保存了什么具体的值, r 的类型总是 io.Reader :Go是静态类型的,而 r 的静态类型为 io.Reader

一个非常重要的接口类型是空接口:

interface{}

它表示空方法集。由于任何值都有零个 或多个方法,因此任何值都满足它。

有人说Go的接口是动态类型的,不过这是种误解。它们确实是静态类型的: 接口类型的变量总有着相同的静态类型,就算存储在其中的值的类型在运行时会改变, 它也总是满足该接口。

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

Russ Cox 写过一篇关于 Go 中接口值的表示的详细的博文。 这里没必要再重复了,只给出简单的摘要。

接口的表示

Russ Cox 写过一篇题为 Go中接口值的表示 的文章,我们就不在此赘述了,这里仅简要概括一下。

接口类型的变量存储了一对内容:即赋予该变量的具体值,以及该值的类型描述符。 更准确地说,接口的值是实现了该接口的底层具体数据条目,而类型则描述了该条目的完整类型。 例如,在执行完

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

之后,`r` 包含的 (值, 类型) 对可以形式化地描述为(`tty`,`*os.File`) 注意,类型 *os.File 还实现了除 Read 以外的其它方法:尽管该接口值只提供了访问 Read 方法的能力,但其内部却携带了有关该值的所有类型信息。 这就是我们可以写出这种代码的原因:

var w io.Writer
w = r.(io.Writer)

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

接着,我们可以这样做:

var empty interface{}
empty = w

而我们的空接口值 e 也将再次包含同样的一对 ( tty, *os.File )。 这很方便:空接口可保存任何值,同时包含关于该值的所有信息。

(在这里我们无需类型断言,因为 w 肯定是满足空接口的。在本例中, 我们将一个值从 Reader 变成了 Writer ,由于 Writer 的方法集并非 Reader 方法集的子集,因此我们必须显式地使用类型断言。)

一个很重要的细节,就是接口内部的对总是 (值, 具体类型) 的形式,而不会是 (值, 接口类型) 的形式。接口不能保存接口值。

现在我们准备好聊聊“反射”了。

反射法则之一

1. 从接口值可反射出反射对象。

从基本层面上看,反射只是一种检查存储在接口变量中的“类型-值”对的机制。 首先,我们需要了解 reflect 包中的两种类型: TypeValue,这两种类型可用来访问接口变量的内容。 还有两个简单的函数,叫做 reflect.TypeOfreflect.ValueOf , 它们用来从接口值中分别获取 reflect.Typereflect.Value 。 (同样,从 reflect.Value 也能很容易地获取 reflect.Type , 不过让我们先保持 ValueType 概念的独立性吧。

我们先从 TypeOf 开始:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

此程序会打印出

type: float64

你可能会问接口在哪,因为该程序看起来只是向 reflect.TypeOf 传递了一个 float64 类型的变量 x ,而不是一个接口值。但它就在那, 正如 godoc 所显示的那样reflect.TypeOf 的签名包含了一个空接口:

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

当我们调用 reflect.TypeOf(x) 时, x 首先会被存储在一个空接口中, 然后它会作为实参被传入; reflect.TypeOf 通过解包该空接口来还原其类型信息。

当然, reflect.ValueOf 函数也会还原它的值(从这里开始, 我们会略过那些代码框架,而只关注于可执行的部分):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

会打印出

value: <float64 Value>

The String method does not.)

(我们显式地调用了 String 方法,因为默认情况下 fmt 包会动用 reflect.Value 来显示其内部具体的值。 而 String 方法则不会如此。)

reflect.Typereflect.Value 都有许多方法来让我们检测并操作它们。 一个重要的例子就是 Value 拥有一个 Type 方法,它会返回 reflect.ValueType 。另外就是 TypeValue 都有一个 Kind 方法,它会返回一个常量来表明条目的类型: Uint 、 Float64Slice 等等。同样, Value 拥有像 IntFloat 这样的方法来让我们获取存储在内部的值 (作为 int64float64 ):

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

会打印出

type: float64
kind is float64: true
value: 3.4

同样还有 SetIntSetFloat 这样的方法,不过在使用它们之前, 我们需要理解其可设置性,该主题会在后面的第三条反射法则中讨论。

反射库有几点特性值得一提。首先,为了让 API 保持简单, Valuegettersetter 方法会在能够保存其值的最大类型上进行操作:例如 int64 就能用于所有的带符号整数。也就是说, ValueInt 方法会返回 int64 类型的值,而 SetInt 会接受 int64 类型的值;因此该值可能需要转换为它所涉及到的实际类型:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns a uint64.

第二个特性就是反射对象的 Kind 描述了其基本类型,而非静态类型。 若反射对象包含了用户定义的整数类型的值,比如

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

那么 vKind 仍为 reflect.Int ,尽管 x 的静态类型为 MyInt 而非 int 。换句话说, Kind 无法区分 intMyInt ,而 Type 则可以。

反射法则之二

2. 从反射对象可反射出接口值。

如同物理中的反射现象那样,Go中的反射也会产生它自己的镜像。

给定一个 reflect.Value ,我们可以使用 Interface 方法还原其接口值;在效果上,该方法会将类型与值的信息打包成接口表示,并返回其结果:

// Interface 将 v 的值返回成 interface{}。
func (v Value) Interface() interface{}

因此,我们可以通过

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

打印出反射对象 v 所表示的 float64 值。

不过我们可以做得更好。 fmt.Printlnfmt.Printf 等都会将实参作为空接口值传递,它们会被包 fmt 进行内部解包, 就像我们刚做的那样。因此,正确地打印出 reflect.Value 内容的方法就是 将 Interface 方法的结果传至格式化打印功能:

fmt.Println(v.Interface())

(为什么不是 fmt.Println(v) ?因为 v 是个 reflect.Value ,而我们想要的是它保存的具体值。)由于值的类型是 float64 ,如果需要的话,我们甚至可以使用浮点数格式化:

fmt.Printf("value is %7.1e\n", v.Interface())

然后就会得到

3.4e+00

再次强调,这里无需将 v.Interface() 的结果类型断言为 float64 , 因为空接口值中拥有具体值的类型信息,而 Printf 则会将它还原。

简单来说, Interface 方法就是 ValueOf 函数的“反函数”, 不过其结果总是静态类型 interface{}

重申一遍:从接口值可反射出反射对象,反之亦可。

反射法则之三

第三条法则是最微妙而令人困惑的,但如果我们从第一条法则开始,还是很容易理解的。

这些代码虽然不能工作,但很值得学习。

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

如果你运行这段代码,它就会报出神秘的恐慌信息:

panic: reflect.Value.SetFloat using unaddressable value

其问题的根源不在于值 7.1 能不能寻址,而在于 v 不可设置。 可设置性是反射值 Value 的一种属性,而且并不是所有的反射值都拥有它。

ValueCanSet 方法会报告 Value 的可设置性。 在我们的例子中,

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

会打印出

settability of v: false

对不可设置的 Value 调用 Set 方法会产生错误,但什么是可设置性呢?

可设置性有点像可寻址性,不过它更加严格。它是反射对象能否修改其创建之初的实际值的一种属性。 可设置性决定了反射对象能否保存原始条目。当我们执行完

var x float64 = 3.4
v := reflect.ValueOf(x)

之后,就将 x 的一份副本传入了 reflect.ValueOf , 因此该接口值也就作为传递给 reflect.ValueOf 的实参创建了一份 x 的副本,而非 x 本身。因此,假如语句

v.SetFloat(7.1)

能够成功执行,它也无法更新 x ,即便 v 看起来创建自 x 。就算它能够更新存储在该反射值中的 x 的副本, x 本身也不会受影响。这是令人困惑且毫无用处的,因此它是非法的。 而可设置性就是用于避免此类问题的属性。

这看起来很奇怪,事实却并非如此。它其实就是隐藏在奇特外表下的一种常见情况。 考虑将 x 传递给一个函数:

f(x)

我们并不期望 f 能修改 x ,因为我们传入的是值 x 的副本,而非 x 本身。如果我们想让 f 直接修改 x ,就必须向该函数传入 x 的地址(即指向 x 的指针):

f(&x)

这即熟悉又直白,反射也是以相同的方式工作的。如果我们想要通过反射修改 x ,就必须向反射库提供要修改的值的指针。

让我们试试吧。首先像平时那样初始化 x ,接着创建指向它的反射值,叫做 p

var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:获取 x 的地址
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

目前会输出

type of p: *float64
settability of p: false

反射对象 p 并不是可设置的,不过我们也不想设置 p , 而(效果上)是 *p 。为获得 p 指向的内容,我们调用 ValueElem 方法,它会间接通过指针,并将结构保存到叫做 v 的反射值 Value 中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

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

settability of v: true

由于它代表 x ,因此最终我们可使用 v.SetFloat 来修改 x 的值:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

并得到期望的输出:

7.1
7.1

反射可能很难理解,但语言做了它应该做的,尽管反射类型 Type 和值 Value 隐藏了发生的事情。你只要记住反射值需要某些东西的地址来修改它所代表的东西即可。

结构体

在我们前面的例子中, v 本身并不是指针,它只是从一个指针中获取的。 在使用反射修改结构体的字段时,这种情况经常出现。即,当我们有结构体的地址时, 就能修改它的字段。

下面这个简单的例子分析了结构体类型的值 t 。我们从它的地址创建了反射对象, 因为待会儿要修改它。接着我们将 typeOfT 设置为它的类型, 然后以直白的方法遍历其字段(详见 reflect 包)。 注意,我们从该结构体类型中提取了其字段名,但字段本身是一般的 reflect.Value 对象。

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

此程序的输出为:

0: A int = 23
1: B string = skidoo

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

由于 s 包含了可设置的反射对象,因此我们可以修改该结构体的字段:

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

其结果为:

t is now {77 Sunset Strip}

若我们修改此程序使 s 创建自 t 而非 &t ,那么调用 SetIntSetString 就会失败,因为 t 的字段不可设置。

总结

再次提示,反射法则如下:

  • 从接口值可反射出反射对象。
  • 从反射对象可反射出接口值。
  • 要修改反射对象,其值必须可设置。

一旦你理解了Go中的这些反射法则,它就会变得更容易使用了,尽管它还是很微妙。 这是个强大的工具,因此除非必要,否则应当避免或小心使用。

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

Rob Pike 编写

相关文章