16 反射
16 反射
反射是程序在运行时动态获取变量类型信息并操作其值的能力。
在 cpp 中,是没有反射这个概念的,在编译期就确定了所有类型,那假如说我有一个函数传入的参数是一个 std::any
类型,那我如何在这个函数里对传入的参数进行取值和更改呢,这就是反射的作用: 不需要在编译期确定所有类型,而是在运行期间处理类型未知的值。
Go 的反射功能由 reflect 包提供,核心是两个类型: reflect.Type
和 reflect.Value
。
Type 与 Value
类型信息
如果需要某个参数的类型信息,需要调用 reflect.TypeOf()
方法,它会返回 reflect.Type
,包含类型的许多元信息,比如类型名称、大小、方法、字段以及内存对齐等信息:
func reflectType(x any) {
t := reflect.TypeOf(x)
fmt.Printf("Type: %v, Name: %v, Kind: %v\n", t, t.Name(), t.Kind())
}
func main() {
reflectType(3.14) // Type: float64, Name: float64, Kind: float64
}
对于类型名称,我们上面给了两种方式来获取,这两种方式是有区别的:
Name: 类型名称,自定义类型就是我们使用 type 定义的名字,内置类型中只有基本类型会返回名字,引用类型一类的为空。
Kind: 底层类型种类,指向自定义类型底层基于的内置类型。
值信息
如果需要值相关信息,则需要调用 reflect.ValueOf()
方法,它会返回 reflect.Value
,它不只有值的类型信息,还具有许多与值相关的方法,如获取值、修改值、调用方法等,类似于一个大杂烩,它把切片、map、chan等操作都放到这个类型里,我们可以通过 value 类型直接调用所有内置类型的方法。
它本身为了维持一个统一,并不具备类型信息,如果我们想获取值,还是需要通过类型指针先来获取到类型再转换为对应类型才可操作:
func reflectValue(x any) {
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Int:
fmt.Println("int:", v.Int())
case reflect.String:
fmt.Println("string:", v.String())
case reflect.Bool:
fmt.Println("bool:", v.Bool())
}
}
如果想要修改值,就需要传入指针,这点在反射中也是如此,但是就会出现一个问题,Value 包装的值为指针,该怎么取值进行修改?此时就需要引入反射的解引用操作 Elem()
了:
func reflectSetValue(x any) {
v := reflect.ValueOf(x)
v.Elem().SetInt(10) // 调用 Elem 后 Value 包装的值就和原来的一样了
}
func main() {
var x int = 5
reflectSetValue(&x)
fmt.Println(x) // 10
}
结构体反射
字段信息
还是上面那句话,获取类型信息就通过 TypeOf
,操作是一样的,可以通过索引和字段名来获取字段信息:
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Score int `json:"score"`
}
func printStructField(s any) {
t := reflect.TypeOf(s)
// 兼容值类型和指针类型
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
fmt.Println("不是结构体")
return
}
// 通过索引获取字段
field0 := t.Field(0)
fmt.Println(field0.Name) // Name
fmt.Println(field0.Type) // string
fmt.Println(field0.Tag.Get("json")) // name
// 通过名称获取字段
field1, _ := t.FieldByName("Age")
fmt.Println(field1.Name) // Age
// 字段数量
fmt.Println(t.NumField()) // 3
}
同样的,修改字段值依旧是通过 ValueOf
,操作也很简单:
func reflectChangeStruct(s any) {
v := reflect.ValueOf(s)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
fmt.Println("必须传结构体指针")
return
}
v.Elem().FieldByName("Name").SetString("李四")
v.Elem().FieldByName("Age").SetInt(20)
}
func main() {
s := Student{Name: "张三", Age: 18, Score: 90}
reflectChangeStruct(&s)
fmt.Println(s) // {李四 20 90}
}
方法调用
这个部分和结构体的方法那块内容是一样的,不过需要按照反射的方式调用:
func (s Student) GetInfo() string {
return fmt.Sprintf("姓名:%s, 年龄:%d, 成绩:%d", s.Name, s.Age, s.Score)
}
func (s *Student) SetInfo(name string, age, score int) {
s.Name = name
s.Age = age
s.Score = score
}
func callStructFunc(x any) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
// 调用无参方法
result := v.MethodByName("GetInfo").Call(nil)
fmt.Println(result[0].String())
// 调用有参方法
params := []reflect.Value{
reflect.ValueOf("王五"),
reflect.ValueOf(25),
reflect.ValueOf(95),
}
v.MethodByName("SetInfo").Call(params)
}
反射这玩意虽然很好使,但是不能乱用,因为反射非常的慢,要比普通的代码慢 10~100 倍,且只有运行中会报错,也不易检查,因此尽量还是使用泛型和接口一类的方案来替代反射吧。