1. 程式人生 > >Golang的反射機制(The Laws of Reflection)

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.Readerio.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中的兩種基本型別:TypeValue。這兩種型別得以分析介面型別變數的值與型別。同時引入兩個簡單函式reflect.TypeOf以及reflect.ValueOf。用來檢索reflect.Typereflect.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.Typereflect.Value變數都有一大堆的方法用來操縱它們。比如Value型別變數就有一個名曰Type的方法用來返回一個reflect.Type型別的變數。同時,TypeValue型別的變數都有一個Kind方法用來返回一個指示變數型別的常量。而Value型別變數還擁有一些名如IntFloat的方法用來獲取內部儲存的值(返回的是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

當然,我們還有類似SetIntSetFloat的方法用來設定內部的值。但是使用的時候要小心,這關係到一個叫做 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)