Go語言中反射包的實現原理(The Laws of Reflection)

kjfcpua發表於2015-01-04

前言

過去只是知道某些語言帶有反射,但是一直沒機會使用這種高階功能,所以也沒有深入瞭解過。昨天看golang時裡面提到reflection,既然這麼多語言支援這個性質,那就深入瞭解下好了。這篇文件翻譯自官方文件的The Laws of Reflection,翻譯目的不是為了翻譯,而是加深自己記憶以及理解,所以有些地方可能不會直譯,因為我沒那麼高水平,有時自己能看懂,但是按著原話翻譯出來給別人聽感覺好難。某些專用名詞會繼續保留原文,有時,其實我覺得還是英文更加容易理解。

The Laws of Reflection翻譯

計算機中提到的反射指的是程式藉助某種手段檢查自己結構的一種能力,通常就是藉助程式語言中定義的各種型別(types)。同時反射也是困惑的最大來源之一。

在這篇文章中,我們嘗試通過解釋反射在Go中是如何工作來掃除這些困惑。不同語言的反射模型(reflection model)的實現也是不同的(當然某些語言根本就不支援反射),但是這篇文章是關於Go的,所以這篇文章剩餘部分提到“反射”的時候特指“Go中的反射”。

型別和介面(Types and interfaces)

因為反射是建立在型別系統(the type system)上的,所以讓我們從複習Go中的型別開始講起。

Go是靜態型別化的。每個變數都有一個靜態型別,也就是說,在編譯的時候變數的型別就被很精確地確定下來了,比如要麼是int,或者是float32,或者是MyType型別,或者是[]byte等等。如果我們像下面這樣宣告:

1
2
3
4
type MyInt int

var i int
var j MyInt

那麼i的型別就是int,而j的型別就是MyInt。這裡的變數i和j具有不同的靜態型別,雖然它們有相同的底層型別(underlying type),如果不顯示的進行強制型別轉換它們是不能互相賦值的。

型別(type)中非常重要的一類(category)就是介面型別(interface type),一個介面就表示一組確定的方法(method)集合。一個介面變數能儲存任意的具體值(這裡的具體concrete就是指非介面的non-interface),只要這個具體值所屬的型別實現了這個介面的所有方法。一個大家都很熟悉的例子是io.Reader和io.Writer,型別Reader和型別Writer來自io包:

1
2
3
4
5
6
7
8
9
// 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方法的型別所定義的值,比如:

1
2
3
4
5
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。

一個非常非常重要的介面型別例子就是空介面:

1
interface{}

空介面表示方法集合為空並且可以儲存任意值,因為任意值都有0個或者更多方法。

有些人說Go的介面是動態型別化的,但這是一種誤導。Go的介面都是靜態型別化的:一個介面型別變數總是保持同一個靜態型別,即使在執行時它儲存的值的型別發生變化,這些值總是滿足這個介面。

我們需要搞清楚上面說的這些,因為反射和介面是緊緊聯絡在一起的。

介面的表示(The representation of an interface)

Russ Cox曾經寫過一篇關於Go中介面值的表示的部落格detailed blog post。在這裡我們沒必要重複他的整篇文章內容了,但是簡單概括下還是應該的。

一個介面型別變數儲存了一個pair:賦值給這個介面變數的具體值,以及這個值的型別描述符。更進一步的講,這個”值”是實現了這個介面的底層具體資料項(underlying concrete data item),而這個“型別”是描述了那個項(item)的全型別(full type)。舉個例子,執行完下面這些:

1
2
3
4
5
6
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)。注意,除了Read方法以外,型別os.File也實現了其它方法;即使這個介面值僅僅提供了對Read方法的訪問,這個介面值內部仍然帶有關於這個值的全部型別資訊。這就是為什麼我們能幹下面這些事兒:

1
2
var w io.Writer
w = r.(io.Writer)

這個賦值操作中的表示式是一個型別斷言(type assertion);它所斷言的是r中儲存的項(item)也實現了io.Writer介面,所以我們可以把它賦值給w。賦值操作完畢以後,w將會包含 (tty, *os.File)對。這個pair跟r中的pair是同樣的。介面的靜態型別決定了能用介面變數呼叫哪些方法,即使介面裡存的具體值內部可能還有一大坨其它方法。(換句話說,介面定義的方法集合是該種介面變數所儲存的具體值所含有的方法集合的一個子集,通過這個介面變數只能呼叫這個介面定義過的方法,沒法通過這個介面變數呼叫其它任何方法。)

