(譯)反射的法則 | 《 The Go Blog 》

jerrkill發表於2019-03-09

第一次翻譯,英文不好,歡迎指正,爭取有時間多看 & 翻譯一些外文文章。在翻譯過程中發現了很多的官方技術部落格,講得都很 nice !有時間了繼續譯過來。

原文章是 @Rob Pike Go 發表在 Blog Go 上的一篇官方關於 Golang reflection 的闡述 。《The Laws of Reflection》。
Interface 表示介面 不譯。
Reflection 表示 反射 不譯。
『』「」為附加解釋和說明。
ps 的內容非譯文內容

原文地址:https://blog.golang.org/laws-of-reflection

介紹

反射 是程式執行時檢查自身結構的一種能力,特別是通過型別;它是超程式設計的一種形式,同時也是『混亂』的重要來源。

在本文中,我們試圖通過解釋反射在 Go 中的工作原理來解釋。每種語言的反射模型都是不同的(甚至很多語言更本就不支援),但是這篇文章是關於 Go 的,因此本文的『反射』你應該理解為『Go 的反射』。

type 與 interface

因為反射建立在型別系統上,所以我們先來回顧一下 Go 中的型別。

Go 是靜態型別的。每個變數都有一個靜態型別,即在編譯時就已知並固定的一種型別: int, float32, *MyType, []byte 等等。我們來看如下程式碼,加深理解

type MyInt int

var i int
var j MyInt

i 是 int,j 是 MyInt。變數 i 跟 j具有不同的靜態型別,雖然它們具有相同的基礎型別,如果沒有型別轉換就不能將它們賦值給彼此。

比較重要的是 interface 型別「介面型別」,它是固定方法的集合。它定義了一組方法。只要該值實現介面的方法,介面變數就可以儲存任何具體(非介面)值。一個眾所周知的例子是來自 io 包的 Reader 和 Writer 型別:

ps: 如果沒有明白,看這 interface 型別 與 值

// Reader 是 io 包 Read 基礎方法的介面
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer 是 io 包 Writer 基礎方法的介面
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 {}

它表示空方法集合,並且完全可以儲存任何值,因為任何值都有零個或多個方法。

有人說Go的介面是動態型別的,這是誤導。他們是靜態型別的:一個介面型別的變數始終具有相同的靜態型別,即使在執行時儲存在介面變數中的值可能會改變型別,該值也將始終滿足介面的「實現該介面」。

我們需要準確地瞭解所有這些,因為 reflection 和 interface 密切相關。

interface 的表示

Russ Cox 寫了一篇關於 interface 值詳細的表示 detailed blog post ,這裡沒必要在這裡詳細的闡述,但是有一個簡單的摘要:

interface 型別的變數儲存是成對的:賦給變數的具體值,以及該值的型別描述符。更確切的說:值是實現 interface 的基礎具體資料項,型別描述符描述該項的完整型別

先來看例子

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之外的方法; 即使 interface 值僅提供對 Read (io.Reader 中只有 Read 方法)方法的訪問,內部值也包含有關該值的所有型別資訊。所以我們還可以這麼玩

var w io.Writer
w = r.(io.Writer) // 型別斷言

此賦值中的表示式是型別斷言;它斷言的內容是 r 中的專案也實現了 io.Writer,因此我們可以將它分配給 w。 賦值後,w 將包含(tty,* os.File)。還可以這樣做:

var empty interface{}
empty = w

我們的空 interface 將包含跟上面一樣的兩項(tty, *os.File)。這樣一來,一個空介面可以儲存任何值,幷包含我們可能需要的有關該值的所有資訊。

這裡我們不需要型別斷言,因為靜態地知道 w 實現了空 interface。在我們將值從 Reader 分配到 Writer 的示例中,我們需要顯式並使用型別斷言,因為 Writer 的方法不是 Reader 的子集

一個重要的細節是 interface 內的對總是以(值,具體型別)的形式存在,而不能是以(值,interface 型別)的形式存在。 interface 不包含 interface 值。

Relection 從 interface 值到反射物件

本質上講,反射只是一種檢查儲存在介面變數中的型別和值對的機制。
在開始之前,這裡有 reflect 包的兩個型別 Type 和 Value 需要去了解。
這兩種型別可以訪問 interface 變數的內容,兩個簡單的函式:

  • reflect.TypeOf 從 interface 值中檢索型別 reflect.Type
  • reflect.ValueOf 從 interface 值中檢索值 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

您可能想知道 interface 在哪裡,因為程式看起來像是將 float64 變數 x 而不是 interface 值傳遞給 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 首先儲存在一個空 interface 中,然後作為引數傳遞; reflect.TypeOf 函式取出該空 interface 以恢復型別資訊。
同理,reflect.ValueOf 函式可以恢復該值。

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

