Go 語(yǔ)言 通過(guò)reflect.Value修改值

2023-03-14 17:00 更新

原文鏈接:https://gopl-zh.github.io/ch12/ch12-05.html


12.5. 通過(guò)reflect.Value修改值

到目前為止,反射還只是程序中變量的另一種讀取方式。然而,在本節(jié)中我們將重點(diǎn)討論如何通過(guò)反射機(jī)制來(lái)修改變量。

回想一下,Go語(yǔ)言中類似x、x.f[1]和*p形式的表達(dá)式都可以表示變量,但是其它如x + 1和f(2)則不是變量。一個(gè)變量就是一個(gè)可尋址的內(nèi)存空間,里面存儲(chǔ)了一個(gè)值,并且存儲(chǔ)的值可以通過(guò)內(nèi)存地址來(lái)更新。

對(duì)于reflect.Values也有類似的區(qū)別。有一些reflect.Values是可取地址的;其它一些則不可以??紤]以下的聲明語(yǔ)句:

x := 2                   // value   type    variable?
a := reflect.ValueOf(2)  // 2       int     no
b := reflect.ValueOf(x)  // 2       int     no
c := reflect.ValueOf(&x) // &x      *int    no
d := c.Elem()            // 2       int     yes (x)

其中a對(duì)應(yīng)的變量不可取地址。因?yàn)閍中的值僅僅是整數(shù)2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址,它只是一個(gè)指針&x的拷貝。實(shí)際上,所有通過(guò)reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是對(duì)于d,它是c的解引用方式生成的,指向另一個(gè)變量,因此是可取地址的。我們可以通過(guò)調(diào)用reflect.ValueOf(&x).Elem(),來(lái)獲取任意變量x對(duì)應(yīng)的可取地址的Value。

我們可以通過(guò)調(diào)用reflect.Value的CanAddr方法來(lái)判斷其是否可以被取地址:

fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

每當(dāng)我們通過(guò)指針間接地獲取的reflect.Value都是可取地址的,即使開始的是一個(gè)不可取地址的Value。在反射機(jī)制中,所有關(guān)于是否支持取地址的規(guī)則都是類似的。例如,slice的索引表達(dá)式e[i]將隱式地包含一個(gè)指針,它就是可取地址的,即使開始的e表達(dá)式不支持也沒(méi)有關(guān)系。以此類推,reflect.ValueOf(e).Index(i)對(duì)應(yīng)的值也是可取地址的,即使原始的reflect.ValueOf(e)不支持也沒(méi)有關(guān)系。

要從變量對(duì)應(yīng)的可取地址的reflect.Value來(lái)訪問(wèn)變量需要三個(gè)步驟。第一步是調(diào)用Addr()方法,它返回一個(gè)Value,里面保存了指向變量的指針。然后是在Value上調(diào)用Interface()方法,也就是返回一個(gè)interface{},里面包含指向變量的指針。最后,如果我們知道變量的類型,我們可以使用類型的斷言機(jī)制將得到的interface{}類型的接口強(qiáng)制轉(zhuǎn)為普通的類型指針。這樣我們就可以通過(guò)這個(gè)普通指針來(lái)更新變量了:

x := 2
d := reflect.ValueOf(&x).Elem()   // d refers to the variable x
px := d.Addr().Interface().(*int) // px := &x
*px = 3                           // x = 3
fmt.Println(x)                    // "3"

或者,不使用指針,而是通過(guò)調(diào)用可取地址的reflect.Value的reflect.Value.Set方法來(lái)更新對(duì)應(yīng)的值:

d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"

Set方法將在運(yùn)行時(shí)執(zhí)行和編譯時(shí)進(jìn)行類似的可賦值性約束的檢查。以上代碼,變量和值都是int類型,但是如果變量是int64類型,那么程序?qū)伋鲆粋€(gè)panic異常,所以關(guān)鍵問(wèn)題是要確保改類型的變量可以接受對(duì)應(yīng)的值:

d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int

同樣,對(duì)一個(gè)不可取地址的reflect.Value調(diào)用Set方法也會(huì)導(dǎo)致panic異常:

x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value

這里有很多用于基本數(shù)據(jù)類型的Set方法:SetInt、SetUint、SetString和SetFloat等。

d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // "3"

從某種程度上說(shuō),這些Set方法總是盡可能地完成任務(wù)。以SetInt為例,只要變量是某種類型的有符號(hào)整數(shù)就可以工作,即使是一些命名的類型、甚至只要底層數(shù)據(jù)類型是有符號(hào)整數(shù)就可以,而且如果對(duì)于變量類型值太大的話會(huì)被自動(dòng)截?cái)?。但需要?jǐn)慎的是:對(duì)于一個(gè)引用interface{}類型的reflect.Value調(diào)用SetInt會(huì)導(dǎo)致panic異常,即使那個(gè)interface{}變量對(duì)于整數(shù)類型也不行。

x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2)                     // OK, x = 2
rx.Set(reflect.ValueOf(3))       // OK, x = 3
rx.SetString("hello")            // panic: string is not assignable to int
rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int

var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2)                     // panic: SetInt called on interface Value
ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
ry.SetString("hello")            // panic: SetString called on interface Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"

當(dāng)我們用Display顯示os.Stdout結(jié)構(gòu)時(shí),我們發(fā)現(xiàn)反射可以越過(guò)Go語(yǔ)言的導(dǎo)出規(guī)則的限制讀取結(jié)構(gòu)體中未導(dǎo)出的成員,比如在類Unix系統(tǒng)上os.File結(jié)構(gòu)體中的fd int成員。然而,利用反射機(jī)制并不能修改這些未導(dǎo)出的成員:

stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
fmt.Println(stdout.Type())                  // "os.File"
fd := stdout.FieldByName("fd")
fmt.Println(fd.Int()) // "1"
fd.SetInt(2)          // panic: unexported field

一個(gè)可取地址的reflect.Value會(huì)記錄一個(gè)結(jié)構(gòu)體成員是否是未導(dǎo)出成員,如果是的話則拒絕修改操作。因此,CanAddr方法并不能正確反映一個(gè)變量是否是可以被修改的。另一個(gè)相關(guān)的方法CanSet是用于檢查對(duì)應(yīng)的reflect.Value是否是可取地址并可被修改的:

fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)