Go的反射法则
前言
反射指程序在运行时检查自身结构(尤其 类型)的能力,是元程序设计的一种形式,同时也是很多人为这迷惑的地方。
本文针对Go来说明反射如何工作,不同语言反射模型不同,有些语言早根本不支持反射,所以本文内容仅适用Go.
类型与接口
因为反射建立在类型系统之上,首先回顾一下Go的类型。
Go是静态类型。 每个变量都有静态类型,意味着,编译时*即有唯一&确定的类型。
i与j有着不同的静态类型,它们虽然有着相同uderlying type
,但未经强制转换不是不能直接相互赋值的。
接口代表指定的方法集合,是一种重要的类型。接口变量可以存储任何实现了接口所定义方法的类型的变量。比如说io.Reader io.Writer。
任何实现了Read方法的类型都可以赋给io.reader。
需要明白的是,不管r被赋予何值,其类型永远是io.Reader
。因为,Go是静态类型的,而r的静态类型是io.Reader
.
一个极其重要的接口类型例子是空接口。
它代表空方法集合,既然任何类型都有至少0个方法,那么自然都实现了interface{}接口。
有些人认为Go的接口是动态类型的,这是误解。接口也是静态类型的,一个接口变量永远有着固定的静态类型,虽然在运行其存储的值可以被改变类型,但其值一定是满足接口的。
接口的表示法
关于接口类型的表示法有一篇详细的文章, 这里只概括性地说明。
接口变量总是存储着两个信息: 被赋予的具体值+值的类型描述。更准确地说是,值是实现了该接口的具体类型的数据项,而类型描述是该具体类型的完整信息。例如
r是保存着 (tty, *os.File)信息的,虽然通过r只能调用Read()方法,但是它依然保存着tty的完整信息,这也是为什么我们能够执行这样的操作。
通过类型断言r中的具体类型tty也实现了Write()方法,我们可以将其赋值给w, w也保存着 (tty, *os.File)信息对。
接口的静态类型决定着通过它可以调用哪些方法,而其存储的实际数据项则可能包含着更大的方法集。
接着,我们也可以这样赋值。
它也有着同样的信息对:这很方便,我们可以给它赋任何值,而且它也会保留该值的完整信息。
(这里将w赋值给empty没有作类型强制转换,而之前将r赋值给w之前则作了断言,因为w并不是r的子集,而interface{}则是能静态确定满足条件的)
总之需要明白的是接口存储的是 (value, concrete type)信息,而不是 (value, interface type),接口不保存接口类型。
(注:接口本身具自己的静态类型,而它存储的则是被赋予的确定类型的值的信息,对于非接口变量,本身的静态类型==被赋予值的类型,所以并不需要再存储额外的类型信息)
现在我们可以开始谈反射了。
第一反射定律
从接口值 到 反射对象 的反射
最基本的反射是检查接口存储的(值,类型)信息对。 起步之前我们需要了解reflect包中的
1,两个类型Type
和Value
. 通过它们可以接触到接口变量的内容。
2,两个方法reflect.TypeOf
and reflect.ValueOf
, 从接口变量是提取上述两种信息。
(事实上通过reflect.Value可以很方便地获得 reflect.Type信息,不过我们先将其分开)
|
|
看起来没有用到接口,不过
输出是
(接下来可能会省略包名)
reflect.Type 和 reflect.Value都有大量的方法可供操作,比如
1 Value可以通过Type()获得Type
2 都有Kind()方法,返回一个常量,表明存储何种类型的数据项。
3 Value有如Int,Float的方法,可以抓取其中存储的值。(见上例)
(也有SetInt SetFloat方法,不过需要在理解settability之后讨论)
关于kind(注:省略了许多类型)
reflect包中有一此值得单独说明的属性。
1 为使api简洁,Value的set,get方法全部以该类别的最大类型来操作:int64 , float64,所以有时需要自行转换至实际的类型。
2 Kind返回的是底层类型而不是静态类型。也就是说自定义类型type MyInt int
的变量会被Kind识别为int类型,它不能区别int与myint,但Type可以。
反射第二定律
从反射对象到接口值
就像物理的反射,Go的反射也有它的可逆性。
从一个reflect.Value我们可以通过interface方法来恢复出一个接口值。事实上这个方法将
(value, concrete type)对重新打包成一个接口的表述并将其返回。
因此我们可以
来打印反射变量v所表述的float64值。
事实上,因为println, printf等函数接受的参数都是interface{}类型,我们可以直接
无需先做断言,print类函数将自己恢复底层类型信息。
简而言之,Interface()方法就ValueOf()方法的逆过程,除了它的返回类型总是interface{}之类。
第三反射定律
如要更改一个反射对象,其值须为settable
这也是三条定律中最奥妙最有迷惑性的一条,但如果我们重新从第一定律开始,还是足够好懂的。
以下代码不能真正运行,但值得一研究。
如果运行就会报如下错误:
当然实际问题不是7.1的值不可寻址,而是变量v不可设置。可设置性是Value的类型的属性,而且不是每种反射Value都具有。
使用CanSet()方法可以确认Value的可设置性。
|
|
那么到底什么是可设置性?
类似可寻址,都是以一个bit来表示,但可设置性更加严格。它表示反射对象可以真正地修改用于创建这个对象的原始内存内容。它取决于反射对象是否拥有原始内容。
当我们进行
|
|
是值传递,即传递的是x的copy。所以Set操作即使被允许,也不会改变原来的x,所以为了避免使用者混淆,将其归类为非法操作。
所以从根本上来说,这和普通函数的值传递与地址传递并无根本区别,只是将其设置为了非法操作。
所以如果我们想通过反射更改原始变量,我们应该进行地址传递。
|
|
想要获得*p指向的值,我们可以调用Value的 Elem()方法。
测试Elem()返回值的可设置性
现在我们可以通过v来改变x的值了
反射可能不太好懂,但它依然遵循语言的规则,虽然中间多了Value和Type的伪装。
结构
在我们之前的例子中,v不是指针而是指针的衍生物。因此通常可以用这种方式来改变结构体 的值。既然有了结构体的地址,那么就能改变它的Field值。
以下例子中,我们通过结构体的地址来创建反射变量,并使用reflect包中Type提供的方法来迭代遍历所有Field。请注意我们是通过reflect对象来提取结构体中的Filed名,提取出来的类型也是reflect.Value
|
|
值得说明的是:只有大写开头的Field名才是可设置的(settable).
|
|
如果不是从&t而是t来反射,那么set操作就会报错。
总结
如果理解了以上3条定律,那么反射就会好懂很多。当然反射的使用依然需要细心与谨慎,而且请在必要时使用。
还有许多没有提及的内容: 在channel中传递, 内存分配, slice与map, 方法与函数调用, 篇幅所限只能再在其它文章中说明。