反射的规则(The Laws of Reflection)
https://go.dev/blog/laws-of-reflection
介绍
反射(Reflection)是指程序检查自身结构的能力,尤其是通过类型(types)。这是元编程(metaprogramming)的一种形式。这也是造成混乱的一大根源。
在这片文章中我们将通过解释反射在Go中如何工作使一些问题变得明了。每种语言的反射模型是不同的(许多语言完全不支持反射),这篇文章是关于Go,因此在文章的剩余部分“反射(reflection)”应该理解为“Go语言的反射(reflection in Go)”。
类型和接口(Types and interfaces)
因为反射是在类型系统(type system)上建立的,因此让我们从复习Go中的类型开始。
Go语言是静态类型。每个变量都有一个静态类型,即在编译时已知且固定的类型:int,float32,*MyType,[]bute
等等。
type MyInt int
var i int
var j MyInt
如果我们声明 i
是 int
类型和 j
是 MyInt
类型。那么变量 i
和 j
将具有不同的静态类型并且尽管它们具有相同的底层类型,但是如果不进行类型转换,它们就不能相互赋值。
接口(interface)是一种重要的type,它代表固定的方法集。(我们在讨论反射的时候,可以忽略使用作为多态代码(泛型)约束中使用的接口定义)。一个接口变量(interface value)可以存储任何实现了该接口方法的具体值(该值不能也是一个接口)。众所周知的一对例子是 io.Reader
和 io.Writer
,Reader
和 Writer
这两个类型来自 io package
:
// Reader是一个包含基本Read方法的interface
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer是包含基本Write方法的interface
type Writer interface {
Write(p []byte) (n int, err error)
}
任何一种实现了 Read
(或者 Write
)方法签名的类型被认为实现了 io.Reader
(或者 io.Writer
)。io.Reader
类型的变量可以接受任何具有 Read
方法的值:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// 等等
无论 r
包含的具体值是什么,r
的类型永远是 io.Reader
,理解这点是非常重要的,Go是静态类型语言,r
的静态类型是 io.Reader
。
接口类型中一个非常重要的例子是空接口(empty interface)
interface{}
空接口的等效别名
any
空接口代表空方法集并且可以接受任何值,因为每种值都包含零种或多种方法。
有些人说Go的接口(interfaces)是动态类型,该说法是有误导性的。实际上它们是静态类型:一个接口类型的变量它的类型是静态不变的,尽管在运行的过程中存储在接口变量中的值可能被改变,但那个值一直是满足该接口的。
我们需要理解好这些概念,因为反射和接口密切相关的。
接口的表示(The representation of an interface)
Russ Cox已经写过一篇详细的博客介绍Go中的接口值表示。这里不做过多赘述,但是做简化总结
一个接口类型的变量存储着两个值(或者说是一对值"pair"):分配给变量的具体值和该值类型的描述符。更精确点说,这个值指的是实现该接口的底层数据项,这个类型描述符指的是数据项的完整类型。
例如
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
之外的其它方法;尽管接口值只提供了对 Read
方法的访问,其内部的值却携带了关于该值的全部类型信息,因此我们可以做如下操作:
var w io.Writer
w = r.(io.Writer)
这个赋值语句是类型断言(type assertion);它断言的是 r
内部的内容同样实现了 io.Writer
我们可以把它分配给 w
。分配完之后,w
将会包含和 r
中一样的数据对(tty,*osFile)。一个接口变量的的静态接口类型决定哪种方法可能被调用,尽管其中具体的值可能包含更大的方法集。
继续,我们可以完成下面的操作:
var empty interface{}
empty = w
我们的空接口值 empty
将会包含同样的(tty,*osFile)对。这很方便,空接口可以包含任何值并且包含所有我们可能用到的关于这个值的信息。
(在这里我们不需要类型断言,因为我们已经静态的知道 w
肯定满足空接口。而在我们将值从 Reader
移动到 Writer
中的例子中,我们则需要明确的使用类型断言,因为 Writer
的方法不是 Reader
的子集。)
这里有一个重要的细节,一个接口变量总是(value,type)的形式,这里的type必须是具体类型,不能是接口类型,即不能是(value, interface type)。一个接口不能持有接口类型的值(Interfaces do not hold interface values.)。
复习完毕,我们准备开始反射的内容。
反射的第一法则(The first law of reflection)
1.反射从接口值到反射对象
从根本上来说,反射只是一种检查接口变量中存储的数据对 (value, type) 的机制。首先,我们需要从 package reflect
包中了解两种类型: Type
和 Value
。这两种类型允许访问接口变量的内容,通过两个简单的函数 reflect.TypeOf
和 reflect.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
。但是go文档中提到 reflect.TypeOf
的签名包含一个空接口。
// TypeOf 会返回返回interface{}中的值的反射类型.
func TypeOf(i interface{}) Type
当我们调用 reflect.TypeOf(x)
,x
是第一个被存储在空接口的值,然后它会做为一个参数被传递;reflect.TypeOf
会解包这个空接口得到类型信息。
使用reflect.ValueOf
会得到value
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
输出
value: <float64 Value>
(这里我们使用 String
方法的原因是默认情况下 fmt package
会深入挖掘feflect.Value
然后去展示里面具体的值。但是 String
方法不会)
reflect.Type
和 reflect.Value
都会给我们提供很多的方法供我们做检查或做操作。比如比较重要的, Value
的Type
方法会返回 reflect.Value
的类型, Type
和 Value
都有一个 Kind
方法,该方法返回一个常量,指示存储的项目类型:Uint,Float64,Slice
等等。此外 Value
上名为 Int
和 Float
的方法可以让我们获取到存储在其中的值(如int64和flaot64):
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
这样的方法,但是使用它们,我们需要了解可设置性,即下面将会讨论的反射第三定律。
反射库中有几个需要特别指出的特性。首先为了保证API简洁,Value
的“getter”和“setter”方法定义能容纳值最大的类型进行操作:例如,对所有的带符号整数,使用 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 会返回一个 uint64.
第二个特性是,反射对象的 Kind
描述的是基础类型,而非静态类型。如果一个反射对象包含了一个用户自定义的整数类型值,如下所示:
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
那么 v
的 Kind
仍然是 reflect.Int
,即使 x
的静态类型是 MyInt
而不是 int
。换句话说,Kind
无法区分 int
和 MyInt
,即使 Type
方法可以做到这一点。
反射的第二定律(The second law of reflection)
2.反射从反射对象到接口值
和物理中的反射类似,Go中的反射会生成自身的逆
给定一个 reflect.Value
我们可以使用 Interface
方法恢复接口值;实际上,该方法将type和value信息打包进了一个接口表达中并返回回去。
// Interface以interface{}返回v的value。
func (v Value) Interface() interface{}
因此我们可以说
y := v.Interface().(float64) // y将会拥有类型float64.
fmt.Println(y)
打印反射对象 v
所表示的 float64
值
不过我们可以做的更好。fmt.Println
、fmt.Printf
等的参数都是作为空接口值传递,空接口之会被 fmt
包在内部解包就像我们之前的例子中那样。因此正确的打印 reflect.Value
的内容只需要将 interface
方法的结果传递给格式化的打印例程:
fmt.Println(v.Interface())
(因为这篇文章编写的较早,fmt已经做类更改,以便它能够自动解包 reflect.Value ,因此我现在我们能直接做下面的操作)
fmt.Println(v)
由于我们的值是 float64
,因此如果我们需要我们可以直接使用
fmt.Printf("value is %7.1e\n", v.Interface())
输出结果
3.4e+00
这里不需要将 v.Interface()
声明为float64;空接口值里面有具体值的类型信息,Print 会把它恢复出来。
总之,Interface
方法是 ValueOf
方法的逆方法,它的结果的静态类型始终是interface{}
。
反射的第三定律
3.要修改反射对象,其值必须可设置。
第三定律是最微妙和最令人困惑的,但如果我们从第一原理开始,它就很容易理解。
这里有一些并不会工作但是值得学习的代码。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
如果你运行这段代码,他将会 panic
一段神秘信息
panic: reflect.Value.SetFloat using unaddressable value
问题并不是值7.1不可寻址;而是 v
是不可设置的。可设置性是反射 Value
的一个属性,并不是所有的反射 Value
都拥有可设置性。
Value
的 CanSet
方法会报告 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
的反射值(refleaction value)指向它。
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
指向的内容,我们调用 Value
的 Elem
方法,该方法会通过指针间接访问,并将结果保存在名为 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
虽然反射可能难以理解,但它所做的正是语言本身在做的事情,只是通过反射类型和值来进行,这有时会掩盖实际发生的过程。请记住,反射 Value
需要地址才能修改它们所表示的内容。
Structs
在我们之前的例子中 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
创建的,那么对 SetInt
和 SetString
的调用将会失败,因为 t
的字段将不会是可设置的。
结论
以下是反射的基本法则:
- 反射从接口值转换到反射对象。
- 反射从反射对象转换回接口值。
- 若要修改反射对象,该值必须是可以设置的。
一旦你理解了这些法则,Go 语言中的反射就会变得容易使用得多,尽管它依然很微妙。它是一个强大的工具,应当谨慎使用,并且除非绝对必要,否则应避免使用。
我们尚未涵盖反射的更多内容——比如在通道上发送和接收数据、分配内存、使用切片和映射、调用方法和函数等——但这篇帖子已经够长了。我们将在后续的文章中探讨其中的一些主题。