Go 語言:The Laws of Reflection 中文版
翻譯了一篇 Go 官方部落格介紹反射的文章:
簡介
在電腦科學中,反射是一種在執行時檢測自身結構(型別)的能力,反射構成超程式設計的基礎,也是混亂的來源。
在這篇文章中我們會嘗試澄清 Go 語言中的反射如何運作,每個語言的反射模型都不一樣(典型如 Java),很多語言甚至不支援反射,因此在這篇文章中說明的只是 Go 語言反射。
型別和介面
因為整個反射模型構建在型別系統之上,我們先複習一遍 Go 中的型別。
Go 是靜態型別語言,任何變數在編譯時都有明確的型別,如 int、float32、*MyType, []byte
等型別...
type MyInt int
var i int
var j MyInt
複製程式碼
變數 i
的型別為 int
,變數 j
的型別為 MyInt
。它們兩個明顯有著不同的靜態型別,除此之外又有著相同的基本型別 int
。因為靜態型別不同,所以兩者必須在轉換後才能進行賦值。
介面型別是型別系統中非常重要的一個分類,其代表約定的方法集。介面變數可以儲存任意的值,只要該值實現對應的介面方法集。io
包中的 io.Reader 和 io.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{}
:
interface{}
複製程式碼
其方法集為空表示任何型別都實現空介面,任何型別的值都可以對其賦值。
有些人說介面是動態型別,這種說法是不對的,它們是靜態型別。一個介面型別變數總是擁有固定的靜態型別,即使在執行時儲存在介面中的值有不同的型別(型別實現介面的方法集)。
我們需要理解這些概念是因為反射和介面密切相關。
介面值
Russ Cox 寫了一篇文章 Go Data Structures: Interfaces 詳細解釋了 Go 語言種的介面值。再次不必重複文章中的概念,下面對文章的簡單總結:
介面型別變數儲存一對值:
- value:賦值給介面型別變數的實際值;
- type:實際值的型別資訊。
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
方法,介面中的型別值(the value inside)攜帶所有關於值的型別資訊,因此我們可以實現下面的操作(型別資訊都有自然可以斷言):
var w io.Writer
w = r.(io.Writer)
複製程式碼
上面的賦值表示式稱為型別斷言,其斷言 r
介面變數內部儲存的 (value, type)
實現 io.Writer
介面,所以我們可以將其賦值給 w
。在賦值結束後,w
包含 (tty, *os.File)
,與我們之前在 r
中看到的一樣。介面的靜態型別決定了哪些方法可以通過該介面變數呼叫,即使內部儲存的 (value, type)
擁有更大的方法集。
繼續,我們還可以這樣做:
var empty interface{}
empty = w
複製程式碼
我們的空介面 empty
依然會在內部儲存相同的 (tty, *os.File)
。這意味著空介面可以儲存任何值並擁有我們需要的所有資訊。
在對空介面賦值時沒有使用型別斷言,因為任何值都滿足空介面,w
顯然實現空介面(方法集是空介面的超集)。而上面的 Reader
轉換的 Writer
則不一樣,我們需要顯式使用型別斷言是因為 Reader
介面不是 Writer
介面的超集。
一個重要的細節是介面內部總是儲存 (value, concrete type),並不能儲存 (value, interface type),介面內部並不儲存介面值!
現在我們準備好研究反射了。
第一反射定律
反射從介面值中提取反射物件。
在最基本的概念上,反射只是一種檢測儲存在介面中的 type 和 value 的機制。因此我們需要理解 reflect 包中的兩個型別 Type 和 Value。這兩個型別提供訪問介面變數內部儲存的能力,並提供兩個簡單的函式 TypeOf
和 ValueOf
從介面變數中獲取 Type
和 Value
(從 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
變數作為引數給 TypeOf
函式,而不是介面變數。實際上 TypeOf
函式簽名中的引數是空介面,x
會先賦值給空介面,然後作為函式引數傳遞,TypeOf
函式內部處理空介面恢復型別資訊 Type
。
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
複製程式碼
ValueOf
函式也是通過類似的方法得到 Value
型別變數。
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
複製程式碼
輸出:
value: <float64 Value>
複製程式碼
直接呼叫
String
方法是因為在預設情況下fmt
包直接深入Value
顯示內部真正的值(3.4)。
Type
和 Value
都包含許多檢測和操縱它們方法,一個重要的方法是 Value
的 Type
方法返回對應的 Type
型別值。另一個重要的方法是兩者都擁有 Kind
方法返回常量基本型別(Uint、Float64、Slice
等)。通常 Value
上的 Int
、Float
等函式作用是提取內部儲存的值。
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
作為引數和返回值。如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
方法返回靜態型別對應的基本型別,例如下面x
的靜態型別是MyInt
型別,基本型別是reflect.Int
。
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
複製程式碼
第二反射定律
反射從反射物件中提取介面值。
就像物理反射定律,與第一條定律相反,從反射物件逆向可以得到介面值。
通過 Value
的 Interface
方法可以恢復介面值,實際上這個方法打包 type
和 value
資訊放到空介面中返回。
// 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
等函式做得更好,這些函式接收空介面值作為引數,並像上面學到的一樣對這些引數進行解包。因此如果要直接列印 reflect.Value
的內容需要使用 Interface
函式獲取介面值後傳遞。
fmt.Println(v.Interface())
複製程式碼
為什麼不直接使用 fmt.Println(v)
?因為 v
是 reflect.Value
型別的值,我們想要的是實際儲存的值。
fmt.Printf("value is %7.1e\n", v.Interface())
複製程式碼
輸出
3.4e+00
複製程式碼
再次強調,這裡不需要使用型別斷言 v.Interface()
到 float64
是因為空介面內部儲存的值和型別在 Printf
函式內部會被恢復。
簡而言之,Interface
方法是 ValueOf
方法的逆方法,除了返回值總是靜態型別 interface{}
。
第三反射定律
要修改反射物件,值必須是可設定的。
第三條定律是非常容易使人迷惑的,如果我們從第一條原則開始理解就簡單多了。
下面是一些不能工作但值得學習的程式碼:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
複製程式碼
如果你允許這些程式碼,會產生 panic
錯誤。
panic: reflect.Value.SetFloat using unaddressable value
複製程式碼
這個錯誤不是說值 7.1 是 not addressable
的,而是說 v
是不可設定的,可設定(settability) 是 Value
的重要屬性,並不是所有 Value
都是可設定的。
CanSet
方法檢測值是否可設定。
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
複製程式碼
輸出:
settability of v: false
複製程式碼
在不可設定的 Value
上呼叫 Set
方法會出錯,那什麼是可設定?
可設定(settability)有一點像地址可達(addressability),嚴格上說:**這是一個反射物件可以修改實際建立該反射物件的值的屬性,可設定與否取決於反射物件是否持有原始值(指標)。
var x float64 = 3.4
v := reflect.ValueOf(x)
複製程式碼
當我們傳遞一個 x 的拷貝給 reflect.ValueOf
,所以引數的空介面值內部持有 x
的拷貝而不是 x
本身。
v.SetFloat(7.1)
複製程式碼
因此,如果這個語句執行成功,也不會更新 x
,即使 v
看起來是通過 x
建立的。反而會更新儲存在 Value
中的複製值,真正的 x
並不受影響。上述情況容易產生混亂和困擾,因此在語言層面講這種行為定義為非法的,通過判斷可設定屬性避免這個問題。
如果上面看起來有些奇怪,實際上並非如此,這只是熟悉情境的奇怪包裝罷了(值傳遞和指標傳遞,拿到指標才可以修改原始的值)。
思考傳遞 x
給函式。
f(x)
複製程式碼
我們不會期望 f
能給修改 x
的值,因為我們傳遞給 f
的是 x
值的拷貝,而不是 x
本身。如果我們想要直接修改 x
,我們必須傳遞 x
的地址(指標)。
f(&x)
複製程式碼
上面的方式非常簡單和直接,並且反射的工作原理也是一樣的。如果我們想通過反射修改 x
,我們必須傳遞指標給 Value
。
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
方法,其間接通過指標取到原始值,並將結果儲存到新的 Value
值中返回。
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
複製程式碼
現在 v
是可設定的反射物件。
settability of v: true
複製程式碼
自從 v
開始代表 x
,我們最終可以使用 v.SetFloat
方法修改 x
的值:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
複製程式碼
輸出:
7.1
7.1
複製程式碼
儘管反射有些難以理解,但反射所做的一切都是語言層面支援的,也許 Value
和 Type
會掩飾所發生的事情。只要保持清醒,關注 Value
在被修改時需要指向某個地址。
Structs
在上面的例子中 v
只是指向一個基本型別,而更通用的問題是修改結構體的欄位,當我們擁有結構體的指標,我們可以修改它的欄位值。
下面是一個簡單的例子用於分析一個結構體值。使用 T
型別的指標建立一個 Value
,因此在後續可以修改 t
。
宣告並初始化 typeOfT
作為 t
的型別,並通過直接了當的方法 NumField
和 Field
提取出欄位的名字、型別和值。
Value
的Field
還是Value
,並且是可設定的;Type
的Field
則是StructField
。
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
包含可設定的反射物件(Elem 獲得原始物件),我們可以修改結構體的欄位。
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
複製程式碼
輸出:
t is now {77 Sunset Strip}
複製程式碼
關於
- My Blog
- My Wechat: