Golang的反射機制(The Laws of Reflection)
Introduction(簡介)
反射機制能夠在陳故鄉執行過程中檢查自身元素的結構,型別;屬於元程式程式設計。但同時也帶來了不少迷惑。
本文我們嘗試通過解釋Go中的反射機制來解釋一些使用細節。每種語言的反射機制都是不同的(有很多語言甚至沒有反射),此文針對Go語言,所以下文的所有反射感念都是Go中的反射。
Types and interfaces(型別和介面)
由於反射機制建立在型別系統只想,讓我們先來回顧下Go中的型別吧。
Go是靜態型別語言。每個變數都擁有一個靜態型別,這意味著每個變數的型別在編譯時都是確定的:int,float32, *MyType, []byte, 諸如此類。
type MyInt int
var i int
var j MyInt
在上面的程式碼中,i型別為int,j型別為MyInt。雖然變數i和j擁有相同的基型別,然而他們是不同的靜態型別,不通過轉換將不能相互賦值。
介面型別是一類十分重要的型別,表示了一堆固定的方法集合。一個介面型別可以儲存任意的混合值(非介面),只要該型別實現了介面定義的方法集。一對廣為人知的例子是io.Reader和io.Writer。下面的例子中介面Reader和Writer來自於包 io package
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
任何實現了Read方法(或者Write方法)的型別都可以認為實現了io.Reader
(或者io.Writer
)。這意味著一個io.Reader型別的變數可以儲存任意實現了Read方法的型別,如下所示:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
需要清楚的是r所持有的混合值。r的型別始終是io.Reader
:Go是靜態型別語言且r的型別值是io.Reader
。
有一種極端特殊的介面是所謂的空介面:
interface{}
它包含了空方法集。由於任何型別都至少實現了0個或多個方法,所以空介面可以承接任意型別。
有些人以為Go的介面型別是動態型別,實際上是不對的。介面型別仍舊是靜態型別:某個介面型別的變數的型別始終不變,即使在執行時其內部儲存的介面變數(confusing?沒關係,繼續看)在變換值,他們始終是該介面型別。
我們需要明確這一點,因為反射機制和介面型別密切相關。
The representation of an interface(介面的表示)
一個介面型別可以理解為儲存了一對值:具體變數值以及該變數的型別描述符。更精確地來說,介面變數儲存了實現了該介面的型別變數,以及被儲存的變數型別。舉例來說:
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
r語義上可以理解為一個(value, type)對,在這裡是(tty, *os.File)。注意這裡*os.File
實現了Read方法;雖然該介面型別只能呼叫該變數的Read的方法,然而該變數的型別描述符儲存在了介面變數中,即所有的型別資訊。所以我們才可以做如下的操作:
var w io.Writer
w = r.(io.Writer)
上面的表示式是一個型別斷言;它斷言r中儲存的變數同時也實現了io.Writer
介面。故此我們可以我們可以將其賦值給w。賦值之後,w所包含的同樣是(tty, *os.File),與r中儲存的值與型別相同(當然方法列表是不一樣的)。靜態型別決定了該變數可以呼叫的方法,雖然內部儲存的值可能包含了更多介面未定義的方法。
接下來,我們這樣:
var empty interface{}
empty = w
我們的空介面變數empty儲存了相同的(tty, *os.File)。這很方便,因為空介面可以承接任意的型別,並將該型別的變數資訊完全保留。
(在這裡我們不需要型別斷言,由於空介面一定能夠承接成功。在上面的例子中我們將值從一個Reader型別的介面變數中傳遞到Writer介面變數。需要注意的是必須顯示的使用型別斷言,因為Writer的方法集不是Reader的方法集的子集)
有一個重要的細節是介面變數邏輯上儲存的值是(value, concrete type)而不是(value, interface type),介面型別變數不能儲存介面型別變數值。
需要注意的被介面承接的值是值傳遞,從Russ Cox的部落格中我們知道介面型別對內部值得儲存是值傳遞,即一個變數賦值給了一個介面變數,如果改變了原始的變數,其由介面儲存的值也不會改變。
好啦,終於可以開始將反射機制了!!!
The laws of reflection(反射機制)
1. Reflection goes from interface value to reflection object.(反射可以從介面型別到反射型別物件)
基本的來說,反射僅僅是一種在介面中校驗型別和值的機制。我們先來引入package reflect中的兩種基本型別:Type和Value。這兩種型別得以分析介面型別變數的值與型別。同時引入兩個簡單函式reflect.TypeOf
以及reflect.ValueOf
。用來檢索reflect.Type
和reflect.Value
變數。(當然,從reflect.Value
變數可以很容易地得到reflect.Type
,但暫時先讓我們將兩者分開對待)
讓我們來看看TypeOf的用法:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
程式輸出:
type: float64
你可能疑惑這裡沒有藉口型別啊,明明傳的是float64型別呢。實際上,Typeof函式接受的引數是:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
Go語言傳參的時候先做了型別轉換再傳參壓棧的哈。
同樣的,reflect.ValueOf
函式用來取出對應的值(但仍然是reflect.Value型別的變數)
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
輸出:
value: 3.4
reflect.Type和reflect.Value變數都有一大堆的方法用來操縱它們。比如Value型別變數就有一個名曰Type的方法用來返回一個reflect.Type型別的變數。同時,Type和Value型別的變數都有一個Kind方法用來返回一個指示變數型別的常量。而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
的方法用來設定內部的值。但是使用的時候要小心,這關係到一個叫做 settability(可設定性)的一個東西,細紋會細將。
反射庫中有很多屬性值得單獨拎出來細講。首先,為了保持API的簡潔性,Get方法和Set方法有一定的簡化考慮:比如用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 returns a uint64.
第二個特性是反射變數對應的Kind方法的返回值是基型別,並不是靜態型別。下面的例子中:
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
變數v的Kind依舊是reflect.Int,而不是MyInt這個靜態型別。Type可以表示靜態型別,而Kind不可以。
2. Reflection goes from reflection object to interface value.(反射可以從反射型別物件到介面型別)
就像物理裡的反射定律一樣,Go中的反射物件也能反射到自己。
給定一個reflect.Value型別的物件我們可以通過Interface方法來將其反轉回介面變數。將其型別和值重新打包回一個介面變數中:
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
於是我們可以使用:
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
實際上,由於fmt.Println()接受的是介面型別的變數,我們並不需要型別斷言,可以直接將介面傳入。
fmt.Println(v.Interface())
(為什麼不直接 fmt.Println(v)?因為v的型別是reflect.Value,我們需要的是內部的具體值)。甚至,我們可以直接用float64型別的格式控制:
fmt.Printf("value is %7.1e\n", v.Interface())
得到:
3.4e+00
簡單來說,interface方法是ValueOf方法的反函式。其結果總是靜態型別interface{}
3. To modify a reflection object, the value must be settable.(修改反射型別變數的內部值需要保證其可設定性)
第三條有點讓人困惑,什麼是可設定性?
先來看一段錯誤程式碼:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
如果你直接執行,會得到如下錯誤:
panic: reflect.Value.SetFloat using unaddressable value
此處的問題在於v變數並不是可設定的。並不是所有的Value型別的變數都是可設定的。
CanSet方法可以用來檢測Value型別的可設定性:
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方法,所以傳入的引數僅僅是拷貝,而不是x本身。如果我們允許下面的操作成功:
v.SetFloat(7.1)
這並不會更新x。這好像真的沒有啥意義(設計者這樣認為)。所以乾脆定義它非法好了。
在平時的值傳遞的函式中我們也會遇到:
f(x)
這樣的呼叫時不會期待它能修改x值得。如果我們想要修改x,就這樣傳好了:
f(&x)
類似的,我們想要在反射物件中修改原值,就傳指標好了:
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
輸出是:
type of p: *float64
settability of p: false
我艹,怎麼還不行?廢話,p和當初你上面的例子有何區別,要取它的指向的值才可以:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
現在v終於是一個可設定的反射物件了:
settability of v: true
由於它指代了x變數,我們可以通過v.SetFloat方法來修改它:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
輸出:
7.1
7.1
其實也蠻簡單,記住一點,指標就是。
struct(結構體的特殊情況)
上面的例子中v是由一個物件引出的。一個常見的情況是使用反射機制去修改結構體的域。只要我們有結構體的地址,我們就能修改這個其中的域。
下面這個例子中我們可以提取域的名字,但是提取出的域本身也是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
注意結構體中的域名只有以大寫字母開頭的域才是可設定的。如下:
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
結果是:
t is now {77 Sunset Strip}
Conclusion(結論)
Go的反射機制總結就是:
- 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.
理解以上3條反射機制就很簡單啦^^
當然還有很多反射內容沒有講,包括channel中的收發,分配記憶體,使用分片和map,呼叫方法和函式。這些都以後再討論吧。
By Rob Pike(translator: xiaohu)