【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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM