GO语言入门经典-反射3(Value 与对象的值)

12.3 Value 与对象的值

调用 ValueOf 函数,可以获得 Go 代码对象值相关的信息。这些信息将由 Value 结构体封装。例如:

go 复制代码
var nm uint32 = 10000
theVal := reflect.ValueOf(nm)

上述代码先定义了变量 nm,类型为 uint32,初始化的值为 10000,接着调用 ValueOf 函数获得一个 Value 对象,最后通过此对象可以在应用程序运行期间进行动态分析,例如:

go 复制代码
if theVal.Kind() == reflect.Uint32 {
    fmt.Printf("此对象的值: %v\n", theVal.Uint())
}

12.3.1 修改对象的值

使用 Value 类型公开的 Set 方法,能在运行时修改某个对象的值。其实现的功能与赋值运算符相同,但通过反射技术进行赋值,在一些需要动态处理的代码逻辑中会比较灵活(例如动态生成函数体逻辑)。

Set 方法是用另一个 Value 对象的值来修改当前 Value 对象的值。在调用 Set 方法前,最好先调用 CanSet 方法,判断对象的值是否允许修改。

请看下面例子:

go 复制代码
var who string = "小明"
fmt.Printf("变量 who 的原值: %v\n", who)

// 通过反射技术修改变量的值
val := reflect.ValueOf(&who)
if val.Kind() == reflect.String {
    if val.CanSet() {
        val.Set(reflect.ValueOf("小吴"))
    } else {
        fmt.Println("变量 who 不允许被修改")
    }
}

fmt.Printf("修改后,变量 who 的值: %v\n", who)

上面代码首先定义变量 who,初始值为"小明",然后通过 ValueOf 函数取得与变量相关的 Value 对象,最后调用 Set 方法尝试将变量 who 值修改为"小吴"。

可是,运行上述代码后,得到的结果是变量 who 不允许被修改。

复制代码
变量 who 的原值: 小明
变量 who 不允许被修改
修改后,变量 who 的值: 小明

这说明 CanSet 方法返回了 false。此时不妨查看一下 CanSet 方法的源代码。

go 复制代码
func (v Value) CanSet() bool {
    return v.flag&(flagAddr|flagRO) == flagAddr
}

不管 Value 对象所包含的值是否为只读,首先它要满足的条件是------可以引用其内存地址。因为变量在传递过程中会进行自我复制,这会导致后续代码所操作的值已经不是 who 变量自身,而是它的副本。

所以,在调用 ValueOf 函数的时候,应该传递 who 变量的地址。上面代码可以做以下修改。

go 复制代码
val := reflect.ValueOf(&who)
// 获取指针指向的对象
val = val.Elem()
if val.Kind() == reflect.String {
    ......
}

注意 ValueOf 函数获取的是 who 变量的地址,即其类型为 *string,它的 Kind 方法返回的不是 string,而是 ptr。所以在修改对象值之前,可以调用一次 Elem 方法,获取另一个 Value 对象,它包含 who 变量的实际值。

代码经过修改后,再次运行就能得到正确的结果。who 变量在传递过程中没有进行自我复制,只是传递了它的内存地址,因此它能够被修改。

复制代码
变量 who 的原值: 小明
修改后,变量 who 的值: 小吴

为了方便调用,在 Set 方法之外,Value 类型还公开了以下方法:

go 复制代码
func (v Value) SetBool(x bool)
func (v Value) SetBytes(x []byte)
func (v Value) SetComplex(x complex128)
func (v Value) SetFloat(x float64)
func (v Value) SetInt(x int64)
func (v Value) SetMapIndex(key, elem Value)
func (v Value) SetPointer(x unsafe.Pointer)
func (v Value) SetString(x string)
func (v Value) SetUint(x uint64)

intint8int16int32int64 类型的值统一使用 SetInt 方法来设置;uintuint8uint16uint32uint64 类型的值统一使用 SetUint 方法来设置;float32float64 类型的值使用 SetFloat 方法来设置;complex64complex128 类型的值使用 SetComplex 方法来设置。

下面例子演示了 SetBool 方法的使用。

go 复制代码
var bv = false
fmt.Printf("变量的原值: %v\n", bv)

