反射增強了語言的動態描述能力,你要問我什麼是動態,我只能說,所有可能產生意料之外情理之中的變化,都是動態。
概述
反射這個詞並不是特定語言持有的,相反很多語言擁有著自己的反射模型。老實說,我並不喜歡去用專業的術語去解釋一些概念性的東西,這樣往往觀看的人也雲裡霧裡,這些概念性的東西,每個人腦海中都有自己的“解釋語言“,隨他去吧。
我主要想談談為什麼需要反射,應用場景是什麼?其實在我看來,這兩個問題嚴格意義上是等價的,即 為什麼=應用場景,應用場景=為什麼。
go 作為靜態型別語言,如果沒有反射,很多能夠作用於 “任意型別” 的函式,實現起來會很麻煩。我舉一個最簡單的場景:
package main
// 會員
type member struct {
Name string
Level int
}
// 遊客
type visitor struct {
NickName string
}
func main() {
m := member{
Name: "wqq",
Level: 520,
}
v := visitor{
NickName: "curry",
}
checkSomeTing(m)
checkSomeTing(v)
}
func checkSomeTing(v interface{}) {
if "如果是會員的話" {
// ...
} else {
// ...
}
}
上面 member 和 visitor 兩種結構體表示兩種身份,他們都需要經過公共的 checkSomeTing 操作,我們希望在這個函式中能根據不同的結構體,操作不同的邏輯。如果沒有反射,如何知道傳進來具體是什麼型別的值。(我只是單純的指出沒有反射情況下的問題,而不是吐槽上面這個設計)。
因此,我們需要能有一種方法,它可以在程式執行時獲取傳入引數真正的型別,如果是 struct 那麼這個 struct 有哪些屬性,哪些方法……。
Interface
說起 go 反射,就必然繞不開 interface 型別。在 go 中 interface 是一種特殊的型別,可以存放任何實現了其方法的值。如果是一個空 interface ,意味著可以傳遞任意型別值。interface 型別有(value,type) 對,而反射就是檢查 interface 的 (value,type) 對,所有的反射包中的方法,都是圍繞 reflect.Type 和 reflect.Value 進行的。reflect.Type 是一個介面型別,提供了一系列獲取 interface 資訊的介面。原始碼位置在 src/reflect/type.go。
type Type interface {
// Methods applicable to all types.
Align() int
// FieldAlign returns the alignment in bytes of a value of
// this type when used as a field in a struct.
FieldAlign() int
Method(int) Method
String() string
........
}
而 reflect.Value 的型別被宣告成結構體。原始碼位置 src/reflect/value.go
type Value struct {
// typ holds the type of the value represented by a Value.
typ *rtype
// Pointer-valued data or, if flagIndir is set, pointer to data.
// Valid when either flagIndir is set or typ.pointers() is true.
ptr unsafe.Pointer
flag
}
可以看到,這個結構體的三個欄位都是私有的,沒有對外暴露任何欄位,但是它提供了一系列獲取資料和寫入等操作的方法。
反射三大定律
go 官方提供了三大定律來說明反射,我們也從這三個定律中學習如何 使用反射。
- 定律一:反射可以將 interface 變數轉換為反射物件。
package main
import (
"fmt"
"reflect"
)
func main() {
var a float64 = 32
var b int64 = 32
doSomeTing(a)
doSomeTing(b)
}
func doSomeTing(res interface{}) {
t := reflect.TypeOf(res)
v := reflect.ValueOf(res)
fmt.Println("型別是:", t)
fmt.Println("值是:", v)
}
程式列印結果:
我們定義了兩個變數,他們的型別分別是 float64 和 int64 ,傳入 doSomeTing 函式,此函式引數型別為空的 interface ,因此可以接收任意型別引數,最終我們通過 reflect.TypeOf 獲取了變數的真實型別,通過 reflect.ValueOf 獲取變數真實的值。
我們可以再試試通過結構體使用其他操作方法。
package main
import (
"fmt"
"reflect"
)
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
filed := t.Field(i)
value := v.Field(i).Interface()
fmt.Printf("field:%v type:%v value:%v\n", filed.Name, filed.Type, value)
}
}
執行結果:
上面就不解釋了,主要解釋迴圈裡面的,我們通過 reflect.type 的 NumField 獲取結構體中個數,通過 reflect.type 的 Field 方法下標獲取屬性名,通過 interface() 獲取對應屬性值。
- 定律二:反射可以將反射物件還原成 interface 物件。
package main
import (
"fmt"
"reflect"
)
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
var user2 user = v.Interface().(user)
fmt.Printf("使用者:%+v\n",user2)
}
u 變數轉換成反射物件 v,v 又通過 interface() 介面轉換成 interface 物件,再通過顯性型別轉換成 user 結構體物件,賦值給型別為 user 的變數 user2 。
- 定律三:反射物件是否可修改,取決於 value 值是否可設定
我們在通過反射將任意型別的變數(不管什麼型別最終傳遞到 reflect.TypeOf 或者 reflect.ValueOf 都會隱式轉換成 interface)轉換成反射物件,那麼理論上我們就可以基於反射物件設定其所持有的值。
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(u)
v.FieldByName("Name").SetString("curry")
fmt.Printf("v的值:%+v",v)
}
上面程式碼我們想的是通過反射物件修改結構體屬性 Name 值為 curry 。當執行這段程式碼時,會報執行恐慌(panic)。
錯誤的原因正是值是不可修改的。
反射物件是否可修改取決於其所儲存的值,上面 reflect.ValueOf 傳入的其實是 u 的值,而非它的本身。(想想函式傳值的時候是傳值還是傳址),既然是傳值,那麼通過修改 v 的值當然不可能修改到 u 的值,我們要設定的應該是指標所指向的內容,即 *u 。
type user struct {
Name string
Age int
}
func main() {
var u = user{
Age: 18,
Name: "wuqq",
}
v := reflect.ValueOf(&u)
v.Elem().FieldByName("Name").SetString("curry")
fmt.Printf("v的值:%+v",v)
}
首先我們通過 &u 傳入 u 變數實際儲存的地址。然後通過反射物件中的 Elem() 獲得指標所指向的 value 。
執行結果值已然被修改。
結尾
關於反射,我還想說說它不好的地方:
作為靜態語言,編碼過程中,編譯器可以提前發現一些型別錯誤,但是反射程式碼是不行的(如果可以請告知)。可能會因為 bug 導致執行恐慌。
反射對效能影響比較大,對於一些注重執行效率的關鍵點,儘量避免使用反射。
還有其他有趣的操作,推薦先多看幾遍官方的一篇部落格:
blog.golang.org/laws-of-reflection
參考資料:
draveness.me/golang/docs/part2-fou...
www.bookstack.cn/read/GoExpertProg...
本作品採用《CC 協議》,轉載必須註明作者和本文連結