【Go反射】修改对象

前言

最近在写一个自动配置的库cfgm,其中序列化和反序列化的过程用到了大量反射,主要部分写完之后,我在这里回顾总结一下反射的基本操作。

上一篇【Go反射】读取对象中总结了利用反射读取对象的方法。

本篇总结一下写入操作,即对简单类型(int、uint、float、bool、string)、指针、切片、数组、map、结构体的修改操作,后记中讨论了.CanSet()的设计思想。

先声明一下后续代码中需要引入的包:

import (
	"github.com/stretchr/testify/assert"
	"reflect"
	"testing"
)

参考

目录

基础知识

CanSet() 和 CanAddr()

我们通过反射修改一个对象,通常会使用到Value结构体的SetXxx()方法,而这些方法往往需要该Value结构体.CanSet()为true。

通过查看文档,我们知道.CanSet()为true的条件为:

CanSet reports whether the value of v can be changed. A Value can be changed only if it is addressable and was not obtained by the use of unexported struct fields. If CanSet returns false, calling Set or any type-specific setter (e.g., SetBool, SetInt) will panic.

即大部分情况下.CanAddr()为true即可,如果该Value是一个结构体的字段,则还需要满足该字段是可访问的(字段名首字母大写)。

.CanAddr()为true的条件在文档中是这样描述的:

CanAddr reports whether the value's address can be obtained with Addr. Such values are called addressable. A value is addressable if it is an element of a slice, an element of an addressable array, a field of an addressable struct, or the result of dereferencing a pointer. If CanAddr returns false, calling Addr will panic.

.CanAddr()描述了一个叫addressable的属性,这个属性描述的是Value结构体的性质(本质上是Value结构体的flag字段的一个标志位),而这个属性具有以下规则:

  1. 切片中的元素;
  2. addressable的数组中的元素;
  3. addressable的结构体的字段;
  4. 对指针进行解引用得到的结果;

其中第2、3条规则是addressable的传播规则,而第1、4条规则是它的产生规则。

当然,有一些特殊情况,我们在修改对象的时候,是不需要满足.CanSet()==true的,我目前已知的特例是map的.SetMapIndex()

关于如何理解.CanSet()的设计,在后记中进行探讨。

反射修改简单对象

通过Setter方法

func TestSetInt(t *testing.T) {
	var integer int = 1
	value := reflect.ValueOf(&integer).Elem()

	value.SetInt(234)
	assert.Equal(t, 234, integer)
}

这里我们通过先取指针再解引用的方式,来获得了一个addressable的Value结构体value,然后调用int对应的Setter方法.SetInt()来写入新值。

类似于我们读取时使用的.Int().String()等方法,Value结构体也提供了对应的Setter:

Kind 方法
Int、Intxx SetInt() int64
Uint、Uintxx SetUint() uint64
String SetString() string
Float32、Float64 SetFloat() float64
Bool SetBool() bool

直接调用Set()方法

func TestSetInt_Raw(t *testing.T) {
	var origin int = 1
	var target int = 234
	valueOrigin := reflect.ValueOf(&origin).Elem()
	valueTarget := reflect.ValueOf(target)

	valueOrigin.Set(valueTarget)
	assert.Equal(t, 234, origin)
	origin = 567
	assert.Equal(t, 234, target)
}

这种方式要求获得一个目标值的Value结构体(这个结构体不要求.CanAddr()),目标值的Value必须拥有和旧值相同的Type(不是Kind)。

不过对于简单类型,当其Kind相同的时候,可以进行转换(更广泛的转换规则,在此不赘述,以后有空再研究研究):

func TestSetInt_WrongType(t *testing.T) {
	type MyInteger int
	var origin int = 1
	var target MyInteger = 234
	valueOrigin := reflect.ValueOf(&origin).Elem()
	valueTarget := reflect.ValueOf(target)

	// BOOM! panic: reflect.Set: value of type experiment.MyInteger is not assignable to type int
	// valueOrigin.Set(valueTarget)
	valueOrigin.Set(valueTarget.Convert(valueOrigin.Type()))
	assert.Equal(t, 234, origin)
}

事实上,.Set()方法似乎是很多情况下我们修改值唯一的选择(如指针),不过对于简单对象,使用Setter不仅更简单,而且性能略微微微微高一小些(不用检查传入的值)。

