Golang-interface(四 反射)
一 反射的規則
反射是程式執行時檢查其所擁有的結構,尤其是型別的一種能力;這是超程式設計的一種形式。它同時也是造成混淆的重要來源。
每個語言的反射模型都不同(同時許多語言根本不支援反射)。本節將試圖明確解釋在 Go 中的反射是如何工作的。
1. 從介面值到反射物件的反射
在基本的層面上,反射只是一個檢查儲存在介面變數中的型別和值的演算法。在 reflect 包中有兩個型別需要了解:Type 和 Value。這兩個型別使得可以訪問介面變數的內容,還有兩個簡單的函式,reflect.TypeOf 和 reflect.ValueOf,從介面值中分別獲取 reflect.Type 和 reflect.Value。
從 TypeOf 開始:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
這個程式列印 type: float64
介面在哪裡呢,讀者可能會對此有疑慮,看起來程式傳遞了一個 float64 型別的變數 x,而不是一個介面值,到 reflect.TypeOf。但是,它確實就在那裡:如同 godoc 報告的那樣,reflect.TypeOf 的宣告包含了空介面:
// TypeOf 返回 interface{} 中的值反射的型別。
func TypeOf(i interface{}) Type
當呼叫 reflect.TypeOf(x) 的時候,x 首先儲存於一個作為引數傳遞的空介面中;reflect.TypeOf 解包這個空介面來還原型別資訊。
reflect.ValueOf 函式,當然就是還原那個值(從這裡開始將會略過那些概念示例,而聚焦於可執行的程式碼):
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
列印
value: <float64 Value>
除了reflect.Type 和 reflect.Value外,都有許多方法用於檢查和操作它們。一個重要的例子是 Value 有一個 Type 方法返回 reflect.Value 的 Type。另一個是 Type 和 Value 都有 Kind 方法返回一個常量來表示型別:Uint、Float64、Slice 等等。同樣 Value 有叫做 Int 和 Float 的方法可以獲取儲存在內部的值(跟 int64 和 float64 一樣):
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
列印
type: float64
kind is float64: true
value: 3.4
同時也有類似 SetInt 和 SetFloat 的方法,不過在使用它們之前需要理解可設定性,這部分的主題在下面的第三條軍規中討論。
反射庫有著若干特性值得特別說明。
-
為了保持 API 的簡潔,“獲取者”和“設定者”用 Value 的最寬泛的型別來處理值:例如,int64 可用於所有帶符號整數。也就是說 Value 的 Int 方法返回一個 int64,而 SetInt 值接受一個 int64;所以可能必須轉換到實際的型別:
var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) // uint8. fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true. x = uint8(v.Uint()) // v.Uint 返回一個 uint64.
-
反射物件的 Kind 描述了底層型別,而不是靜態型別。如果一個反射物件包含了使用者定義的整數型別的值,就像
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)‘
v 的 Kind 仍然是 reflect.Int,儘管 x 的靜態型別是 MyInt,而不是 int。換句話說,Kind 無法從 MyInt 中區分 int,而 Type 可以。
2. 從反射物件到介面值的反射
如同物理中的反射,在 Go 中的反射也存在它自己的映象。
從 reflect.Value 可以使用 Interface 方法還原介面值; 此方法可以高效地打包型別和值資訊到介面表達中,並返回這個結果:
// Interface 以 interface{} 返回 v 的值。
func (v Value) Interface() interface{}
可以這樣作為結果
y := v.Interface().(float64) // y 將為型別 float64。
fmt.Println(y)
通過反射物件 v 可以列印 float64 的表達值。
然而,還可以做得更好。fmt.Println
,fmt.Printf
等其他所有傳遞一個空介面值作為引數的函式,在 fmt 包內部解包的方式就像之前的例子這樣。因此正確的列印 reflect.Value 的內容的方法就是將 Interface 方法的結果進行格式化列印(formatted print routine).
fmt.Println(v.Interface())
為什麼不是 fmt.Println(v)?因為 v 是一個 reflect.Value;這裡希望獲得的是它儲存的實際的值。
由於值是 float64,如果需要的話,甚至可以使用浮點格式化:
fmt.Printf("value is %7.1e\n", v.Interface())
輸出: 3.4e+00
再次強調,對於 v.Interface() 無需型別斷言其為 float64;空介面值在內部有實際值的型別資訊,而 Printf 會發現它。
簡單來說,Interface 方法是 ValueOf 函式的映象,除了返回值總是靜態型別 interface{}。
回顧:反射可以從介面值到反射物件,也可以反過來。
3. 為了修改反射物件,其值必須可設定
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
如果執行這個程式碼,它報出神祕的 panic 訊息
panic: reflect.Value.SetFloat using unaddressable value
問題不在於值 7.1 不能地址化;在於 v 不可設定。設定性是反射值的一個屬性,並不是所有的反射值有此特性。
Value的 CanSet 方法提供了值的設定性;在這個例子中,
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:" , v.CanSet())
列印
settability of v: false
對不可設定值呼叫 Set 方法會有錯誤。
但是什麼是設定性?
設定性有一點點像地址化,但是更嚴格。這是用於建立反射物件的時候,能夠修改實際儲存的屬性。設定性用於決定反射物件是否儲存原始專案。當這樣
var x float64 = 3.4
v := reflect.ValueOf(x)
就傳遞了一個 x 的副本到 reflect.ValueOf,所以介面值作為 reflect.ValueOf 引數建立了 x 的副本,而不是 x 本身。因此,如果語句
v.SetFloat(7.1)
允許執行,雖然 v 看起來是從 x 建立的,它也無法更新 x。反之,如果在反射值內部允許更新 x 的副本,那麼 x 本身不會收到影響。這會造成混淆,並且毫無意義,因此這是非法的,而設定性是用於解決這個問題的屬性。
這很神奇?其實不是。這實際上是一個常見的非同尋常的情況。考慮傳遞 x 到函式:
f(x) 由於傳遞的是 x 的值的副本,而不是 x 本身,所以並不期望 f 可以修改 x。如果想要 f 直接修改 x,必須向函式傳遞 x 的地址(也就是,指向 x 的指標):
f(&x) 這是清晰且熟悉的,而反射通過同樣的途徑工作。如果希望通過反射來修改 x,必須向反射庫提供一個希望修改的值的指標。
來試試吧。首先像平常那樣初始化 x,然後建立指向它的反射值,叫做 p。
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:獲取 X 的地址。
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:" , p.CanSet())
這樣輸出為
type of p: *float64
settability of p: false
反射物件 p 並不是可設定的,而且我們也不希望設定 p,實際上是 *p。為了獲得 p 指向的內容,呼叫值上的 Elem 方法,從指標間接指向,然後儲存反射值的結果叫做 v:
v := p.Elem()
fmt.Println("settability of v:" , v.CanSet())
現在 v 是可設定的反射物件,如同示例的輸出,
settability of v: true
而由於它來自 x,最終可以使用 v.SetFloat 來修改 x 的值:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
得到期望的輸出
7.1
7.1
反射可能很難理解,但是語言做了它應該做的,儘管底層的實現被反射的 Type 和 Value 隱藏了。務必記得反射值需要某些內容的地址來修改它指向的東西。
二結構體
在之前的例子中 v 本身不是指標,它只是從一個指標中獲取的。這種情況更加常見的是當使用反射修改結構體的欄位的時候。也就是當有結構體的地址的時候,可以修改它的欄位。
這裡有一個分析結構值 t 的簡單例子。由於希望對結構體進行修改,所以從它的地址建立了反射物件。設定了 typeOfT 為其型別,然後用直白的方法呼叫來遍歷其欄位(參考 reflect 包瞭解更多資訊)。注意從結構型別中解析了欄位名字,但是欄位本身是原始的 reflect.Value 物件。
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
程式輸出:
0: A int = 23
1: B string = skidoo
還有一個關於設定性的要點:T 的欄位名要大寫(可匯出),因為只有可匯出的欄位是可設定的。
由於 s 包含可設定的反射物件,所以可以修改結構體的欄位。
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
這裡是結果:
t is now {77 Sunset Strip}
如果修改程式使得 s 創建於 t,而不是 &t,呼叫 SetInt 和 SetString 會失敗,因為 t 的欄位不可設定。
三 總結
反射的規則如下:
從介面值到反射物件的反射
從反射物件到介面值的反射
為了修改反射物件,其值必須可設定
一旦理解了 Go 中的反射的這些規則,就會變得容易使用了,雖然它仍然很微妙。這是一個強大的工具,除非真得有必要,否則應當避免使用或小心使用。
還有大量的關於反射的內容沒有涉及到——channel 上的傳送和接收、分配記憶體、使用 slice 和 map、呼叫方法和函式。