Go 部落格
反射定律
引言
計算中的反射是指程式能夠檢查自身結構的能力,特別是透過型別;它是一種超程式設計形式。它也是一個主要的混淆來源。
在本文中,我們試圖透過解釋反射在 Go 中的工作原理來澄清問題。每種語言的反射模型都不同(許多語言根本不支援反射),但本文是關於 Go 的,因此在本文的其餘部分中,“反射”一詞應理解為“Go 中的反射”。
2022年1月補充說明:這篇博文寫於2011年,早於 Go 中的引數多型(又名泛型)。儘管該語言的這一發展並沒有使文章中的任何重要內容變得不正確,但為了避免混淆熟悉現代 Go 的人,文章在一些地方進行了調整。
型別和介面
因為反射建立在型別系統之上,所以我們從回顧 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
,來自 io 包 的 Reader
和 Writer
型別。
// 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{}
或其等價別名,
any
它表示空方法集,任何值都滿足它,因為每個值都具有零個或更多方法。
有些人說 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
方法的子集。)
一個重要的細節是,介面變數內部的對總是具有 (值, 具體型別) 的形式,不能具有 (值, 介面型別) 的形式。介面不持有介面值。
現在我們準備好進行反射了。
反射的第一定律
1. 反射從介面值到反射物件。
在基本層面,反射只是一種檢查儲存在介面變數中的型別和值對的機制。首先,我們需要了解 reflect 包 中的兩種型別:Type 和 Value。這兩種型別提供了對介面變數內容的訪問,兩個簡單的函式,稱為 reflect.TypeOf
和 reflect.ValueOf
,從介面值中檢索 reflect.Type
和 reflect.Value
片段。(此外,從 reflect.Value
很容易獲取相應的 reflect.Type
,但我們現在將 Value
和 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.TypeOf
,而不是介面值。但它在那裡;正如 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.Value
的 Type
。另一個是 Type
和 Value
都有一個 Kind
方法,返回一個常量,指示儲存的專案型別:Uint
、Float64
、Slice
等。此外,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
這樣的方法,但要使用它們,我們需要理解可設定性,這是反射第三定律的主題,下面將討論。
反射庫有一些值得單獨指出的特性。首先,為了保持 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
無法區分 int
和 MyInt
,儘管 Type
可以。
反射的第二定律
2. 反射從反射物件到介面值。
就像物理反射一樣,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
包發生了一個更改,使其會自動解包 reflect.Value
,因此我們只需說
fmt.Println(v)
即可獲得相同的結果,但為了清晰起見,我們在此處保留 .Interface()
呼叫。)
由於我們的值是 float64
,我們甚至可以使用浮點格式,如果需要的話
fmt.Printf("value is %7.1e\n", v.Interface())
在這種情況下得到
3.4e+00
同樣,無需將 v.Interface()
的結果進行型別斷言為 float64
;空介面值內部包含具體值的型別資訊,Printf
會將其恢復。
簡而言之,Interface
方法是 ValueOf
函式的逆操作,只是其結果始終是靜態型別 interface{}
。
重申:反射從介面值到反射物件,再返回。
反射的第三定律
3. 要修改反射物件,該值必須是可設定的。
第三定律是最微妙和令人困惑的,但如果我們從基本原理開始,就很容易理解。
這是一些不起作用的程式碼,但值得研究。
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
的副本,而不是 x
本身。因此,如果語句
v.SetFloat(7.1)
被允許成功,它不會更新 x
,儘管 v
看起來是從 x
建立的。相反,它會更新儲存在反射值內部的 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
指向的值,我們呼叫 Value
的 Elem
方法,它透過指標進行間接引用,並將結果儲存到一個名為 v
的反射 Value
中
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
反射可能很難理解,但它所做的事情與語言完全相同,儘管是透過反射 Types
和 Values
來進行的,這可能會掩蓋正在發生的事情。請記住,反射 Values 需要某物的地址才能修改它們所代表的內容。
結構體
在我們前面的例子中,v
本身不是指標,它只是從指標派生出來的。這種情況常見的發生方式是當使用反射修改結構體的欄位時。只要我們擁有結構體的地址,我們就可以修改它的欄位。
這是一個簡單的示例,它分析一個結構體值 t
。我們使用結構體的地址建立反射物件,因為稍後我們將要修改它。然後我們將 typeOfT
設定為其型別,並使用直接的方法呼叫遍歷欄位(有關詳細資訊,請參閱 reflect 包)。請注意,我們從結構體型別中提取欄位名稱,但欄位本身是常規的 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}
如果我們修改程式,使 s
是從 t
而不是 &t
建立的,那麼對 SetInt
和 SetString
的呼叫將會失敗,因為 t
的欄位將不可設定。
結論
再此重申反射定律
-
反射從介面值到反射物件。
-
反射從反射物件到介面值。
-
要修改反射物件,該值必須是可設定的。
一旦你理解了這些定律,Go 中的反射就會變得更容易使用,儘管它仍然很微妙。它是一個強大的工具,應謹慎使用,除非絕對必要,否則應避免使用。
關於反射還有很多我們沒有涉及到的內容——在通道上傳送和接收,分配記憶體,使用切片和對映,呼叫方法和函式——但這篇博文已經足夠長了。我們將在以後的文章中介紹其中一些主題。
下一篇文章: Go image 包
上一篇文章: 兩次 Go 演講:《Go 中的詞法掃描》和《Cuddle:一個 App Engine 演示》
部落格索引