反射修改指针

将指针指向另一个对象

func TestSetPtr(t *testing.T) {
	var integer int = 1
	ptr := &integer
	ptrValue := reflect.ValueOf(&ptr).Elem()
	var target int = 2
	targetValue := reflect.ValueOf(&target).Elem()

	ptrValue.Set(targetValue.Addr())
	assert.Equal(t, 2, *ptr)
	assert.Equal(t, &target, ptr)
	assert.NotEqual(t, &integer, ptr)
}

通过对一个addressable的Value结构体调用.Addr()方法创建一个指向该对象的指针的Value结构体,然后将其通过.Set()赋值给目标指针即可。

修改指针指向的对象的值

func TestSetPtr_ChangeTarget(t *testing.T) {
	var integer int = 1
	ptr := &integer
	ptrValue := reflect.ValueOf(&ptr).Elem()

	ptrValue.Elem().SetInt(2)
	assert.Equal(t, 2, integer)
}

指针指向的对象还是原来的对象,但是这个对象的值发生了改变。

由于指针解引用获得的Value结构体是addressable的,所以直接对其进行修改即可。

将指针置为nil

func TestSetPtr_Nil(t *testing.T) {
	var integer int = 1
	ptr := &integer
	ptrValue := reflect.ValueOf(&ptr).Elem()

	ptrValue.Set(reflect.Zero(ptrValue.Type()))
	assert.Equal(t, (*int)(nil), ptr)
}

reflect.Zero()创建一个给定Type的零值的反射对象(Value结构体),即默认值。注意创建的这个反射对象不是addressable的,但是通过.Set()将其赋值给一个addressable的反射对象后,这个反射对象仍旧是addressable的。

反射修改数组

逐元素修改

func TestSetArray_Elem(t *testing.T) {
	array := [...]int{1, 2, 3}
	arrayValue := reflect.ValueOf(&array).Elem()

	length := arrayValue.Len()
	for i := 0; i < length; i++ {
		elemValue := arrayValue.Index(i)
		elemValue.SetInt(int64(i + 4))
	}
	assert.Equal(t, [...]int{4, 5, 6}, array)
}

通过前面,我们知道只要数组是addressable的,那么其中的元素也将是addressable的,所以我们仍要通过取地址再解引用来让数组为addressable的。

之后我们可以直接对其中的元素进行修改。

整体修改

func TestSetArray_All(t *testing.T) {
	origin := [...]int{1, 2, 3}
	target := [...]int{4, 5, 6}	// 必须长度相同
	originValue := reflect.ValueOf(&origin).Elem()
	targetValue := reflect.ValueOf(target)

	originValue.Set(targetValue)
	assert.Equal(t, [...]int{4, 5, 6}, origin)
	target[2] = 7
	assert.Equal(t, 6, origin[2])
}

整体修改时,注意两个数组的长度必须相同。

整体修改是对整个数组进行值拷贝,整体修改完成后,对其中一个的某元素进行修改,另一个不会随之修改(这与下文的切片是不一样的)。

反射修改切片

逐元素修改

func TestSetSlice_Elem(t *testing.T) {
	slice := []int{1, 2, 3}
	sliceValue := reflect.ValueOf(slice)
	// sliceValue := reflect.ValueOf(&slice).Elem()

	length := sliceValue.Len()
	for i := 0; i < length; i++ {
		elemValue := sliceValue.Index(i)
		elemValue.SetInt(int64(i + 4))
	}
	assert.Equal(t, []int{4, 5, 6}, slice)
}

跟数组逐元素修改类似。不过由于切片中的元素无条件为addressable的,所以我们逐元素修改时不必像数组那样先取地址再解引用(不过其它情况仍旧需要这样做)。

整体修改

func TestSetSlice_All(t *testing.T) {
	origin := []int{1, 2, 3}
	target := []int{4, 5, 6, 7}
	originValue := reflect.ValueOf(&origin).Elem()
	targetValue := reflect.ValueOf(target)

	originValue.Set(targetValue)
	assert.Equal(t, []int{4, 5, 6, 7}, origin)
	target[3] = 8
	assert.Equal(t, 8, origin[3])
}