繼續說,我們可以這樣做:

1
2
var empty interface{}
empty = w

我們的空介面值,empty,也能包含同樣的pair即(tty, *os.File)。這樣的話就很方便了,一個空介面可以儲存任意值和我們所需要的關於所儲存值的全部資訊。

(我們不需要在這裡做型別斷言了,因為可以靜態地知道w滿足空介面。在上面那個把一個值從一個Reader移到一個Writer的例子裡,我們需要顯式地用一個型別斷言,因為Writer定義的方法集合不是Reader定義的方法集合的子集。)

一個很重要的細節是,一個介面中的pair總有(值,具體型別)這樣的格式,而不能有(值,介面型別)這樣的格式。介面不能儲存介面值(也就是說,你沒法把一個介面變數值儲存到一個介面變數中,只能把一個具體型別的值儲存到一個介面變數中。)

現在,我們終於準備好了可以看看反射是怎麼回事兒了。

第一反射定律(The first law of reflection)

1.從介面值到反射物件的反射(Reflection goes from interface value to reflection object)

最最基本的,反射是一種檢查儲存在介面變數中的(型別,值)對的機制。作為一個開始,我們需要知道reflect包中的兩個型別:TypeValue。這兩種型別給了我們訪問一個介面變數中所包含的內容的途徑,另外兩個簡單的函式reflect.Typeof和reflect.Valueof可以檢索一個介面值的reflect.Type和reflect.Value部分。(還有就是,我們可以很容易地從reflect.Value到達reflect.Type,但是現在暫且讓我們先把Value和Type的概念分開說。先劇透,從Value到達Type是通過Value中定義的某些方法來實現的,雖然先分開講,但是後面多注意一下。)

讓我們從Typeof開始:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

上面這段程式將會列印輸出:

1
type: float64

你可能想知道我們所說的介面在上面程式哪個地方,因為這個程式看起來就是把float64型別的變數x,而不是一個介面值,傳遞給reflect.Typeof函式。但是,它就在那呢!就像godoc reports所描述的,reflect.Typeof 簽名裡就包含了一個空介面:

1
2
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

當我們呼叫reflect.Typeof(x)的時候,x首先被儲存到一個空介面中,這個空介面然後被作為引數傳遞。reflect.Typeof 會把這個空介面拆包(unpack)恢復出型別資訊。

當然,reflect.Valueof可以把值恢復出來(從這裡開始,我們將會省略這個樣板而是專注與可執行程式碼):

1
2
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))//Valueof方法會返回一個Value型別的物件

列印出

1
value: <float64 Value>

reflect.Type和reflect.Value這兩種型別都提供了大量的方法讓我們可以檢查和操作這兩種型別。一個重要的例子是,Value型別有一個Type方法可以返回reflect.Value型別的Type(這個方法返回的是值的靜態型別即static type,也就是說如果定義了type MyInt int64,那麼這個函式返回的是MyInt型別而不是int64,看後面那個Kind方法就可以理解了)。另外一個重要的例子是,Type和Value都有一個Kind方法可以返回一個常量用於指示一個項到底是以什麼形式(也就是底層型別即underlying type,繼續前面括號裡提到的,Kind返回的是int64而不是MyInt)儲存的(what sort of item is stored),這些常量包括:Unit, Float64, Slice等等。而且,有關Value型別的帶有名字諸如Int和Float的方法可讓讓我們獲取存在裡面的值(比如int64和float64):

1
2
3
4
5
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())

列印出

1
2
3
type: float64
kind is float64: true
value: 3.4

還有一些方法像SetInt和SetFloat,但是為了使用它們,我們得理解什麼叫settability(在維基百科查不到,google翻譯說來自於義大利語,定形的意思,覺得不是太直白,不翻譯這個詞了,直接用原詞),下面會討論。

反射庫裡有倆性質值得單獨拿出來說說。第一個性質是,為了保持API簡單,Value的”setter”和“getter”型別的方法操作的是可以包含某個值的最大型別:比如,所有的有符號整型,只有針對int64型別的方法,因為它是所有的有符號整型中最大的一個型別。也就是說,Value的Int方法返回的是一個int64,同時SetInt的引數型別採用的是一個int64;所以,必要時要轉換成實際型別:

1
2
3
4
5
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.看到啦嘛?這個地方必須進行強制型別轉換!

第二個性質是,反射物件(reflection object)的Kind描述的是底層型別(underlying type),而不是靜態型別(static type)。如果一個反射物件包含了一個使用者定義的整型,比如:(還記得我在上面括號裡舉例子說明Type方法和Kind方法時說的那一坨嘛?):

1
2
3
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v的Kind仍然是reflect.Int,即使x的靜態型別是MyInt而不是int。換句話說,Kind不能將一個int從一個MyInt中區別出來,但是Type能做到!

第二反射定律(The second law of reflection)

2.從反射隊形到介面值的反射(Reflection goes from reflection object to interface value)

就像物理學上的反射,Go中到反射可以生成它的逆。

給定一個reflect.Value,我們能用Interface方法把它恢復成一個介面值;效果上就是這個Interface方法把型別和值的資訊打包成一個介面表示並且返回結果:

1
2
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

作為一個結果,我們可以說

1
2
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

把用反射物件v表示的float64型別的值列印了出來。

我們甚至可以做得更好一些,fmt.Println等方法的引數是一個空介面型別的值,所以我們可以讓fmt包自己在內部完成我們在上面程式碼中做的工作。因此,為了正確列印一個reflect.Value,我們只需把Interface方法的返回值直接傳遞給這個格式化輸出例程:

1
fmt.Println(v.Interface())

(為什麼我們不直接fmt.Println(v)?因為v是一個reflect.Value;我們想要的是v裡面儲存的具體值。)因為我們的值是float64型別的,所以我們甚至可以用一個floating-point格式來列印:

1
   fmt.Printf("value is %7.1e\n", v.Interface())

會得到

1
3.4e+00

還有就是,我們不需要對v.Interface方法的結果呼叫型別斷言(type-assert)為float64;空介面型別值內部包含有具體值的型別資訊,並且Printf方法會把它恢復出來。

簡要的說,Interface方法是Valueof函式的逆,除了它的返回值的型別總是interface{}靜態型別。(不知道會不會有人看到前面這句話既用了方法又用了函式,會覺得奇怪。我推測,Go對於型別裡面定義的都叫方法,包級別全域性性的不屬於任何型別的叫做函式。)

重申一遍:反射就是從介面值到反射物件,然後再反射回來。(Reflection goes from interface value to reflection object and back again.)

第三反射定律(The third law of reflection)

3.為了修改一個反射物件,值必須是settable的(To modify a reflection object, the value must be settable)

這個第三定律是最微妙最讓人困惑的了,但是如果我麼能從第一定律出發可以很容易的理解它。

下面是一些不能正常執行的程式碼,但是很值得研究:

1
2
3
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你執行這段程式碼,將會帶著神祕的資訊發生panic

1
panic: reflect.Value.SetFloat using unaddressable value

問題不是出在值7.1不是可以定址的,而是出在v不是settable的。Settability是Value的一條性質,而且,不是所有的Value都具備這條性質

Value的CanSet方法用與測試一個Value的settablity;在我們的例子中,

1
2
3
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

輸出

1
settability of v: false

如果對一個non-settable的Value呼叫Set方法會出現錯誤。但是,settability到底是什麼呢?

settability有點像addressability,但是更加嚴格。settability是一個性質,描述的是一個反射物件能夠修改創造它的那個實際儲存的值的能力。settability由反射物件是否儲存原始項(original item)而決定。當我們說

1
2
var x float64 = 3.4
v := reflect.ValueOf(x)

我們傳遞了x的一個副本給reflect.Valueof函式,所以作為reflect.Valueof引數被創造出來的介面值只是x的一個副本,而不是x本身。因為,如果下面這條語句

1
v.SetFloat(7.1)

