gopl-zh.github.com/ch12/ch12-05.md

104 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 12.5. 通過reflect.Value脩改值
到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。
迴想一下Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。
對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的其它一些則不可以。考慮以下的聲明語句
```Go
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對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址它隻是一個指針`&x`的拷貝。實際上所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d它是c的解引用方式生成的指向另一個變量因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem()來獲取任意變量x對應的可取地址的Value。
我們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址
```Go
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
```
每當我們通過指針間接地獲取的reflect.Value都是可取地址的卽使開始的是一個不可取地址的Value。在反射機製中所有關於是否支持取地址的規則都是類似的。例如slice的索引表達式e[i]將隱式地包含一個指針它就是可取地址的卽使開始的e表達式不支持也沒有關繫。以此類推reflect.ValueOf(e).Index(i)對於的值也是可取地址的卽使原始的reflect.ValueOf(e)不支持也沒有關繫。
要從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法它返迴一個Value里面保存了指向變量的指針。然後是在Value上調用Interface()方法也就是返迴一個interface{}里面通用包含指向變量的指針。最後如果我們知道變量的類型我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了:
```Go
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"
```
或者不使用指針而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值
```Go
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"
```
Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼變量和值都是int類型但是如果變量是int64類型那麽程序將拋出一個panic異常所以關鍵問題是要確保改類型的變量可以接受對應的值
```Go
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
```
通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常
```Go
x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
```
這里有很多用於基本數據類型的Set方法SetInt、SetUint、SetString和SetFloat等。
```Go
d := reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // "3"
```
從某種程度上説這些Set方法總是盡可能地完成任務。以SetInt爲例隻要變量是某種類型的有符號整數就可以工作卽使是一些命名的類型隻要底層數據類型是有符號整數就可以而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常卽使那個interface{}變量對於整數類型也不行。
```Go
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"
```
當我們用Display顯示os.Stdout結構時我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員比如在類Unix繫統上os.File結構體中的fd int成員。然而利用反射機製併不能脩改這些未導出的成員
```Go
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
```
一個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員如果是的話則拒絶脩改操作。因此CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的
```Go
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
```