整体修改时,因为我们修改的是切片的描述结构体,而不是切片内的元素,所以有以下现象:

  • 需要先取地址再解引用;
  • 赋值与被赋值的切片长度可以不一致;
  • 赋值与被赋值的切片共享同一个底层数组,当通过一个切片修改了某个元素后,另一个切片也可能会观测到这次修改;

修改len和cap

func TestSetSlice_LenAndCap(t *testing.T) {
	slice := []int{1, 2, 3, 4}
	sliceValue := reflect.ValueOf(&slice).Elem()

	sliceValue.SetLen(3)
	assert.Equal(t, []int{1, 2, 3}, slice)
	assert.Equal(t, 4, cap(slice))
	// BOOM! panic: reflect: slice capacity out of range in SetCap
	// sliceValue.SetCap(2)
	// sliceValue.SetCap(5)
	sliceValue.SetCap(3)
	assert.Equal(t, 3, cap(slice))
}

通过.SetLen().SetCap()可以修改切片的len和cap,注意需要满足\(Len_{new} \leq Cap_{new} \leq Cap_{old}\)\(Len_{new} \leq Len_{old}\)

接下来两种方法本质是创建新的切片(.CanAddr()皆为false),不过我们一般将其视作修改切片的手段,所以这里还是将其纳入进来(将来研究反射创建对象的时候可能又要再看一遍)。

从数组、切片创建切片

再非反射中,我们可以通过数组、切片来创建新的切片:

func TestCreatNewSlice(t *testing.T) {
	array := [...]int{1, 2, 3, 4, 5}
	slice1 := array[1:4]
	slice2 := slice1[0:2:3]
	assert.Equal(t, []int{2, 3, 4}, slice1)
	assert.Equal(t, []int{2, 3}, slice2)
	assert.Equal(t, 3, cap(slice2))
}

Value结构体提供了.Slice().Slice3()来完成这种操作:

func TestSetSlice_FromArray(t *testing.T) {
	array := [...]int{1, 2, 3, 4}
	var slice []int
	sliceValue := reflect.ValueOf(&slice).Elem()
	arrayValue := reflect.ValueOf(&array).Elem() // arrayValue must be addressable

	sliceValue.Set(arrayValue.Slice(1, 3))
	assert.Equal(t, []int{2, 3}, slice)
	array[1] = 5
	assert.Equal(t, 5, slice[0])
}

func TestSetSlice_FromSlice(t *testing.T) {
	slice := []int{1, 2, 3, 4}
	sliceValue := reflect.ValueOf(&slice).Elem()

	sliceValue.Set(sliceValue.Slice3(1, 3, 3))
	assert.Equal(t, []int{2, 3}, slice)
	assert.Equal(t, 2, cap(slice))
}

.Slice().Slice3()只能对切片和addressable的数组使用,其实质是创建了一个新的切片。通过.Set()方法,我们可以将新创建的切片赋值给目标切片。

append

func TestSetSlice_Append(t *testing.T) {
	slice := []int{1, 2, 3}
	sliceValue := reflect.ValueOf(&slice).Elem()
	elemValue := reflect.ValueOf(int(4))

	sliceValue.Set(reflect.Append(sliceValue, elemValue))
	assert.Equal(t, []int{1, 2, 3, 4}, slice)
}

使用起来和内置函数append()十分类似。

反射修改结构体

查找字段并修改

func TestSetStruct_Field(t *testing.T) {
	type NameStruct struct {
		Name string
	}
	type MyStruct struct {
		NameStruct
		Age int
		NickName NameStruct
		secretName string
	}
	myStruct := MyStruct{NameStruct{"abc"}, 123, NameStruct{"def"}, "ghi"}
	structValue := reflect.ValueOf(&myStruct).Elem()
	structValue.FieldByName("Name").SetString("name")
	structValue.FieldByName("Age").SetInt(35)
	structValue.FieldByName("NickName").FieldByName("Name").SetString("nick")
	// BOOM! panic: reflect: reflect.Value.SetString using value obtained using unexported field
	// structValue.FieldByName("secretName").SetString("secret")
	expect := MyStruct{NameStruct{"name"}, 35, NameStruct{"nick"}, "ghi"}
	assert.Equal(t, expect, myStruct)
}

通过上一篇中介绍的.Field(i)或者.FieldByName()方法获得字段的Value结构体,然后调用其.Set()方法或者Setter方法进行修改即可。

需要注意的是,私有字段(首字母小写)不可以通过反射直接修改,但可以通过一些手段来修改:

修改私有字段

func TestSetStruct_PrivateField(t *testing.T) {
	type MyStruct struct {
		privateField string
	}
	myStruct := MyStruct{"I'm private!"}
	targetStr := "No! I can access you!"
	structValue := reflect.ValueOf(&myStruct).Elem()
	privateField, ok := structValue.Type().FieldByName("privateField")
	assert.True(t, ok)
	*(*string)(unsafe.Pointer(structValue.UnsafeAddr() + privateField.Offset)) = targetStr
	assert.Equal(t, MyStruct{targetStr}, myStruct)
}

本质是强行计算该字段的地址,然后修改该地址上的值。

反射修改map

由前面对于addressable的定义,我们知道map的子对象(所有key和value)都不是addressable的,所以没法像前面几种类型那样获取子对象的Value结构体,然后对其进行修改,似乎只能通过.SetMapIndex()来设置新值,我暂时没有找到可以直接修改的方法。

逐对修改

func TestSetMap_Elem(t *testing.T) {
	dict := map[int]int {1: 1, 2: 2, 3: 3}
	dictValue := reflect.ValueOf(dict)
	// dictValue := reflect.ValueOf(&dict).Elem()

	iter := dictValue.MapRange()
	for iter.Next() {
		key := iter.Key()
		value := iter.Value()
		dictValue.SetMapIndex(key, reflect.ValueOf(int(value.Int()) + 1))
	}
	target := map[int]int{1: 2, 2: 3, 3: 4}
	assert.Equal(t, target, dict)
}

这里因为.SetMapIndex()传入的key永远是在map中的,所以不会修改map的键值对个数,所以不会导致迭代器失效,所以直接只用.MapRange()进行遍历。

添加键值对

func TestSetMap_AddElem(t *testing.T) {
	dict := map[int]int{1: 1, 2: 2, 3: 3}
	dictValue := reflect.ValueOf(dict)
	// dictValue := reflect.ValueOf(&dict).Elem()

	keys := dictValue.MapKeys()
	for _, key := range keys {
		dictValue.SetMapIndex(reflect.ValueOf(int(key.Int())+3), reflect.ValueOf(int(4)))
	}
	target := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5: 4, 6: 4}
	assert.Equal(t, target, dict)
}

当传入.SetMapIndex()的key不在map中时,将插入新的键值对,此时有可能触发扩容。注意这里不宜使用.MapRange()进行遍历,因为会向map添加新元素,执行结果不确定。

删除键值对

func TestSetMap_DeleteElem(t *testing.T) {
	dict := map[int]int {1: 1, 2: 2, 3: 3, 4: 4, 5: 4, 6: 4}
	dictValue := reflect.ValueOf(dict)
	// dictValue := reflect.ValueOf(&dict).Elem()

	keys := dictValue.MapKeys()
	for _, key := range keys {
		if key.Int() % 2 == 0 {
			dictValue.SetMapIndex(key, reflect.Value{})
		}
	}
	target := map[int]int{1: 1, 3: 3, 5: 4}
	assert.Equal(t, target, dict)
}

当传入.SetMapIndex()的key在map中,且value为空的Value结构体,将删除该键值对。

总结SetMapIndex()

