Go 語言:The Laws of Reflection 中文版

浮x塵發表於2018-11-27

翻譯了一篇 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)
}
複製程式碼

任何型別只要實現 ReadWrite 方法即實現 io.Readerio.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 包中的兩個型別 TypeValue。這兩個型別提供訪問介面變數內部儲存的能力,並提供兩個簡單的函式 TypeOfValueOf 從介面變數中獲取 TypeValue(從 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)。

TypeValue 都包含許多檢測和操縱它們方法,一個重要的方法是 ValueType 方法返回對應的 Type 型別值。另一個重要的方法是兩者都擁有 Kind 方法返回常量基本型別(Uint、Float64、Slice 等)。通常 Value 上的 IntFloat 等函式作用是提取內部儲存的值。

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 類方法,使用它們必須理解可設定的概念,下面的第三反射定律詳細談到了這些。

反射庫中有幾個概念值得單獨拿出來講一講。

  1. 為了保持 API 簡單,且 Value 型別的 gettersetter 方法集可以操作比較大的值,所有無符號整數都使用 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.
複製程式碼
  1. Kind 方法返回靜態型別對應的基本型別,例如下面 x 的靜態型別是 MyInt 型別,基本型別是 reflect.Int
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
複製程式碼

第二反射定律

反射從反射物件中提取介面值。

就像物理反射定律,與第一條定律相反,從反射物件逆向可以得到介面值。

通過 ValueInterface 方法可以恢復介面值,實際上這個方法打包 typevalue 資訊放到空介面中返回。

// 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)?因為 vreflect.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 指標的值,實際上我們想修改的是 *pp 指向的值。我們需要呼叫 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
複製程式碼

儘管反射有些難以理解,但反射所做的一切都是語言層面支援的,也許 ValueType 會掩飾所發生的事情。只要保持清醒,關注 Value 在被修改時需要指向某個地址。

Structs

在上面的例子中 v 只是指向一個基本型別,而更通用的問題是修改結構體的欄位,當我們擁有結構體的指標,我們可以修改它的欄位值。

下面是一個簡單的例子用於分析一個結構體值。使用 T 型別的指標建立一個 Value,因此在後續可以修改 t

宣告並初始化 typeOfT 作為 t 的型別,並通過直接了當的方法 NumFieldField 提取出欄位的名字、型別和值。

  • ValueField 還是 Value,並且是可設定的;
  • TypeField 則是 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}
複製程式碼

關於