【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欄位的一個標誌位),而這個屬性具有以下規則:
- 切片中的元素;
addressable
的陣列中的元素;addressable
的結構體的欄位;- 對指標進行解引用得到的結果;
其中第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.New(ptrValue.Type()).Elem())
assert.Equal(t, (*int)(nil), ptr)
}
目前沒有找到什麼好的辦法,只能先new一個指標,因為新建立的指標是預設初始化的,即為nil指標。注意reflect.New()
傳入的是指標的Type,返回的是指向該指標的指標的Value結構體,所以需要進行一次解引用。
反射修改陣列
逐元素修改
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()
的兩個要素:
- 該Value結構體是否
addressable
; - 如果該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