我們明確地呼叫 String 方法,因為預設情況下,fmt 包會去拿 reflect.Value 以顯示其中的具體值。 String 方法沒有。

reflect.Typereflect.Value 提供了大量方法供我們去檢索或者操作他們。值得一提的是:relect.Value 有一個獲取值型別的方法(返回該值的 reflect.Type
Kind 方法
另一個是 Type 和 Value 都有一個 Kind 方法,它返回一個常量,指示儲存的專案型別:Uint,Float64,Slice 等等
來看下面的例子

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 這樣的方法,我們將在 反射的 第三點鐘討論,

Reflection 庫有一些需要單獨提出來講的地方:

  • 為了保持 API 簡潔,Value 的「getter」和「setter」方法是對可以儲存值的最大型別進行操作的。比如所有符號整數都是 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,儘管它的靜態變數型別是 MyInt,不是 int;換句話說:即使 Type 可以,Kind 也無法區分 int 和 MyInt。

從反射物件到 interface 值

像物理反射一樣,Go 中的反射會產生自己的反轉。

給定一個 reflect.Value,我們可以使用 Interface 方法恢復 interface 值

// Interface 方法以空 interface 型別返回 v 的值.
func (v Value) Interface() interface{}

所以就有如下寫法

y := v.Interface().(float64) // y 將有 float64 型別
fmt.Println(y)

輸出由反射物件 v 表示的 float64 值。

不過,我們可以做得更好。 fmt.Println,fmt.Printf 等的引數都是 空interface 值傳遞,然後由 fmt 包內部處理,就像我們在前面的示例中所做的那樣。 因此,正確列印 reflect.Value 的內容的姿勢是將 Interface 方法的結果傳遞給格式化的列印即可:

fmt.Println(v.Interface())

為什麼不是 fmt.Println(v)?

因為 v 是一個 reflect.Value;我們想要的是 v 的具體值。

同樣的道理,我們也沒有必要將 v.Interface()的結果型別斷言為 float64; 空 interface 值內部具有具體值的型別資訊,Printf將恢復它。

簡而言之,Interface 方法是 ValueOf 函式的反轉,除了它的結果總是靜態型別 空 interafce (interface {})。

最後重聲一邊:Reflection『反射』 是從 interface 值到反射物件再返回到 interface 值。

要修改反射物件,該值可設定

第三定律是最微妙和最令人困惑的,但是如果我們從第一原則開始就很容易理解

我們先研究一下下面錯誤的程式碼

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

如果執行會 panic(可以理解為 php throw) 到費解的資訊:

panic: reflect.Value.SetFloat using unaddressable value

問題不在 7.1 不可定址,而是 v 是不可設定的。
可設定性 是反射值的屬性,並非所有反射值都具有它。我們通過 CanSet 方法來判斷;列如列印 v 的 可設定性

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

在不可設定的值上呼叫 Set 方法是錯誤的,那麼怎麼樣才是可設定的呢?

可設定性 有點像可定址,但是又比他更嚴格。
反射物件可以修改被反射物件的實際儲存的屬性。 可設定性取決於反射物件是否包含原始項。『PS 說了那麼多,其實就是要傳 x 的地址、而不能傳 x 的 copy』

我們將x的副本傳遞給reflect.ValueOf,因此作為reflect.ValueOf的引數建立的介面值是x的副本,而不是x本身。 因此,如果宣告

v.SetFloat(7.1)

被允許成功,它不會更新x,即使v看起來像是從x建立的。 相反,它會更新儲存在反射值中的x的副本,而x本身將不受影響。 這將是混亂和無用的,因此它是非法的,可設定性是用於避免此問題的屬性。

如果這看起來很奇怪,那就不是了。 這實際上是一種不尋常的服裝中常見的情況。 考慮將x傳遞給函式:

f(x)

如果我們不想 f() 修改 x 的值,我們就傳 x 的 copy,反之我們要傳 地址。

只有傳地址,給反射庫一個指向要修改值得指標,反射庫才能夠修改值。

我們在看如下程式碼

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: 傳 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 方法,它通過指標間接,並將結果儲存在一個名為 v的反射值中。

正確的姿勢應該是:

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

列印

settability of v: true

反射可能很難理解,但它正在做語言所做的事;儘管通過反射的 Type 和 Value 可以模擬正在發生的事情,請記住:需要通過傳遞地址才能修改他們所代表的內容

Reflection can be hard to understand but it's doing exactly what the language does, albeit through reflection Types and Values that can disguise what's going on.

「ps:感覺這句譯得不對,望大佬糾正」

ps: disguise 是偽裝的意思,這裡用偽裝似乎不合適,我感覺作者要表達的意思是:反射的 Type Value 可以偽裝 正在發生的事 = 反射的 Type Value 可以獲取程式執行時的狀態

高度自律,深度思考,以勤補拙

相關文章