key在map中 key不在map中
value为空 删除该键值对 删除该键值对(实际上不会修改任何键值对,但是如果map扩容未完成会执行growWork()
value不为空 修改map中该键对应的值 为map添加新的键值对,可能触发扩容

总结

本文介绍了利用反射进行写入(修改)的操作,即对简单类型(int、uint、float、bool、string)和复杂类型(指针、切片、数组、map、结构体)的修改操作。

转载请注明原文地址:https://www.cnblogs.com/SnowPhoenix/p/15695730.html





后记

为什么要设计CanSet()?

按照《The Laws of Reflection》中的说法,CanSet()的设计是为了让反射的行为和非反射的情况下一致。

再来回顾一下决定CanSet()的两个要素:

  1. 该Value结构体是否addressable
  2. 如果该Value是一个结构体的字段,则还需要满足该字段是否是可访问的(字段名首字母大写);

关于第二个要素的设计,按照让反射的行为和非反射的情况下一致的设计原则是容易解释的通的(虽然我们可能更希望反射能够提供绕过这层限制的能力),但是第一个要素要怎么解释呢?

addressable的四条规则的合理性

我们先来看一个示例:

func TestInterfaceCopy(t *testing.T) {
	produce := func(i interface{}) {
		switch v := i.(type) {
		case int:
			v += 1
		case *int:
			*v += 1
		}
	}

	integer := 1
	produce(integer)
	assert.Equal(t, 1, integer)
	produce(&integer)
	assert.Equal(t, 2, integer)
}

通过示例我们看到,当一个对象绑定到一个interface{}时,实际上会对该对象进行一次复制。我们将integer直接绑定到interface{}上后,对interface{}进行的修改,实际上并不会影响原先的integer

再来看反射中起始的函数reflect.ValueOf(),它接受的参数恰好就是一个interface{},此时我们对返回的Value结构体进行操作很有可能并不会影响到原对象!

而当我们将一个指针绑定到一个interface{}时,对interface{}解引用后的修改,就可以在指针指向的对象上生效。这也是为什么addressable的规则中会规定解引用获得的Value结构体是addressable的。

同理,我们也就可以理解另外三条规则是怎么来的了。

再来个例子:

func TestInterfaceChangePart(t *testing.T) {
	type MyStruct struct {
		Name string
		Age *int
		Tools []string
	}
	produce := func(i interface{}) {
		v, ok := i.(MyStruct)
		assert.True(t, ok)
		v.Name = "new"	// useless work
		*v.Age = 2
		v.Tools[1] = "knife"
		v.Tools = append(v.Tools, "fork") // useless work
	}
	integer1 := 1
	obj := MyStruct{
		Name: "origin",
		Age: &integer1,
		Tools: []string{"shovel", "pan"},
	}
	produce(obj)
	assert.Equal(t, "origin", obj.Name)
	assert.Equal(t, 2, *obj.Age)
	assert.Equal(t, []string{"shovel", "knife"}, obj.Tools)
}

在produce中对传入的interface{}进行了一系列修改,而其中的一些修改其实在退出函数后并不会影响传入的obj,而在反射中,尝试进行这些“无用”的修改就会因为.CanSet()false而panic。

我们可以归纳出.CanAddr()true的一个必要条件:

对该Value结构体的操作能够被外部观测到

为什么map的键和值都不是addressable的?

按照我C++的经验,不允许修改key是可以理解的,因为key关系到hash,关系到这个键值对被放到哪个bucket中,不应当被修改。

但是为什么不允许反射来修改value的值呢?难道说——其实Go根本就不允许修改value?

然后我就进行了一下尝试:

func TestChangeMap(t *testing.T) {
	type MyStruct struct {
		Name string
		Age int
	}
	dict := map[int]MyStruct {1: {"A", 1}, 2: {"B", 2}}
	// Compile Error! cannot assign to struct field dict[1].Name in map
	// dict[1].Name = "C"
	_ = dict
}

func TestChangeMapPtr(t *testing.T) {
	type MyStruct struct {
		Name string
		Age int
	}
	dict := map[int]*MyStruct {1: {"A", 1}, 2: {"B", 2}}
	dict[1].Name = "C"
	assert.Equal(t, "C", dict[1].Name)
}

果然Go语言根本就不允许直接修改value,并不是仅仅不允许通过反射修改value。我猜Go之所以不允许修改value,跟Go的map的扩容机制有关系(并不是立即扩容,而是将扩容操作分摊到map的其它操作中)。

那么map的值不是addressable也可以理解了,因为不反射也无法修改map中的value。

再结合不允许反射直接修改(虽然可以hack)结构体的私有字段的设计,我们就得出了.CanSet()true的另一个必要条件:

不反射也可以完成对该对象的修改操作

后记的总结

正如《The Laws of Reflection》中所说,反射对象(Value结构体)和interface{}是息息相关的,反射的作用就是为操作interface{}提供工具。

.CanSet()的设计,就是试探对反射对象修改的有效性合法性,当对反射对象的修改有意义且合法时,修改操作才会被允许。

转载请注明原文地址:https://www.cnblogs.com/SnowPhoenix/p/15695730.html

posted @ 2021-12-15 22:42  SnowPhoenix  阅读(1067)  评论(0编辑  收藏  举报