[翻譯]反射的規則

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

第一次知道反射的時候還是許多年前在學校裡玩 C# 的時候。那時總是弄不清楚這個複雜的玩意能有什麼實際用途……然後發現 Java 有這個,後來發現 PHP 也有了,再後來 Objective-C、Python 什麼的也都有……甚至連 Delphi 也有 TRttiContext……反射無處不在!!!

Go 作為一個集大成的現代系統級語言,當然也需要有,必須的!

大牛 Rob Pike 的這篇文章相對全面的介紹了 Go 語言中的反射的機制已經使用。覺得值得研讀,於是翻譯於此。

———-翻譯分割線———-

反射的規則

在運行時反射是程式檢查其所擁有的結構,尤其是類型的一種能力;這是元編程的一種形式。它同時也是造成混淆的重要來源。

在這篇文章中將試圖明確解釋在 Go 中的反射是如何工作的。每個語言的反射模型都不同(同時許多語言根本不支援反射)。不過這篇文章是關於 Go 的,因此接下來的內容“反射”這一詞表示“在 Go 中的反射”。

類型和介面

由於反射構建於類型系統之上,就從複習一下 Go 中的類型開始吧。

Go 是靜態類型的。每一個變數有一個靜態類型,也就是說,有一個已知類型並且在編譯時間就確定下來了:int,float32,*MyType,[]byte 等等。如果定義

type MyInt intvar i intvar j MyInt

那麼 i 的類型為 int 而 j 的類型為 MyInt。即使變數 i 和 j 有相同的底層類型,它們仍然是有不同的靜態類型的。未經轉換是不能相互賦值的。

在類型中有一個重要的類別就是介面類型,表達了固定的一個方法集合。一個介面變數可以儲存任意實際值(非介面),只要這個值直線了介面的方法。眾所周知的一個例子就是 is io.Reader 和 io.Writer,來自 io 包的類型 Reader 和 Writer:

// Reader 是包裹了基礎的 Read 方法的介面。.type Reader interface {    Read(p []byte) (n int, err os.Error)}// Writer 是包裹了基礎 Write 方法的介面。type Writer interface {    Write(p []byte) (n int, err os.Error)}

任何用這個聲明實現了 Read(或 Write)方法的類型,可以說它實現了 io.Reader(或 io.Writer)。基於本討論來說,這意味著 io.Reader 類型的變數可以儲存任意值,只要這個值的類型實現了 Read 方法:

var r io.Readerr = os.Stdinr = bufio.NewReader(r)r = new(bytes.Buffer)// 等等

有一個事情是一定要明確的,不論 r 儲存了什麼值,r 的類型總是 io.Reader:Go 是靜態類型,而 r 的靜態類型是 io.Reader。

介面類型的一個極端重要的例子是空介面:

interface{}

它表示空的方法集合,由於任何值都有另個或者多個方法,所以任何值都可以滿足它。

也有人說 Go 的介面是動態類型的,不過這是一種誤解。它們是靜態類型的:介面類型的變數總是有著相同的靜態類型,這個值總是滿足空介面,只是儲存在介面變數中的值運行時也有可能被改變類型。

對於所有這些都必須嚴謹的對待,因為反射和介面密切相關。

介面的特色

Russ Cox 已經寫了一篇詳細介紹 Go 中介面值的特點的博文。所以無需在這裡重複整個故事了,不過簡單的總結還是必要的。

介面類型的變數儲存了兩個內容:賦值給變數實際的值和這個值的類型描述。更準確的說,值是底層實現了介面的實際資料項目,而類型描述了這個項目完整的類型。例如下面,

var r io.Readertty, 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 方法的可能,但是內部包含了這個值的完整的類型資訊。這也就是為什麼可以這樣做:

var w io.Writerw = r.(io.Writer)

在這個賦值中的斷言是一個類型斷言:它斷言了 r 內部的條目同時也實現了 io.Writer,因此可以賦值它到 w。在賦值之後,w 將會包含 (tty, *os.File)。跟在 r 中儲存的一致。介面的靜態類型決定了哪個方法可以通過介面變數調用,即便內部實際的值可能有一個更大的方法集。

接下來,可以這樣做:

var empty interface{}empty = w

而空介面值 e 也將包含同樣的 (tty, *os.File)。這很方便:空介面可以儲存任何值同時保留關於那個值的所有資訊。

(這裡無需類型斷言,因為 w 是肯定滿足空介面的。在這個例子中,將一個值從 Reader 變為 Writer,由於 Writer 的方法不是 Reader 的子集,所以就必須明確使用類型斷言。)

一個很重要的細節是介面內部的對總是 (value, 實際類型) 的格式,而不會有 (value, 介面類型) 的格式。介面不能儲存介面值。

現在準備好來反射了。

反射的第一條規則

1. 從介面值到反射對象的反射。

在基本的層面上,反射只是一個檢查儲存在介面變數中的類型和值的演算法。從頭來講,在 reflect 包中有兩個類型需要瞭解:Type 和 Value。這兩個類型使得可以提供者變數的內容,還有兩個簡單的函數,reflect.TypeOf 和 reflect.ValueOf,從介面值中分別擷取 reflect.Type 和 reflect.Value。(同樣,從 reflect.Value 也很容易能夠獲得 reflect.Type,不過這裡讓 Value 和 Type 在概念上分離了。)

從 TypeOf 開始:

