原文链接: The Laws of Reflection

原文日期: 6 September 2011

Introduction

在计算机领域,反射是一种检查自身结构特别是类型的能力,是元编程的一种形式。也是令人疑惑的主要来源。

在这篇文章中,我们将尝试的解释反射如何在 Go 中工作的。每种语言的反射模型是不一样的(许多语言还不支持反射),但是本文是关于 Go 的,在文章剩下的部分,“反射” 其实是说 “Go 中的反射”。

Types and interfaces

因为反射是基于类型系统(type system),让我们一起复习一下 Go 中的类型。

Go 是静态类型。每个变量都有一个 static type,也就是说编译时已知并确定了一种类型:int,float32,*MyType,[]byte 等。如果我们进行下面的声明

type MyInt int

var i int
var j MyInt

那么 i 是一个 int 类型,j 是 MyInt 类型,变量 i 和 j 具有不同的 static types,虽然它们具有相同的 underlying type,但如果没有转换,它们就不能相互分配。

一个重要的类型是接口类型,它表示固定的方法集合。接口变量可以存储任何值(非接口)只要值实现了接口的方法集合。一个著名的例子是 io.Readerio.Writer ,来自 io包 的 Reader 和 Writer 类型:

// 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)
}

任何使用此 signature 实现 Read(或 Write)方法的类型都可以实现io.Reader(或io.Writer)。对于这次讨论的目的,这意味着 io.Reader 类型的变量可以是任何类型具有 Read 方法的值。

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

重要的是要明确无论具体值 r 是什么,r 的类型总是 io.Reader:Go 是静态类型的,r 的 static type 是 io.Reader。

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

interface{}

它表示空方法集,并且完全满足任何值,因为任何值都有零个或多个方法。

有些人说 Go 的接口是动态类型,这是一个误导。它们是静态类型:接口类型的变量始终具有相同的 static type,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口。

我们需要准确地了解所有这些,因为反射和界面密切相关。

The representation of an interface

Russ Cox 写过一篇详细的文章具体介绍 Go 中是如何保存接口的值。没有必要在这里重复那篇文章的全部内容,但是简单的总结还是很有必要的。

接口变量存储了一对:赋给变量的具体值(concrete value assigned to the variable)和那个 value 的类型描述(value’s type descriptor)。更准确的说,value 是实现接口的基础具体数据项,type 该项的完整类型,例如:

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

r 包含了(value,type)(tty,*os.File),注意类型 *os.File 不仅仅实现了 Read 方法,即使这个接口的 value 仅提供对 Read 的访问,内部值也包含该值的所有类型信息。这就是为什么我们可以做:

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

上面的代码是类型断言,它断言的内容是 r 中的项目也实现了 io.Writer,所以我们断言它为 w。通过断言,w 将会包含 (tty,*os.FIle)。和 r 中所持是一样的。接口的 static type 确定可以使用接口变量调用哪些方法,即使内部的具体值可能具有更大的方法集。(*The static type of the interface determines what methods may be invoked with an interface variable, even though the concrete value inside may have a larger set of methods*)

我们还可以这样写

var empty interface{}
empty = w

我们空接口值 empty 也将会包含同样的 (tty,*os.File),这很方便:一个空接口可以保存任何值,并包含我们可能需要的有关该值的所有信息。

(这个地方我们不需要类型断言,因为它静态的知道 w 满足空接口。在例子中我们移动一个值从 Reader 到 Writer,我们需要使用准确的类型断言,因为 Writer 的方法集合不是 Reader 的子集。)

一个重要的细节是接口内的对总是具有形式 (value, concrete type) 并且不能具有(value, interface type),接口不包含接口值。

现在让我们开始反射。

1. Reflection goes from interface value to reflection object.

反射可以将“接口类型变量”转换为“反射类型对象”。

在基本级别,反射只是一种检查存储在接口变量中的类型和值对的机制。在开始之前我们需要知道两种类型 reflect.Typereflect.Value,这两种类型可以访问接口变量的内容,并且有两个简单的方法 reflect.TypeOfreflect.ValueOf,检索 reflect.Type 和 reflect.Value 片段的接口值。(另外,从 reflect.Value 可以很容易地获得 reflect.Type,但是现在让我们将 Value 和 Type 概念分开。)

让我们从 TypeOf 开始

package main

import (
    "fmt"
    "reflect"
)

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

输出

type: float64

您可能想知道接口在哪里,因为程序看起来像是将 float64 类型的变量 x 而不是接口值传递给 reflect.TypeOf。但是根据 godoc reports,reflect.TypeOf 的定义包括一个空接口:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

当我们调用 reflect.TypeOf(x),x 先会存储在一个空的接口,然后作为参数传递,reflect.TypeOf 解压缩该接口以回复类型信息。

当然,reflect.ValueOf 函数可以恢复该值(从这里我们将省略样板并仅关注可执行代码):

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

输出

value: <float64 Value>

(我们明确地调用 String 方法,因为默认情况下,fmt 包会深入到 reflect.Value 中以显示其中的具体值 而 String 方法不会。)

reflect.Type 和 reflect.Value 都有很多方法可以让我们检查和操作它们。一个重要的例子是 Value 有一个 Type 方法可以返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有一个 Kind 方法,它返回一个常量,指示存储的项目类型:Uint,Float64,Slice等。使用 Int 和 Float 等名称的 Value 方法也可以获取存储在里面的值(如int64和float64):

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

还有像 SetInt 和 SetFloat 这样的方法,但要使用它们,我们需要理解 settability,这是第三反射定律的主题,将在下面讨论。