var val = reflect.ValueOf(&bv)
// 获取指针指向的值
var bval = val.Elem()
if bval.Kind() == reflect.Bool && bval.CanSet() {
    bval.SetBool(true)
}

fmt.Printf("变量的最新值: %v\n", bv)

12.3.2 读写结构体实例的字段

Type 类型相似,Value 类型也定义了 NumFieldFieldFieldByName 等方法。用法与 Type 相同,不同的是,在 Type 类型中,这些方法获取的是字段的类型,而在 Value 类型中,这些方法获取的是字段的值。

例如,dog 结构体的定义如下:

go 复制代码
type dog struct {
    Nick string
    Color string
    Age uint8
}

定义 printValues 函数,用于输出对象中各个字段的值。

go 复制代码
func printValues(obj interface{}) {
    var theVal = reflect.ValueOf(obj)
    // 如果传递过来的是对象的指针
    // 那么先获取该指针所指向的对象
    if theVal.Kind() == reflect.Ptr {
        theVal = theVal.Elem()
    }
    // 获取字段成员数量
    ln := theVal.NumField()
    // 访问所有字段
    for i := 0; i < ln; i++ {
        tm := theVal.Type().Field(i).Name
        vm := theVal.Field(i).Interface()
        fmt.Printf("%s: %v\n", tm, vm)
    }
}

定义 setValues 函数,用于输出对象中各个字段的值。

go 复制代码
func setValue(obj interface{}, fdname string, fdval interface{}) {
    var objval = reflect.ValueOf(obj)
    if objval.Kind() != reflect.Ptr {
        fmt.Println("请使用指针类型")
        return
    }
    objval = objval.Elem()
    // 查找字段
    fd := objval.FieldByName(fdname)
    if fd.IsValid() == false {
        fmt.Println("未找到目标字段")
        return
    }
    // 验证一下值的类型是否与字段匹配
    newVal := reflect.ValueOf(fdval)
    if fd.Kind() != newVal.Kind() {
        fmt.Println("字段值的类型不匹配")
        return
    }
    // 设置新值
    fd.Set(newVal)
}

在修改对象的字段成员时,也应该传递对象的内存地址,否则将操作失败。

定义 dog 类型的变量,然后实例化。

go 复制代码
var mypet = dog{
    Nick: "Peter",
    Color: "black",
    Age: 2,
}

首先调用 printValues 函数输出 dog 对象的字段值,然后调用 setValue 方法修改 Age 字段的值,最后再次打印 dog 对象的字段值。

go 复制代码
fmt.Println("----- 修改前 -----")
printValues(mypet)
setValue(&mypet, "Age", uint8(5))
fmt.Println("\n----- 修改后 -----")
printValues(mypet)

上述例子的运行结果如下:

复制代码
----- 修改前 -----
Nick: Peter
Color: black
Age: 2
----- 修改后 -----
Nick: Peter
Color: black
Age: 5

12.3.3 更新数组/切片的元素

如果 Value 对象所代表的值是数组/切片类型,就可以使用 Index 方法获取指定索引处的元素的值。Index 方法返回的值也是 Value 类型。

在使用反射技术读写数组/切片的元素时,要注意以下规则:

  1. 指定的索引值不能超出有效范围。索引的有效范围为 [0, n)
  2. 新值的类型必须与旧值匹配。例如,元素的原值为 float32 类型,在更新元素时,如果新值是 string 类型,就会发生冲突。

下面代码定义了 updateElement 函数,它的功能是更新元素值。

go 复制代码
func updateElement(obj interface{}, index int, elval interface{}) {
    v := reflect.ValueOf(obj)
    // 要求目标对象是数组或者切片
    if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
        // 获取元素的总数量
        ln := v.Len()
        // 验证指定的索引是否有效
        if index < 0 || index >= ln {
            fmt.Println("索引超出有效范围")
            return
        }
        // 获取元素值
        oldval := v.Index(index)
        newval := reflect.ValueOf(elval)
        // 验证新值的类型是否与旧值匹配
        if oldval.Kind() != newval.Kind() {
            fmt.Println("元素值的类型不匹配")
            return
        }
        // 更新元素值
        oldval.Set(newval)
    } else {
        fmt.Println("对象类型不是数组或切片类型")
        return
    }
}

接下来可以测试一下updateElement 函数。

go 复制代码
var kx = []int{1, 5, 7, 12}
fmt.Println("更新前:", kx)
updateElement(kx, 1, 9000)
fmt.Println("更新后:", kx)

上面代码中,先定义变量 kx,它是切片类型,初始化之后,其中包含 4 个元素。然后调用 updateElement 函数,将索引为 1 的元素更新为 9000。程序的执行结果如下:

复制代码
更新前: [1 5 7 12]
更新后: [1 9000 7 12]

12.3.4 调用函数

使用 Value.Call 方法可以实现通过反射技术来调用函数,Call 方法的签名如下:

go 复制代码
func Call(in []Value) []Value

Call 调用比较简单,in 参数表示目标函数的输入参数列表,调用后 Call 方法会将目标函数的返回值作为结果返回(可理解为输出参数)。

下面的代码定义了一个 callFunc 函数,它可以根据传入的参数来调用不同的函数,并返回调用结果。

go 复制代码
func callFunc(fun interface{}, args ...interface{}) []interface{} {
    fv := reflect.ValueOf(fun)
    if fv.Kind() != reflect.Func {
        fmt.Println("被调用的不是函数")
        return []interface{}{}
    }
    // 传入的参数个数
    inlen := len(args)
    // 被调用函数的参数个数
    funptLen := fv.Type().NumIn()
    // 检查传入参数的个数是否正确
    if inlen != funptLen {
        fmt.Println("传入的参数个数不正确")
        return []interface{}{}
    }
    // 检查参数类型是否正确
    for i := 0; i < inlen; i++ {
        ti := reflect.TypeOf(args[i])
        tfi := fv.Type().In(i)
        if ti.Kind() != tfi.Kind() {
            fmt.Println("参数类型不正确")
            return []interface{}{}
        }
    }
    // 调用目标函数
    var prts = make([]reflect.Value, inlen)
    for i := 0; i < inlen; i++ {
        prts[i] = reflect.ValueOf(args[i])
    }
    var res = fv.Call(prts)
    // 提取返回值
    outlen := len(res)
    if outlen == 0 {
        return []interface{}{}
    }
    var outs = make([]interface{}, outlen)
    for i := 0; i < outlen; i++ {
        outs[i] = res[i].Interface()
    }
    return outs
}

定义 addsub 函数,稍后可以通过 callFunc 函数去调用。

go 复制代码
func add(m, n int32) int32 {
    return m + n
}

func sub(p, q int32) int32 {
    return p - q
}

callFunc 函数调用 addsub 函数。

go 复制代码
var a1, a2 int32 = 15, 17
var r1 = callFunc(add, a1, a2)
fmt.Printf("add(%v, %v) => %v\n", a1, a2, r1)

a1, a2 = 30, 12
var r2 = callFunc(sub, a1, a2)
fmt.Printf("sub(%v, %v) => %v\n", a1, a2, r2)

调用成功后,屏幕将输出以下内容:

复制代码
add(15, 17) => [32]
sub(30, 12) => [18]

12.3.5 调用方法

运用反射调用方法的过程与调用函数接近,都会用到 Value.Call 方法。与调用函数相比,反射调用方法多了一个步骤------先获取与方法关联的 Value 对象。具体而言,就是:

  1. 获取对象实例的 Value
  2. 使用 MethodMethodByName 方法查找出对象指定方法的 Value
  3. 再调用与方法关联的 Value.Call 方法。

下面通过一个示例来做演示。

步骤 1:定义 Student 结构体,其中有四个字段。
go 复制代码
type Student struct {
    id uint
    name, city string
    course string
}
步骤 2:为 Student 结构体定义方法,用于读取和修改字段。
go 复制代码
// 读写 id 字段
func (x Student) GetID() uint {
    return x.id
}
func (x *Student) SetID(id uint) {
    x.id = id
}

// 读写 name 字段
func (x Student) GetName() string {
    return x.name
}
func (x *Student) SetName(name string) {
    x.name = name
}

// 读写 city 字段
func (x Student) GetCity() string {
    return x.city
}
func (x *Student) SetCity(city string) {
    x.city = city
}

