一哑姚、認識反射
- 維基百科中的定義:
在計算機科學(xué)中叙身,反射是指計算機程序在運行時(Run time)可以訪問、檢測和修改它本身狀態(tài)或行為的一種能力奕枢。用比喻來說,反射就是程序在運行的時候能夠“觀察”并且修改自己的行為佩微。
不同語言的反射模型不盡相同缝彬,有些語言還不支持反射。
Go 語言支持反射哺眯,它提供了一種機制在運行時更新變量和檢查它們的值谷浅、調(diào)用它們的方法,但是在編譯時并不知道這些變量的具體類型奶卓。
1一疯、為什么要用反射
- 需要反射的 2 個常見場景:
(1)有時你需要編寫一個函數(shù),但是并不知道傳給你的參數(shù)類型是什么夺姑,可能是沒約定好墩邀;也可能是傳入的類型很多,這些類型并不能統(tǒng)一表示瑟幕。這時反射就會用的上了磕蒲。
(2)有時候需要根據(jù)某些條件決定調(diào)用哪個函數(shù)留潦,比如根據(jù)用戶的輸入來決定只盹。這時就需要對函數(shù)和函數(shù)的參數(shù)進行反射,在運行期間動態(tài)地執(zhí)行函數(shù)兔院。
- 在講反射的原理以及如何用之前殖卑,還是說幾點不使用反射的理由:
(1)與反射相關(guān)的代碼,經(jīng)常是難以閱讀的坊萝。在軟件工程中孵稽,代碼可讀性也是一個非常重要的指標许起。
(2)Go 語言作為一門靜態(tài)語言,編碼過程中菩鲜,編譯器能提前發(fā)現(xiàn)一些類型錯誤园细,但是對于反射代碼是無能為力的。所以包含反射相關(guān)的代碼接校,很可能會運行很久猛频,才會出錯,這時候經(jīng)常是直接 panic蛛勉,可能會造成嚴重的后果鹿寻。
(3)反射對性能影響還是比較大的,比正常代碼運行速度慢一到兩個數(shù)量級诽凌。所以毡熏,對于一個項目中處于運行效率關(guān)鍵位置的代碼,盡量避免使用反射特性侣诵。
2痢法、反射原理
interface是 Go 語言實現(xiàn)抽象的一個非常強大的工具。當(dāng)向接口變量賦一個實體類型的時候杜顺,接口會存儲實體的類型信息疯暑,反射就是通過接口的類型信息實現(xiàn)的,反射建立在類型的基礎(chǔ)上哑舒。
go語言的兩個特點:
- go語言是靜態(tài)類型語言妇拯,因此在程序編譯階段,類型已經(jīng)確定洗鸵。
- interface{}空接口可以和任意類型進行交互越锈,因此可以利用這一特點實現(xiàn)對任意類型的反射。
go語言的類型:
- 變量包含(type膘滨,value)兩個部分甘凭。
- type 包括 static type和concrete type. 簡單來說 static type是你在編碼是看見的類型(如int、string)火邓,concrete type是runtime系統(tǒng)看見的類型丹弱。
- 類型斷言能否成功,取決于變量的concrete type铲咨,而不是static type躲胳。因此,一個 reader變量如果它的concrete type也實現(xiàn)了write方法的話纤勒,它也可以被類型斷言為writer坯苹。
go語言的反射是建立在類型的基礎(chǔ)上的,golang中對于指定類型的變量的類型是靜態(tài)的(如指定為int摇天、string類型的變量 粹湃,他們的type是static type)恐仑,也就是說在創(chuàng)建變量的時候類型已經(jīng)確定。反射主要針對interface類型(它的type是concrete type)为鳄。
在go語言的實現(xiàn)中裳仆,每一個interface變量都有一個pair,pair中記錄了實際變量的值和類型:
(value, type)
其中:value是實際變量值孤钦,type是實際變量類型鉴逞。一個interface變量包含了2個指針,一個指向值的類型(對應(yīng)concrete type)司训,一個指向?qū)嶋H的值(對應(yīng)value)构捡。
代碼示例:
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 創(chuàng)建一個*os.File類型的變量f
f, _ := os.OpenFile("a.txt", os.O_RDWR, os.ModePerm)
// 創(chuàng)建一個io.Reader接口類型的變量r
var r io.Reader
r = f.(io.Reader)
// 創(chuàng)建一個io.Writer接口類型的變量w
var w io.Writer
w = r.(io.Writer)
// 創(chuàng)建一個interface{}空接口類型的變量i
var i interface{}
i = w
fmt.Printf("%T, %T, %T, %T", f, r, w, i)
}
運行結(jié)果
*os.File, *os.File, *os.File, *os.File
可以看到,接口變量w壳猜、r勾徽、i的pair相同,都是:(f, *os.File)统扳。
interface及其pair的存在喘帚,是Golang中實現(xiàn)反射的前提,理解了pair咒钟,就更容易理解反射吹由。反射就是用來檢測存儲在接口變量內(nèi)部(值value;類型concrete type) pair對的一種機制朱嘴。
下面倾鲫,我們通過go語言的reflect包提供的API來實現(xiàn)反射機制
二、Type和Value
reflect包提供了兩種類型(或者說兩個方法)讓我們可以很容易的訪問接口變量內(nèi)容萍嬉,分別是reflect.ValueOf() 和 reflect.TypeOf()乌昔,看看官方的解釋:
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}
翻譯一下:ValueOf用來獲取輸入?yún)?shù)接口中的數(shù)據(jù)的值,如果接口為空則返回0
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}
翻譯一下:TypeOf用來動態(tài)獲取輸入?yún)?shù)接口中的值的類型壤追,如果接口為空則返回nil
reflect.TypeOf()是獲取pair中的type磕道,reflect.ValueOf()獲取pair中的value。
首先需要把它轉(zhuǎn)化成reflect對象(reflect.Type或者reflect.Value行冰,根據(jù)不同的情況調(diào)用不同的函數(shù)溺蕉。
說明:
- reflect.TypeOf: 直接給到了我們想要的type類型,如
float64
悼做、int
疯特、各種pointer
、struct
等等真實的類型 - reflect.ValueOf:直接給到了我們想要的具體的值贿堰,如
1.2345
這個具體數(shù)值辙芍,或者類似&{1, "Allen.Wu", 25}
這樣的結(jié)構(gòu)體struct的值 - 也就是說明反射可以將“接口類型變量”轉(zhuǎn)換為“反射類型對象”,反射類型指的是reflect.Type和reflect.Value這兩種
Type 和 Value 都包含了大量的方法羹与,其中第一個有用的方法應(yīng)該是 Kind故硅,這個方法返回該類型的具體信息:Uint、Float64 等纵搁。Value 類型還包含了一系列類型方法吃衅,比如 Int(),用于返回對應(yīng)的值腾誉。以下是Kind的種類:
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
三徘层、反射的規(guī)則
下圖描述了實例、Value利职、Type 三者之間的轉(zhuǎn)換關(guān)系:
1趣效、從實例到Value
func ValueOf(i interface {}) Value
2、從實例到Type
func TypeOf(i interface{}) Type
3猪贪、從Type到Value
Type 里面只有類型信息跷敬,所以直接從一個 Type 接口變量里面是無法獲得實例的 Value 的,但可以通過該 Type 構(gòu)建一個新實例的 Value热押。reflect 包提供了兩種方法西傀,示例如下:
//New 返回的是一個 Value,該 Value 的 type 為 PtrTo(typ)桶癣,即 Value 的 Type 是指定 typ 的指針類型
func New(typ Type) Value
//Zero 返回的是一個 typ 類型的零佳拥褂,注意返回的 Value 不能尋址,位不可改變
func Zero(typ Type) Value
4牙寞、從Value到Type
從反射對象 Value 到 Type 可以直接調(diào)用 Value 的方法饺鹃,因為 Value 內(nèi)部存放著到 Type 類型的指針。
func (v Value) Type() Type
5间雀、從Value到實例
Value 本身就包含類型和值信息尤慰,reflect 提供了豐富的方法來實現(xiàn)從 Value 到實例的轉(zhuǎn)換。例如:
//該方法最通用雷蹂,用來將 Value 轉(zhuǎn)換為空接口伟端,該空接口內(nèi)部存放具體類型實例
//可以使用接口類型查詢?nèi)ミ€原為具體的類型
func (v Value) Interface() (i interface{})
//Value 自身也提供豐富的方法,直接將 Value 轉(zhuǎn)換為簡單類型實例匪煌,如果類型不匹配责蝠,則直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64
6、從 Value 的指針到值
從一個指針類型的 Value 獲得值類型 Value 有兩種方法萎庭,示例如下霜医。
//如果 v 類型是接口,則 Elem() 返回接口綁定的實例的 Value驳规,如采 v 類型是指針肴敛,則返回指針值的 Value,否則引起 panic
func (v Value) Elem() Value
//如果 v 是指針,則返回指針值的 Value医男,否則返回 v 自身砸狞,該函數(shù)不會引起 panic
func Indirect(v Value) Value
7、Type 指針和值的相互轉(zhuǎn)換
指針類型 Type 到值類型 Type镀梭。例如:
//t 必須是 Array刀森、Chan、Map报账、Ptr研底、Slice,否則會引起 panic
//Elem 返回的是其內(nèi)部元素的 Type
t.Elem() Type
值類型 Type 到指針類型 Type透罢。例如:
//PtrTo 返回的是指向 t 的指針型 Type
func PtrTo(t Type) Type
8榜晦、Value 值的可修改性
Value 值的修改涉及如下兩個方法:
//通過 CanSet 判斷是否能修改
func (v Value ) CanSet() bool
//通過 Set 進行修改
func (v Value ) Set(x Value)
根據(jù) Go 官方關(guān)于反射的博客,反射有三大定律:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
第一條是最基本的:反射可以從接口值得到反射對象羽圃。
第二條實際上和第一條是相反的機制乾胶,反射可以從反射對象獲得接口值。
第三條不太好懂:如果需要操作一個反射變量统屈,則其值必須可以修改胚吁。
四、反射的使用
1愁憔、從relfect.Value中獲取接口interface的信息
當(dāng)執(zhí)行reflect.ValueOf(interface)之后腕扶,就得到了一個類型為”relfect.Value”變量,可以通過它本身的Interface()方法獲得接口變量的真實內(nèi)容吨掌,然后可以通過類型判斷進行轉(zhuǎn)換半抱,轉(zhuǎn)換為原有真實類型。
已知類型
從反射值對象(reflect.Value)中獲取值得方法:
方法名 | 說 明 |
---|---|
Interface() interface{} | 將值以 interface{} 類型返回膜宋,可以通過類型斷言轉(zhuǎn)換為指定類型 |
Int() int64 | 將值以 int 類型返回窿侈,所有有符號整型均可以此方式返回 |
Uint() uint64 | 將值以 uint 類型返回,所有無符號整型均可以此方式返回 |
Float() float64 | 將值以雙精度(float64)類型返回秋茫,所有浮點數(shù)(float32史简、float64)均可以此方式返回 |
Bool() bool | 將值以 bool 類型返回 |
Bytes() []bytes | 將值以字節(jié)數(shù)組 []bytes 類型返回 |
String() string | 將值以字符串類型返回 |
已知類型后轉(zhuǎn)換為其對應(yīng)的類型的做法如下,直接通過Interface方法然后強制轉(zhuǎn)換肛著,如下:
realValue := value.Interface().(已知的類型)
示例代碼:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 10
rType := reflect.TypeOf(a)
fmt.Println(rType)
rValue := reflect.ValueOf(a)
fmt.Println(rValue)
rPointer := reflect.ValueOf(&a)
fmt.Println(rPointer)
// 1. 轉(zhuǎn)換的時候圆兵,如果轉(zhuǎn)換的類型不完全符合,則直接panic枢贿,類型要求非常嚴格殉农!
// 2. 轉(zhuǎn)換的時候,要區(qū)分是指針還是值
// 3. 也就是說反射可以將“反射類型對象”再重新轉(zhuǎn)換為“接口類型變量”
// convertValue := rValue.Interface().(int) // 10
convertValue := rValue.Int()
// convertValue := rValue.Bool() // panic: reflect: call of reflect.Value.Bool on int Value
convertPointer := rPointer.Interface().(*int)
fmt.Println(convertValue)
fmt.Println(convertPointer)
}
運行結(jié)果
int
10
0xc000012090
10
0xc000012090
結(jié)構(gòu)體類型
反射值對象(reflect.Value)提供對結(jié)構(gòu)體訪問的方法局荚,通過這些方法可以完成對結(jié)構(gòu)體字段和方法的訪問超凳,如下表所示愈污。
方 法 | 說 明 |
---|---|
Field(i int) Value | 根據(jù)索引,返回索引對應(yīng)的結(jié)構(gòu)體成員字段的反射值對象暂雹。當(dāng)值不是結(jié)構(gòu)體或索引超界時發(fā)生panic |
NumField() int | 返回結(jié)構(gòu)體成員字段數(shù)量。當(dāng)值不是結(jié)構(gòu)體或索引超界時發(fā)生panic |
FieldByName(name string) Value | 根據(jù)給定字符串返回字符串對應(yīng)的結(jié)構(gòu)體字段擎析。沒有找到時返回零值挥下,當(dāng)值不是結(jié)構(gòu)體或索引超界時發(fā)生panic |
FieldByIndex(index []int) Value | 多層成員訪問時,根據(jù) []int 提供的每個結(jié)構(gòu)體的字段索引桨醋,返回字段的值棚瘟。 沒有找到時返回零值喜最,當(dāng)值不是結(jié)構(gòu)體或索引超界時發(fā)生panic |
FieldByNameFunc(match func(string) bool) Value | 根據(jù)匹配函數(shù)匹配需要的字段。找到時返回零值瞬内,當(dāng)值不是結(jié)構(gòu)體或索引超界時發(fā)生panic |
NumMethod() int | 返回該類型的方法集中方法的數(shù)目 |
Method(int) Method | 返回該類型方法集中的第i個方法 |
MethodByName(string)(Method, bool) | 根據(jù)方法名返回該類型方法集中的方法 |
示例代碼:
package main
import (
"fmt"
"reflect"
)
// Student 學(xué)生結(jié)構(gòu)體
type Student struct {
Name string
Age int
Address string
}
// Say Student結(jié)構(gòu)體的方法
func (s Student) Say(msg string) {
fmt.Println(msg)
}
// PrintInfo Student結(jié)構(gòu)體的方法
func (s Student) PrintInfo() {
fmt.Printf("姓名:%s\t年齡:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}
func main() {
s := Student{"tom", 20, "上海市"}
Test(s)
}
// Test 測試函數(shù)
func Test(i interface{}) {
// 獲取i的類型
rType := reflect.TypeOf(i)
fmt.Println("i的類型是:", rType.Name()) // Student
fmt.Println("i的種類是:", rType.Kind()) // struct
// 獲取i的值
rValue := reflect.ValueOf(i)
fmt.Println("i的值是:", rValue)
// 獲取i的字段信息
for i := 0; i < rValue.NumField(); i++ {
field := rType.Field(i)
value := rValue.Field(i).Interface()
fmt.Printf("字段名稱:%s, 字段類型:%s, 字段值:%v\n", field.Name, field.Type, value)
}
// 獲取i的方法信息
for i := 0; i < rValue.NumMethod(); i++ {
method := rType.Method(i)
fmt.Printf("方法的名稱:%s, 方法的類型:%s\n", method.Name, method.Type)
}
}
運行結(jié)果:
i的類型是: Student
i的種類是: struct
i的值是: {tom 20 上海市}
字段名稱:Name, 字段類型:string, 字段值:tom
字段名稱:Age, 字段類型:int, 字段值:20
字段名稱:Address, 字段類型:string, 字段值:上海市
方法的名稱:PrintInfo, 方法的類型:func(main.Student)
方法的名稱:Say, 方法的類型:func(main.Student, string)
2迷雪、通過reflect.Value設(shè)置實際變量的值
reflect.Value是通過reflect.ValueOf(X)獲得的,只有當(dāng)X是指針的時候章咧,才可以通過reflec.Value修改實際變量X的值能真,即:要修改反射類型的對象就一定要保證其值是“addressable”的。
// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value {}
// 翻譯:
// Elem返回接口v包含的值或指針v指向的值疼约。
// 如果v的類型不是interface或ptr蝙泼,它會恐慌。
// 如果v為零汤踏,則返回零值。
示例代碼:
package main
import (
"fmt"
"reflect"
)
func main() {
var a int = 10
rValue := reflect.ValueOf(&a)
fmt.Println("是否可修改:", rValue.Elem().CanSet())
rValue.Elem().SetInt(20)
fmt.Println(a)
}
運行結(jié)果
是否可修改: true
20
3昙沦、通過reflect.Value來進行函數(shù)的調(diào)用
如果反射值對象(reflect.Value)中值的類型為函數(shù)時载荔,可以通過 reflect.Value 調(diào)用該函數(shù)。使用反射調(diào)用函數(shù)時丘损,需要將參數(shù)使用反射值對象的切片 []reflect.Value 構(gòu)造后傳入 Call() 方法中,調(diào)用完成時衔蹲,函數(shù)的返回值通過 []reflect.Value 返回呈础。
示例代碼:
package main
import (
"fmt"
"reflect"
)
func main() {
rValue := reflect.ValueOf(add)
res := rValue.Call([]reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)})
fmt.Printf("%v, %T\n", res, res)
fmt.Println(res[0].Int())
}
func add(a, b int) int {
return a + b
}
運行結(jié)果
[<int Value>], []reflect.Value
30
提示:
反射調(diào)用函數(shù)的過程需要構(gòu)造大量的 reflect.Value 和中間變量,對函數(shù)參數(shù)值進行逐一檢查而钞,還需要將調(diào)用參數(shù)復(fù)制到調(diào)用函數(shù)的參數(shù)內(nèi)存中。調(diào)用完畢后撬陵,還需要將返回值轉(zhuǎn)換為 reflect.Value网缝,用戶還需要從中取出調(diào)用值。因此草添,反射調(diào)用函數(shù)的性能問題尤為突出维费,不建議大量使用反射函數(shù)調(diào)用。
4犀盟、通過反射,調(diào)用方法
調(diào)用方法和調(diào)用函數(shù)是一樣的倡怎,只不過結(jié)構(gòu)體需要先通過rValue.Method()先獲取方法再調(diào)用贱枣。
示例代碼:
package main
import (
"fmt"
"reflect"
)
// Student 學(xué)生結(jié)構(gòu)體
type Student struct {
Name string
Age int
Address string
}
// Say Student結(jié)構(gòu)體的方法
func (s Student) Say(msg string) {
fmt.Println(msg)
}
// PrintInfo Student結(jié)構(gòu)體的方法
func (s Student) PrintInfo() {
fmt.Printf("姓名:%s\t年齡:%d\t地址:%s\n", s.Name, s.Age, s.Address)
}
func main() {
s := Student{"tom", 20, "上海市"}
rValue := reflect.ValueOf(s)
m1 := rValue.MethodByName("Say")
m1.Call([]reflect.Value{reflect.ValueOf("hello world")})
m2 := rValue.MethodByName("PrintInfo")
m2.Call(nil)
}
運行結(jié)果
hello world
姓名:tom 年齡:20 地址:上海市