反射库有两个值得特别指出的属性。第一,为了保持 API 简洁,Value 的 “getter” 和 “setter” 类型的方法操作的是可以包含某个值的最大类型:*int64*,所有的有符号整型,只有针对 int64 类型的方法,因为它是所有的有符号整型中最大的一个类型。也就是说,Value的 Int 方法返回的是一个 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.

第二个属性是, 反射对象的种类是描述 underlying type,不是 static type。如果反射对象包含一个用户自定义的 int 类型,如下:

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

v 的种类还是 reflect.Int,即使 x 的 static type 是 MyInt 而不是 int。换句话说,Kind 不能将一个 int 从一个 MyInt 中区别出来,但是 Type 能做到。

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

fmt.Println("value kind:", v.Kind().String())
fmt.Println("value type:", v.Type().String())
fmt.Println("type kind:", t.Kind().String())

输出

value kind: int
value type: play.MyInt
type kind: int

2. Reflection goes from reflection object to interface value.

反射可以将“反射类型对象”转换为“接口类型变量”。

就像物理学上的反射,Go中到反射可以生成它的逆。

通过一个给定的 reflect.Value 我们可以通过使用 Interface 方法恢复成一个 interface 值,实际上,效果上就是这个Interface方法把类型和值的信息打包成一个接口表示并且返回结果:

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//	var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
	return valueInterface(v, true)
}

作为结果我们可以

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

把用反射对象 v 表示的 float64 类型的值打印了出来。

我们可以做的更好,fmt.Println,fmt.Printf 等方法的参数是一个空接口类型的值,所以我们可以让 fmt 包自己在内部完成我们在上面代码中做的工作。因此,为了正确打印一个 reflect.Value ,我们只需把 Interface 方法的返回值直接传递给这个格式化输出例程:

fmt.Println(v.Interface())

(为什么不 fmt.Println(v),因为 v 是一个 reflect.Value,我们想要其保存的具体值)因为我们的 value 是 flaot64 类型,我们设置可以使用 floating-point 格式化:

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

然后我们可以得到

3.4e+00

同样的,这不需要类型断言从 v.Interface() 到 float64,空接口类型值内部包含有具体值的类型信息,并且Printf方法会把它恢复出来。

简而言之, Interface 方法是逆转 ValueOf 函数,除了它的返回值的类型总是interface{}静态类型。

重申:反射从接口值到反射对象再返回。(*Reiterating: Reflection goes from interface values to reflection objects and back again.*)

3. To modify a reflection object, the value must be settable.

如果要修改“反射类型对象”,其值必须是“可写的”(settable)

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

下面的代码是无法工作的,但是非常值得学习。

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

如果运行这段代码,将会带着神秘信息的 panic

panic: reflect.Value.SetFloat using unaddressable value

这个问题不是因为 value 7.1 不是可以寻到地址,而是 v is not settable,Settability 是反射值的属性,并非所有反射值都具有它。

Value 的 CanSet 方法将会判定其是否可以 settability,例如:

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

输出

settability of v: false

如果调用 non-settable Value 的 Set 方法则会报错。所以什么是 settability

Settability 有点像可以寻址,但是更加严格。Settability 是一个性质,描述的是一个反射对象能够修改创造它的那个实际存储的值的能力。Settability由反射对象是否保存原始项(original item)而决定。

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 本身不受丝毫影响,所以如果真这样的话,将会非常那令人困惑,而且一点用都没有,所以,这么干是非法的,而 Settability 就是用来阻止这种非法状况出现的。

如果你觉得这个看起来有点怪的话,其实不是的,它实际上是一个披着不寻常外衣的一个你很熟悉的情况。想想下面这个把 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) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

到目前为止,输出是

type of p: *float64
settability of p: false

反射对象 p 是不能 Settable,但是我们想要设置的不是 p,而是(效果上来说)*p。为了得到 p 指向的东西,我们调用 Value 的 Elem 方法,这样就能迂回绕过指针,同时把结果保存在叫 v 的 Value 中。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

现在 v 是 Settable,如输出所示,

settability of v: true

并且由于它代表 x,我们终于能够使用 v.SetFloat 来修改 x 的值:

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

输出和期望的一样:

7.1
7.1

反射可能很难理解,但它正在完成语言所做的事情,尽管是通过掩盖了所发生的一切的反射 Types 和 Vlues 来实现的。请记住,Values 需要某些东西的地址才能修改它们代表的内容。

Structs

在我们之前的例子中,v 本身不是指针,它只是从一个派生而来。出现这种情况的常见方法是使用反射来修改结构的字段。只要我们有结构的地址,我们就可以修改它的字段。

这是一个分析结构值的简单示例,t 我们通过 struct 的地址创建一个反射对象,因为我们想再后面修改它的值,然后我们把 typeofT 变量设置为这个反射对象的类型,接着使用一些直接的方法调用(细节请见 reflect 包)来迭代各个域。注意,我们从struct类型中提取了各个域的名字,但是这些域本身都是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

关于 Settability 还有一个要点在这里要介绍一下: 这里 T 的域的名字都是大写的(被导出的),因为一个 struct 中只有被导出的域才是 Settable 的。

因为 s 包含了一个 Settable 的反射对象,所以我们可以修改这个 structure 的各个域。

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,那么上面对 SetInt 和 SetString 的调用将会统统失败,因为 t 的各个域不是 settable 的。

Conclusion

重申一下三大法则

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

一旦你理解了这三条反射定律,Go中的反射用起来就很简单了,虽然它还仍然有点微妙。反射是一个强大的工具,你必须要十分小心的使用它,并且应该尽量避免使用它,除非真的是不用不行了。

关于反射,仍然有大量内容我们没有讲到 —- 在 channel 中的发送操作和接收操作,分配内存,使用 slices 和 map,调用方法和函数—但是这篇文章已经够长了。我们将来会在随后的文章中讲到前面提到的这些topics中的一些。

参考资料