執行成功(當然不可能執行成功啦,假設而已),它不會更新x,即使v看起來像是從x創造而來,所以它更新的只是儲存在反射值內部的x的一個副本,而x本身不受絲毫影響,所以如果真這樣的話,將會非常那令人困惑,而且一點用都沒有!所以,這麼幹是非法的,而settability就是用來阻止這種哦給你非法狀況出現的。

如果你覺得這個看起來有點怪的話,其實不是的,它實際上是一個披著不尋常外衣的一個你很熟悉的情況。想想下面這個把x傳給一個函式:

1
f(x)

我們不會期待f能夠修改x的值,因為我們穿了x值的一個副本,而不是x本身。如果我們想要f直接修改x,我們必須把x的地址傳給這個函式(也就是說,給它傳x的指標):

1
f(x)

這個就很直接了,而且看起來很面熟,其實反射也是按同樣的方式來運作。如果我們想通過反射來修改x,我們必須把我們想要修改的值的指標傳給一個反射庫

我們來實際操作一下。首先,我們像平常一樣初始化x,然後創造一個指向它的反射值,叫做p.

1
2
3
4
var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.注意這裡哦!我們把x地址傳進去了!
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

現在輸出就是

1
2
type of p: *float64
settability of p: false

反射物件p不是settable的,但是我們想要設定的不是p,而是(效果上來說)*p。為了得到p指向的東西,我們呼叫Value的Elem方法,這樣就能迂迴繞過指標,同時把結果儲存在叫v的Value中:

1
2
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

現在v就是一個settable的反射物件了,正如輸出所描述的,

1
settability of v: true

並且因為v表示x,我們最終能夠通過v.SetFloat方法來修改x的值:

1
2
3
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

輸出正是我們所期待的,

1
2
7.1
7.1

反射理解起來有點困難,但是它確實正在做程式語言要做的,儘管是通過掩蓋了所發生的一切的反射Types和Vlues來實現的。這樣好了,你就直接記住反射Values為了修改它們所表示的東西必須要有這些東西的地址

Structs

在我們前面的例子中,v本身不是一個指標,它只是從一個指標派生來的。出現這種情況的一個常見的方法是當使用反射來修改一個structure的各個域的時候。只要我們有這個structure的地址,我們就能修改它的各個域。

下面是分析一個struct值,t,的簡單例子。我們用這個struct的地址建立一個反射物件,因為我們想一會改變它的值。然後我們把typeofT變數設定為這個反射物件的型別,接著使用一些直接的方法呼叫(細節請見reflect包)來迭代各個域。注意,我們從struct型別中提取了各個域的名字,但是這些域本身都是rreflect.Value物件。

1
2
3
4
5
6
7
8
9
10
11
12
type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()//把s.Type()返回的Type物件複製給typeofT,typeofT也是一個反射。
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)//迭代s的各個域,注意每個域仍然是反射。
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())//提取了每個域的名字
}

這段程式的輸出是:

1
2
0: A int = 23
1: B string = skidoo

關於settability還有一個要點在這裡要介紹一下: 這裡T的域的名字都是大寫的(被匯出的),因為一個struct中只有被匯出的域才是settable的。

因為s包含了一個settable的反射物件,所以我們可以修改這個structure的各個域。

1
2
3
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

結果在這裡:

1
t is now {77 Sunset Strip}

如果我們修改這個程式,讓s從t建立出來而不是&t,那麼上面對SetInt和SetString的呼叫將會統統失敗,因為t的各個域不是settable的。

總結(Conclusion)

我們在最後再次列出反射的三大定律:
1.Reflection goes from interface value to reflecton object.
2.Reflection goes from reflection object to interface value.
3.To modify a reflection object, the value must be settable.

一旦你理解了這三條反射定律,Go中的反射用起來就很簡單了,雖然它還仍然有點微妙。反射是一個強大的工具,你必須要十分小心的使用它,並且應該儘量避免使用它,除非真的是不用不行了。

關於反射,仍然有大量內容我們沒有講到—-在channel中的傳送操作和接收操作,分配記憶體,使用slices和map,呼叫方法和函式—但是這篇文章已經夠長了。我們將來會在隨後的文章中講到前面提到的這些topics中的一些。

相關文章