// 读写 course 字段
func (x Student) GetCourse() string {
    return x.course
}
func (x *Student) SetCourse(course string) {
    x.course = course
}
步骤 3:定义变量 obj,类型为 Student
go 复制代码
var obj Student
步骤 4:使用反射调用 obj 变量的 SetXXX 方法,以修改各个字段的值。
go 复制代码
valOfObj := reflect.ValueOf(&obj)
// 调用 SetID 方法
m := valOfObj.MethodByName("SetID")
p := reflect.ValueOf(uint(187005))
m.Call([]reflect.Value{p})
// 调用 SetName 方法
m = valOfObj.MethodByName("SetName")
p = reflect.ValueOf("小刘")
m.Call([]reflect.Value{p})
// 调用 SetCity 方法
m = valOfObj.MethodByName("SetCity")
p = reflect.ValueOf("珠海")
m.Call([]reflect.Value{p})
// 调用 SetCourse 方法
m = valOfObj.MethodByName("SetCourse")
p = reflect.ValueOf("C++")
m.Call([]reflect.Value{p})

注意,在调用 ValueOf 函数获取 objValue 对象时,应该传递指针类型(即 *Student),因为 SetXXX 方法在定义时指定对象的"接收者"为 *Student。如果不使用指针类型,Value 对象的 MethodByName 方法将找不到 SetXXX 方法。

步骤 5:最后输出 obj 变量各字段的值,以验证修改操作是否成功。
go 复制代码
fmt.Println("使用反射设置字段的值后:")
fmt.Printf("id: %v\n", obj.GetID())
fmt.Printf("name: %v\n", obj.GetName())
fmt.Printf("city: %v\n", obj.GetCity())
fmt.Printf("course: %v\n", obj.GetCourse())
步骤 6:运行示例程序,结果如下:
复制代码
使用反射设置字段的值后:
id: 187005
name: 小刘
city: 珠海
course: C++

12.3.6 读写映射类型的元素

映射类型的元素由 key 和元素值组成,所以 Value.SetMapIndex 方法在调用时需提供两个参数。

go 复制代码
func SetMapIndex(key Value, elem Value)

SetMapIndex 方法可以通过反射技术为映射对象设置元素,对应地,MapIndex 方法可以根据给定的 key 返回指定的元素,也可以调用 MapKeys 方法获取映射对象中所有元素的 key

下面的示例演示了 SetMapIndexMapKeysMapIndex 方法的用法。

go 复制代码
// 创建映射实例
var mp = make(map[string]float32)
// 获取相关的 Value 对象
valOfMap := reflect.ValueOf(mp)
// 设置映射元素
valOfMap.SetMapIndex(
    reflect.ValueOf("T1"),
    reflect.ValueOf(float32(0.0071)),
)
valOfMap.SetMapIndex(
    reflect.ValueOf("T2"),
    reflect.ValueOf(float32(9.202)),
)
valOfMap.SetMapIndex(
    reflect.ValueOf("T3"),
    reflect.ValueOf(float32(-0.03)),
)
// 获取 key 集合
var keys = valOfMap.MapKeys()
// 输出映射中的元素列表
for _, k := range keys {
    v := valOfMap.MapIndex(k)
    fmt.Printf("%v - %v\n", k.Interface(), v.Interface())
}

上述代码首先定义了 mp 变量,调用 make 函数生成一个映射实例,元素的 keystring 类型,元素值为 float32 类型,随后使用反射为其设置三个元素,最后将元素列表读出并输出到屏幕上。输出内容如下:

复制代码
T3 - -0.03
T1 - 0.0071
T2 - 9.202
相关推荐
知远同学9 分钟前
Docker学习笔记-docker安装、删除
笔记·学习·docker
东风西巷10 分钟前
手机上的PDF精简版:随时随地享受阅读
学习·智能手机·pdf·软件需求
摆烂能手15 分钟前
C++基础精讲-06
开发语言·c++
聪明的墨菲特i24 分钟前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年25 分钟前
Android 副屏录制方案
android·前端
Bl_a_ck27 分钟前
【C++基础】GNU简介
开发语言·c++·gnu
拉不动的猪32 分钟前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年35 分钟前
Android 局域网NIO案例实践
android·前端
虾球xz39 分钟前
游戏引擎学习第228天
c++·学习·游戏引擎
追逐时光者40 分钟前
C#/.NET/.NET Core拾遗补漏合集(25年4月更新)
后端·.net