Go語言反射定律

介紹

計算中的反射是指一段程序有能力檢查它自己的結構,特別是數據類型。這是一個元編程的形式。這也是很多困惑的來源。

在這篇文章我們嘗試通過解釋反射在Go語言中的運作方式來闡明各個知識點。每個語言中的反射模型是有差異的(有許多語言甚至不支持反射),但是這篇文章是關於Go,本文剩餘部分的反射是指Go語言中的反射。

類型和接口

因爲反射建立在數據類型系統上,讓我們從複習Go語言中的數據類型開始吧。

Go是靜態類型語言。每個變量都有靜態類型,換句話說就是在編譯期間變量的類型就已知並固定下來了,例如int、float32、*MyType、[]byte等等。如果我們聲明

type MyInt int

var i int

var j MyInt

這樣i就有類型int,j就有類型MyInt。變量i和j有不同的靜態類型。儘管他們有相同的底層類型,它們在不進行類型轉化的情況下無法互相賦值。

一類重要的類型是接口(interface)類型。接口表示一組固定的方法。一個接口變量能夠存儲任何實體(非接口)值,只要此值實現了接口的方法。一對著名的例子是io.Reader和io.Writer。它們來自io代碼包。

// 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存儲了什麼值,它的類型總是io.Reader:Go是靜態類型語言,r的靜態類型是io.Reader。

一個極其重要的接口類型是空接口

interface{}

它代表方法的空集合。由於任何值都有0個或更多的方法,所有空接口可以滿足任何值。

一些人說Go接口是動態類型,但這是一種誤解。它們是靜態類型:接口類型的一個變量總是有相同的靜態類型。就算運行時接口變量中的值發生了類型變化,這些值總會滿足這個接口的要求。

我們需要準確理解這一點,因爲反射和接口是緊密相關的。

一個接口的表示

Russ Cox 寫過一篇博客詳細討論了Go語言接口值的表示。這裏根據我們的需要簡要地敘述摘要。

接口類型的變量存儲一對數據:賦予的實體值和值類型的描述符。更加精確地說,值包括實現了接口的實體數據和描述其值的類型。例如,在

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

之後,r包含了(值,類型)對,即(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)。這非常便利:一個空接口可以包含任何值和與之對應的類型信息。

(這裏不需要類型判斷,因爲w總是滿足空接口。在把值從Reader移動到Writer的例子裏,我們需要顯式的類型判斷,因爲Writer的方法不是Reader的子集。)

一個重要的細節是接口內部必須保持(值,實體類型)這樣的形式,而非(值,接口類型)這樣的形式。接口不能包含接口的值。

現在我們準備好了解反射了。

反射第一定律

反射從接口值映射到反射對象

就基本功能而言,反射就是檢查存儲在接口變量內部(類型,值)對的一種機制。爲了能夠深入理解,我們需要知道代碼包reflect裏面的兩種類型Type和Value以及兩個函數reflect.TypeOf和reflect.ValueOf。這兩種類型幫助我們瞭解接口變量的內容。這兩個函數分別從接口值種取出reflect.Type和reflect.Value。(從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變量x給reflect.TyoeOf,而非接口值。但是它就在那裏;如同godoc所示,reflect.TypeOf的聲明包括了一個空接口

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

當我們調用reflect.TypeOf(x),x首先被保存在一個空接口裏,接着被當作參數傳遞,reflect.TypeOf打開這個空接口恢復出類型信息。

Reflect.ValueOf函數恢復值(從這裏開始我們忽略樣本,主要關注可執行代碼):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

打印出

value: <float64 Value>

(我們顯式地調用String方法,因爲fmt默認會深入reflect.Value展示內部實際數值。String則不如此。)

reflect.Type和reflect.Value都有很多方法來讓我們檢查和操作它們。一個重要的例子是Value有一個Type方法來返回reflect.Type。另外一個重要方法是Type和Value都有Kind方法來返回一個類型常量:Uint、Float64、Slice等等。像Int和Float這樣Value的方法讓我們提取內部數值:

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這樣的方法,但是爲了使用它們我們需要理解可設置性。這是反射的第三定律,將在後面討論。

反射代碼庫有一些屬性值得宣傳。首先,爲了保證API的簡單,Value的“getter”和“setter”方法使用可以承載該值的最大類型:對於所有的整型是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,儘管x的靜態類型是MyInt,而不是int。換句話說,Kind不能夠區分MyInt和int,儘管Type能。

 反射第二定律

反射從反射對象映射到接口值

類似於物理裏的反射,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)

打印出了反射對象v所包含的float64值。

我們甚至可以做得更好。fmt.Println,fmt.Printf等方法的參數,首先是作爲空接口值傳入內部,然後在fmt內部被打開,如我們之前的例子所示。因此打印reflect.Value內容所需要做的一切就是把Interface方法的返回值傳遞給打印函數:

fmt.Println(v.Interface())

(爲什麼不是fmt.Println(v)呢?因爲v是一個reflect.Value;我們需要它承載的實際值。)由於我們的值是float64,我們甚至可以使用浮點數格式:

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

然後得到這樣的結果

3.4e+00

這裏不需要將v.Interface()的返回值轉化爲float64;空接口值包含實際值類型信息,Printf會恢復它。

簡而言之,Interface方法是ValueOf函數的逆操作,除了它的結果總是靜態類型interface{}。

複習:反射從interface值進行到反射對象,然後再次返回。

反射第三定律

爲了修改反射對象,值必須具有可設置性(Settability)

第三定律是最微妙和令人困惑的,但是如果從第一定律出發是可以理解的。

下面是一些錯誤的代碼,但是值得研究。

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

如果你跑這段代碼,它會報令人疑惑的錯誤

panic: reflect.Value.SetFloat using unaddressable value

這裏的問題在於值7.1是沒有地址;v是不具有可設置性。可設置性是反射Value的一項性質,不是所有的反射Value都具有這個性質。

Value的CanSet方法可以報告一個Value的可變形;如下例

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

打印

settability of v: false

我們只能在可設置的Value上調用Set方法。那麼什麼是可設置性?

可設置性有點類似於可尋址能力,但是更加嚴格。這是一個性質:反射對象能夠修改內部存儲值。可設置性決定於反射對象是否包含了原始變量。比如

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

我們傳遞了x的一個副本給reflect.ValueOf,所以傳遞給reflect.ValueOf返回值內部包含的是x的副本。因此,如果

v.SetFloat(7.1)

是合法的,它不會更新x,儘管v看起來由x創建而來。實際上它會更新存儲在reflect Value對象內部的x副本,原始的x不會受到影響。這既令人困惑,又無用,所以這是不合法的。可設置性就是用來避免這個問題的性質。

這看起來奇怪,實際上並非如此。這本質上和之前的情形類似。想想看把x傳給函數:

f(x)

我們不會期望函數f能夠修改x,因爲我們傳遞了x的副本,而非x自己。如果我們想要函數f直接修改x,我們必須把x的地址傳遞給函數(x的指針):

f(&x)

這是既直觀,又常見,反射用相同的方式運作。如果我們想要通過反射修改x的值,我們必須傳遞指針給反射函數庫。

讓我們開始動手吧。首先我們像往常一樣初始化x,然後創建一個指向它的反射值,叫做p。

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不可變,但是p不是我們的目標,*p纔是。爲了得到p指向的對象,我們調用Elem方法,通過利用指針p,我們能夠修改反射Value,即v:

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

現在v是一個可變的反射對象,就如輸出所示

settability of v: true

另外,由於它表示x,我們最後可以使用v.SetFloat來修改x的值。

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

輸出就如預期:

7.1
7.1

反射可能令人難以理解,但是它就如我們所說的運作,儘管反射的Type和Value的使用掩蓋了它的本質。我們只需要記住,反射的Value需要變量的地址來修改它們的真實值。

結構體

我們以前的例子裏,v本身不是指針,v是從指針而得到的反射Value。當我們需要反射來修改結構體的字段時,我們就需要這種使用反射的方法。只要我們有結構體的地址,我們就能修改它的字段。

下面是個分析結構體值t的簡單例子。我們用結構體地址來創建反射對象,因爲我們打算修改它。然後我們設置typeOfT爲它的類型,以此來遍歷結構體的字段。需要注意的是,我們從結構體類型中獲取字段的名字,但是字段本身是尋常的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

此外,我們還要額外介紹一點關於可設置性:T的字段名是首字母大寫,因爲只有外界可訪問的字段纔可設置的。

因爲s包含可設置的反射對象,我們纔可以修改結構體的字段。

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

這是結果

t is now {77 Sunset Strip}

如果我們修改代碼,把&t改爲t,那麼對SetInt和SetString的調用就會失敗,因爲t的字段不可設置。

結論

這裏重複一遍反射定律:

  1. 反射從接口值映射到反射對象;
  2. 反射從反射對象映射到接口值;
  3. 爲了修改反射對象,其包含的值必須可設置。

一旦你理解了這些定律,Go語言中的反射會變得容易使用很多,儘管它仍然比較微妙。它是一個強大的工具,應該謹慎使用,如無必要就不要使用。

還有很多關於反射的知識我們還沒有覆蓋——頻道(channel)的發送和接收、分配內存、使用切片(slice)和映射(map)、調用方法和函數——但是這篇博客已經足夠長。我們會在後面的博客中討論它們。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章