package mainimport (        "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 返回 interface{} 中的值反射的類型。func TypeOf(i interface{}) Type

當調用 reflect.TypeOf(x) 的時候,x 首先儲存於一個作為參數傳遞的空介面中;reflect.TypeOf 解包這個空介面來還原類型資訊。

reflect.ValueOf 函數,當然就是還原那個值(從這裡開始將會略過那些概念樣本,而聚焦於可執行檔代碼):

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

列印

value: <float64 Value>

reflect.Type 和 reflect.Value 都有許多方法用於檢查和操作它們。一個重要的例子是 Value 有一個 Type 方法返回 reflect.Value 的 Type。另一個是 Type 和 Value 都有 Kind 方法返回一個常量來表示類型:Uint、Float64、Slice 等等。同樣 Value 有叫做 Int 和 Float 的方法可以擷取儲存在內部的值(跟 int64 和 float64 一樣):

var x float64 = 3.4v := reflect.ValueOf(x)fmt.Println("type:", v.Type())fmt.Println("kind is float64:", v.Kind() == reflect.Float64)fmt.Println("value:", v.Float())

列印

type: float64kind is float64: truevalue: 3.4

同時也有類似 SetInt 和 SetFloat 的方法,不過在使用它們之前需要理解可設定性,這部分的主題在下面的第三條軍規中討論。

反射庫有著若干特性值得特別說明。首先,為了保持 API 的簡潔,“擷取者”和“設定者”用 Value 的最寬泛的類型來處理值:例如,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 返回一個 uint64.

第二個特性是反射對象的 Kind 描述了底層類型,而不是靜態類型。如果一個反射對象包含了使用者定義的整數類型的值,就像

type MyInt intvar x MyInt = 7v := reflect.ValueOf(x)

v 的 Kind 仍然是 reflect.Int,儘管 x 的靜態類型是 MyInt,而不是 int。換句話說,Kind 無法從 MyInt 中區分 int,而 Type 可以。

反射的第二條規則

2. 從反射對象到介面值的反射。

如同物理中的反射,在 Go 中的反射也存在它自己的鏡像。

從 reflect.Value 可以使用 Interface 方法還原介面值;方法高效的打包類型和值資訊到介面表達中,並返回這個結果:

// Interface 以 interface{} 返回 v 的值。func (v Value) Interface() interface{}

可以這樣作為結果

y := v.Interface().(float64) // y 將為類型 float64。fmt.Println(y)

通過反射對象 v 可以列印 float64 的表達值。

然而,還可以做得更好。fmt.Println,fmt.Printf 和其他所有傳遞一個空介面值作為參數的,由 fmt 包在內部解包的方式就像之前的例子這樣。因此正確的列印 reflect.Value 的內容的方法就是將 Interface 方法的結果傳遞給格式化列印:formatted print routine:

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{}。

回顧:反射可以從介面值到反射對象,也可以反過來。

反射的第三條規則

3. 為了修改反射對象,其值必須可設定。

第三條軍規是最為精細和迷惑的,但是如果從第一個規則開始,還是足以讓人明白的。

這裡有一些不能工作的代碼,值得學習。

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

如果運行這個代碼,它報出神秘的 panic 訊息

panic: reflect.Value.SetFloat using unaddressable value

問題不在於值 7.1 不能地址化;在於 v 不可設定。設定性是反射值的一個屬性,並不是所有的反射值有它。

值的 CanSet 方法提供了值的設定性;在這個例子中,

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

列印

settability of v: false

對不可設定值調用 Set 方法會有錯誤。但是什麼是設定性?

設定性有一點點像地址化,但是更嚴格。這是用於建立反射對象的時候,能夠修改實際儲存的屬性。設定性用於決定反射對象是否儲存原始項目。當這樣

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

就傳遞了一個 x 的副本到 reflect.ValueOf,所以介面值作為 reflect.ValueOf 參數建立了 x 的副本,而不是 x 本身。因此,如果語句

v.SetFloat(7.1)

允許執行,雖然 v 看起來是從 x 建立的,它也無法更新 x。反之,如果在反射值內部允許更新 x 的副本,那麼 x 本身不會收到影響。這會造成混淆,並且毫無意義,因此這是非法的,而設定性是用於解決這個問題的屬性。

這很神奇?其實不是。這實際上是一個常見的非同尋常的情況。考慮傳遞 x 到函數:

f(x)

由於傳遞的是 x 的值的副本,而不是 x 本身,所以並不期望 f 可以修改 x。如果想要 f 直接修改 x,必須向函數傳遞 x 的地址(也就是,指向 x 的指標):

f(&x)

這是清晰且熟悉的,而反射通過同樣的途徑工作。如果希望通過反射來修改 x,必須向反射庫提供一個希望修改的值的指標。

來試試吧。首先像平常那樣初始化 x,然後建立指向它的反射值,叫做 p。

var x float64 = 3.4p := reflect.ValueOf(&x) // 注意:擷取 X 的地址。fmt.Println("type of p:", p.Type())fmt.Println("settability of p:" , p.CanSet())

這樣輸出為

type of p: *float64settability of p: false

反射對象 p 並不是可設定的,但是並不希望設定 p,(實際上)是 *p。為了獲得 p 指向的內容,調用值上的 Elem 方法,從指標間接指向,然後儲存反射值的結果叫做 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.17.1

反射可能很難理解,但是語言做了它應該做的,儘管底層的實現被反射的 Type 和 Value 隱藏了。務必記得反射值需要某些內容的地址來修改它指向的東西。

結構體

在之前的例子中 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 = 231: 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 中的反射的這些規則,就會變得容易使用了,雖然它仍然很微妙。這是一個強大的工具,除非真得有必要,否則應當避免使用或小心使用。

還有大量的關於反射的內容沒有涉及到——channel 上的發送和接收、分配記憶體、使用 slice 和 map、調用方法和函數——但是這篇文章已經夠長了。這些話題將會在以後的文章中逐一講解。

Rob Pike 撰寫,2011年9月

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.