09-反射reflect
编程语言中反射的概念
在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。
多插一句,Golang的gRPC也是通过反射实现的。
interface 和 反射
在讲反射之前,先来看看 Golang 关于类型设计的一些原则
变量包括(type, value)两部分:
- type 包括
static type
和concrete type
。简单来说static type
是你在编码是看见的类型(如int、string),concrete type
是runtime
系统看见的类型 - 类型断言能否成功,取决于变量的
concrete type
,而不是static type
。因此一个reader
变量如果它的concrete type
也实现了write
方法的话,它也可以被类型断言为writer
。
接下来要讲的 反射
,就是建立在类型之上的,Golang 的指定类型的变量的类型是静态的(也就是指定 int、string 这些的变量,它的 type 是 static type),在创建变量的时候就已经确定,反射主要与 Golang 的 interface 类型相关(它的 type 是 concrete type),只有 interface 类型才有反射一说。
在 Golang 的实现中,每个 interface 变量都有一个对应 pair,pair 中记录了实际变量的值和类型:(value, type)
value 是实际变量值,type 是实际变量的类型。一个 interface{}
类型的变量包含了 2 个指针,一个指针指向值的类型【对应 concrete type】,另外一个指针指向实际的值【对应 value】。
例如,创建类型为 *os.File
的变量,然后将其赋给一个接口变量r:
1 |
|
接口变量 r 的 pair 中将记录如下信息:(tty, *os.File)
,这个 pair 在接口变量的连续赋值过程中是不变的,将接口变量 r 赋给另一个接口变量 w:
1 |
|
接口变量 w 的 pair 与 r 的 pair 相同,都是 (tty, *os.File)
,即使 w 是空接口类型,pair 也是不变的。
interface 及其 pair 的存在,是 Golang 中实现反射的前提,理解了 pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值 value;类型 concrete type) pair 对的一种机制。
1 |
|
再比如:
1 |
|
Golang 的反射 reflect
reflect 的基本功能 TypeOf 和 ValueOf
既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair 对的一种机制。那么在 Golang 的 reflect 反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢?它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是 reflect.ValueOf()
和 reflect.TypeOf()
,看看官方的解释
1 |
|
reflect.TypeOf()
获取 pair 中的 typereflect.ValueOf()
获取 pair 中的 value
示例如下:
1 |
|
说明
reflect.TypeOf
: 直接给到了我们想要的type类型,如 float64、int、各种 pointer、struct 等等真实的类型reflect.ValueOf
:直接给到了我们想要的具体的值, 如 1.2345 这个具体数值,或者类似&{1 "Allen.Wu" 25}
这样的结构体 struct 的值- 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是
reflect.Type
和reflect.Value
这两种
从 relfect.Value
中获取接口 interface
的信息
执行 reflect.ValueOf(interface)
之后,就得到了一个类型为 ”relfect.Value”
变量,可以通过它的 Interface()
方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。
已知原有类型【进行“强制转换”】
已知类型后转换为其对应的类型的做法如下,直接通过 Interface 方法然后强制转换,如下:
1 |
|
示例如下:
1 |
|
说明
- 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
- 转换的时候,要区分是指针还是指值
- 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
未知原有类型【遍历探测其Filed】
很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其 Filed 来得知,示例如下:
1 |
|
说明:通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:
- 先获取
interface
的reflect.Type
,然后通过NumField
进行遍历 - 再通过
reflect.Type
的Field
获取其Field
- 最后通过
Field
的Interface()
得到对应的 value
通过运行结果可以得知获取未知类型的 interface 的所属方法(函数)的步骤为:
- 先获取
interface
的reflect.Type
,然后通过NumMethod
进行遍历 - 再分别通过
reflect.Type
的Method
获取对应的真实的方法(函数) - 最后对结果取其 Name 和 Type 得知具体的方法名
- 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
- struct 或者 struct 的嵌套都是一样的判断处理方式
通过 reflect.Value
设置实际变量的值
reflect.Value
是通过 reflect.ValueOf(X)
获得的,只有当 X
是指针的时候,才可以通过 reflec.Value
修改实际变量 X 的值,即:要修改反射类型的对象就一定要保证其值是 “addressable” 的。
示例如下:
1 |
|
说明
- 需要传入的参数是
* float64
这个指针,然后可以通过pointer.Elem()
去获取所指向的 Value,注意一定要是指针。 - 如果传入的参数不是指针,而是变量,那么
- 通过 Elem 获取原始值对应的对象则直接 panic
- 通过 CanSet 方法查询是否可以设置返回 false
- newValue.CantSet() 表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
- reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
- 也就是说如果要修改反射类型对象,其值必须是 “addressable”【对应的要传入的是指针,同时要通过 Elem 方法获取原始值对应的反射对象】
- struct 或者 struct 的嵌套都是一样的判断处理方式
通过 reflect.ValueOf
来进行方法的调用
前面我们说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。另外一个常用的用法,就是通过 reflect 进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定
示例如下:
1 |
|
说明
- 要通过反射来调用起对应的方法,必须要先通过
reflect.ValueOf(interface)
来获取到 reflect.Value,得到 “反射类型对象”后才能做下一步处理 reflect.Value.MethodByName
这.MethodByName
,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。[]reflect.Value
,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。reflect.Value
的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value'Kind
不是一个方法,那么将直接 panic。- 本来可以用
u.ReflectCallFuncXXX
直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是 MethodByName,然后通过反射调用 methodValue.Call
Golang的反射reflect性能
Golang 的反射很慢,这个和它的 API 设计有关。在 java 里面,我们一般使用反射都是这样来弄的。
1 |
|
这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的 obj,就可以取得这个 obj 上对应的 field。
但是 Golang 的反射不是这样设计的:
1 |
|
这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射
1 |
|
这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。
Golang reflect慢主要有两个原因
- 涉及到内存分配以及后续的GC;
- reflect实现里面有大量的枚举,也就是for循环,比如类型之类的.
总结
上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:
- 反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
- 反射必须结合interface才玩得转
- 变量的type要是concrete type的(也就是interface变量)才有反射一说
- 反射可以将“接口类型变量”转换为“反射类型对象”
- 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
- 反射可以将“反射类型对象”转换为“接口类型变量
- reflect.value.Interface().(已知的类型)
- 遍历reflect.Type的Field获取其Field
- 反射可以修改反射类型对象,但是其值必须是“addressable”
- 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
- 通过反射可以“动态”调用方法
- 因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现