原文链接:https://blog.golang.org/laws-of-reflection 需要翻墙
简介:
在计算机领域,反射(Reflection)提供一个程序检测自身结构的能力,特别是可以检测类型;反射是元编程的一种形式。同时反射也是混乱的根源。
在本文中我们试图解释在Go语言中反射是如何进行的。由于每种编程语言的反射模式都不同(有许多语言甚至不支持反射),本文主要讲解Go语言,所以本文接下来出现的所有“反射”均特指 Go语言中的反射
类型和接口
因为反射是建立在类型系统之上,让我们先来复习一下Go语言中的类型。
Go是静态类型语言,每个值都有其静态类型,也就是说:在编译时,每个值的类型(int, float32, *MyType, []byte 等等)就已经固定了。假设我们定义:
type MyInt int
var i int
var j MyIn
那么 i 的类型是 int,j 的类型就是 MyInt。这时候i和j的值是两种不同的静态类型,尽管它们的底层类型是相同的。他们之间不能互相赋值,除非通过强制类型转换。
有一个重要的类型是 interface 类型(接口类型),接口类型表示一组确定的方法的集合。
一个接口值可以存储任意实现了该接口的类型实体。io 包中的io.Reader 和io.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)
}
任何实现了 Read(Weite)方法的类型都被认为是实现了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{}
空接口表示它的方法集合是空,所以任何类型都可以赋值给空接口,因为任意类型都实现了0个或多个方法。
接口的表示:
Russ Cox 写过文章 detailed blog post 讲述了Go语言中接口值的含义。这里有没必要完整的复述一遍,以下是简单的总结。
接口类型的总是存储了两个值:变量的具体值,以及对值类型的描述。确切的说,是一个实现了改接口类型的对象值,和描述这一对象的完整类型。例如:
(Interface变量存储一对值:赋给该变量的具体的值、值类型的描述符。更准确一点来说,值就是实现该接口的底层数据,类型是底层数据类型的描述。举个例子:)
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方法的路径,type中的值携带着具体类型的所有信息。
这也解释了为何我们能这么做:
var w io.Writer
w = r.(io.Writer)
这里我自己举个原文外的例子:MyType类型同时实现了IType1, IType2两个接口,r中保存了(10, MyType)其中MyType包涵了该类型的所有信息
type IType1 interface{
Add()
}
type IType2 interface {
Sub()
}
type MyType int
func (m MyType)Add(){
fmt.Println("--- Add ---")
}
func (m MyType)Sub(){
fmt.Println("--- Sub ---")
}
func TestConvert(t *testing.T) {
var r IType1
r = MyType(10)
r.Add()
var w IType2
w = r.(MyType)
w.Sub()
}
前面例子中的w = r.(io.Writer) 叫做类型断言。它断言的是r中的具体类型也继承了io.Writer接口,所以我们可以把它赋值给w。经过断言后,w将包涵与r一样的(tty, *os.File)的值-类型对。
静态接口类型决定了那些方法能被相应接口类型的值调用,尽管该值包涵的底层类型可能实现了更多的方法。
当然,接下来我们还可以这么做:
var empty interface{}
empty = w
我们用空接口值empty接收了w,empty将再次获得(tty, *os.File)的值-类型对。我们可以 用一个空接口类型值存储任何值以及值类型,这其中包涵了任何我们需要的信息。
(这里我们不需要类型断言,因为w明显实现了空接口类型。在我们把r从类型Reader转变成Writer时需要明确的使用类型断言,因为 Writer接口的方法并不是Reader接口的子集)
One important detail is that the pair inside an interface always has the form (value, concrete type) and cannot have the form (value, interface type). Interfaces do not hold interface values.
另外需要注意的一点是,(value, type) 对中的 type 必须是 具体的类型(struct或基本类型),不能是 接口类型。 接口类型不能存储接口变量。
接下来,我们开始学习
反射
反射第一定律:
反射可以将“接口类型变量”转换为“反射类型对象”。( 注:这里反射类型指 reflect.Type 和 reflect.Value。)
从最基础的层面,反射提供了一个检查存储在接口类型值中type和 value的机制。首先,我们需要了解reflect包中的两种类型 Type和Value。这两种类型提供了我们访问接口类型值的权限,
reflect.TypeOf 和 reflect.ValueOf 两个方法分别返回一个接口类型值的Type和Value。(当然从reflect.Value中我们可以得到 reflect.Type,现在我们先分清这两个概念)
我们先以 TypeOf 为例子:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
以上代码输出:
type: float64
你可能会怀疑接口在哪里,(这里x是float64类型变量,并不是“接口类型变量-interface value”)上面的例子将float64类型的变量x传给reflect.TypeOf,并不是一个接口类型变量。
其实如文档 godoc reports 所示,reflect.TypeOf 使用一个空接口类型变量(interface{})接收参数。
// TypeOf returns the reflection Type of the value in the interface{}.
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>
(这里特别指定调用 String()方法,因为默认fmt包默认显示输出 reflect.Value 类型的具体值,调用String方法避免这么做)
reflect.Type 和reflect.Value 两个类型都提供了大量方法用于检测和操作他们。
一个重要的例子是 reflect.Value 类型 提供了 Type方法,返回Value的值类型reflect.Type,自己举个例子:
func TestVT(t *testing.T) {
a := 100
typ := reflect.TypeOf(a)
val := reflect.ValueOf(a)
fmt.Println(typ == val.Type())
}
以上代码 输出:
true
即 reflect.ValueOf(a).Type() 等价于 reflect.TypeOf(a)
另一个例子是 Type,Value两个类型都提供了Kind方法。该方法返回一个表示底层数据类型的常量,常见类型有Uint, Float64, Slice等等。
Value类型还有一些类似于Int、Float的方法,用来提取底层的数据。Int方法用来提取 int64, Float方法用来提取 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。
也就是说 v.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.
第二点值得说明的是:Kind方法描述的是被反射对象的底层类型,不是静态类型。如果一个反射对象包含用户自定义整数类型值, 调用对象的Kind方法,依旧返回 reflect.Int
即使它的静态类型是自定义的MyInt。也就是说 Kind 只返回底层类型,不能区分用户自定义类型;不过 Type 可以得到用户定义的静态类型。,如下:
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
fmt.Println(v.Type(), v.Kind())
输出
test_reflect.MyInt int
反射第二定律:
反射可以将“反射类型对象”转换为“接口类型变量”。(与第一定律相对)
就像物理中的反射一样,Go语言的反射支持从 反射类型对象 反向生成 接口类型变量。
给定一个 reflect.Value 类型的对象,可以通过 interface 方法还原出一个 接口类型变量;实际上,这个方法把type 和 value 信息打包并填充到一个接口变量中,然后返回。
其函数声明如下:
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
然后,我们可以通过类型断言,恢复底层的具体值。
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
以上代码打印出一个 float64 类型的值。
我们可以做得更好,由于 fmt.Println, fmt.Printf 等一系列方法都接受一个空接口类型值作为参数,然后就像我们上面做的一样,在fmt包内进行拆包。(在内部对每个参数进行判断,不同类型有不同输出,详见print中的printArg方法)
因此,fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给 格式化打印程序:
为什么不直接调用 fmt.Println(v) 呢?因为 v 是 reflect.Value 类型;我们要的是它储存的具体值。(其实直接调用的输出也是一样的,println方法底层对 reflect.Value 类型又做了单独的处理,文档中如此举例应该是文档太旧-2011.9.6的版本)。由于底层的值是一个 float64,我们可以用 float64 格式化打印:
fmt.Printf("value is %7.1e\n", v.Interface()) // 3.4e+00
这里同样无需使用类型断言(type-assert)将 v.Interface() 断言为 float64 空接口类型值中包含了这个值的所有信息,printf 方法会恢复它的类型。
简单来说, Interface 方法和 ValueOf 方法的作用刚好相反。唯一不同的是 Interface 方法返回的是静态类型 interface{}
再次重申一遍:反射定律前两条 Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。
反射第三定律:
如果要修改"反射类型对象",其值必须是“可写入的”(settable)
这条定律很微妙,也很容易让人迷惑。但是如果你从第一条定律开始看,应该比较容易理解。
下面这段代码不能正常工作,但是非常值得研究:
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 类型的 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)
v.SetFloat(7.1)
我们将变量 x 的一个拷贝赋值给 reflect.ValueOf 方法,所以接收参数的 interface{} 得到的是一个 x 的拷贝,并非 x 本身。
这里假设第三行代码能成功执行的话,那么修改的也不是 x 原始值,及时看起来 v 像是根据 x 创建的。
相反,它会更新 x 存在于 反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。
这看起来似乎有点诡异,事实上并非如此,而且类似的情况很常见。考虑以下两个函数调用
f(x)
f(&x)
第一个函数调用将 x 的值拷贝传递给函数 f 。因此在函数内部无法修改外部变量 x。如果希望函数能修改变量 x 的值,我们需要将 x 的地址传递给函数 f(即传 x 的指针 &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())
以上代码输出的 CanSet 还是false
type of p: *float64
settability of p: false
即使传递的是地址,反射对象 p 依旧是不可写的,为什么呢?其实这里 p 并非我们要修改的值,事实上,我们要修改的是*p (传地址给reflect.Value后得到的反射类型是指针类型,这时候p.Kind()==reflect.Ptr)
为了得到 p 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value类型对象 v中:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
// 输出: settability of v: true
因为这时 v 代表原始值 x 因此我们可以通过 v.SetFloat修改x的值。
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
输出:
7.1
7.1
反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。
你只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。
结构体:
在上述例子中,v并不是指针,它只是从指针衍生出来的。一个类似的场景是使用反射修改结构体内的字段。类似的,只要得到结构体的地址,我们就能修改它的字段。
下面通过一个简单的例子对结构体类型变量 t 进行分析。
我们使用变量t的地址创建反射对象,因为稍后我们将修改它。
然后我们创建 typeOfT 为它的类型,通过简单的方法(NumField 等; 详细参照 package reflect )迭代结构体的字段。
注意我们从Type 类型的 typeOfT 中获取字段名,他们中的每个字段都是 反射对象 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 的字段都是首字母大写的(可导出的),因为struct中只有“可导出的”的字段才是“可写的”。
由于变量 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的值是因为s通过 &t 创建。如果 s通过t创建,那么调用 SetInt和SetString时将会设置t的字段时将会报错。
总结:
再总结一下反射三定律:
反射可以将“接口类型变量”转换成“反射类型对象”
反射可以将“反射类型对象”转换成“接口类型变量”
想要通过反射修改对象,反射对象的值必须是“可写入”的(传递指针)
一旦你理解了这些定律,使用反射将会是一件非常简单的事情。它是一件强大的工具,使用时务必谨慎使用,更不要滥用。
关于反射,我们还有很多内容没有讨论,包括基于管道的发送和接收、内存分配、使用slice和map、调用方法和函数,这些话题我们会在后续的文章中介绍。
有疑问加站长微信联系(非本文作者)