Go 之反射基本思想

Dennis_Ritchie發表於2019-12-26

介紹

計算反射是程式檢查其自身結構的能力,尤其是透過型別。這是超程式設計的一種形式。這也是造成混亂的重要原因。

在本文中,我們嘗試透過解釋反射在Go中的工作原理來澄清一些事。 每種語言的反射模型是不同的(許多語言根本不支援它),但是本文是關於Go的,因此對於本文的其餘部分,應將“反射”一詞理解為“ Go中的反射”。

型別和介面

因為反射建立在型別系統上,所以讓我們從Go語言中的型別開始。

Go是靜態型別的。每個變數都有一個靜態型別,也就是在編譯時已知並固定的一種型別:int,float32,* MyType,[] byte等。如果我們宣告

type MyInt int

var i int
var j MyInt

那麼我的型別為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{}

它表示空方法集,並且由任何值完全滿足,因為任何值具有零個或多個方法。

有人說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的子集。)

一個重要的細節是,介面內的對始終具有形式(值,具體型別),而不能具有形式(值,介面型別)。介面不儲存介面值。 現在我們準備好反射了。

反射的第一條定律

反射從介面值到反射物件。

在基本級別上,反射只是一種檢查儲存在介面變數中的型別和值對的機制。首先,我們需要在反射包中瞭解兩種型別:TypeValue。這兩種型別允許訪問介面變數的內容,還有兩個簡單的函式,稱為reflect.TypeOfreflect.ValueOf,從介面值中檢索reflect.Typereflect.Value。(此外,從reflect.Value可以很容易地到達reflect.Type,但是現在讓Value和Type概念保持分離。)

讓我們從TypeOf開始:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var f float64
    fmt.Println(reflect.TypeOf(f))
}

檢視列印結果:

float64

您可能想知道介面在哪裡,因為該程式看起來像是在向reflect.TypeOf傳遞float64變數x而不是介面值來反映。但是它在那裡;當godoc報告時,reflect.TypeOf的簽名包括一個空介面:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

當我們呼叫reflect.TypeOf(x)時,x首先儲存在一個空介面中,然後將其作為引數傳遞; Reflection.TypeOf解壓縮該空介面以恢復型別資訊。

當然,reflect.ValueOf函式可以恢復值(從這裡開始,我們將省略樣板並只關注可執行程式碼):

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

列印結果:

<float64 Value>

(我們明確地呼叫String方法,因為預設情況下,fmt包會挖掘到一個reflect.Value以顯示其中的具體值。String方法不會。)

reflect.Typereflect.Value都有很多方法可以讓我們檢查和操作它們。一個重要的示例是Value具有Type方法,該方法返回reflect.ValueType。另一個是TypeValue都有Kind方法,該方法返回一個常量,指示儲存的專案型別:Uint,Float64,Slice,等等。同樣,使用諸如Int和Float之類的Value方法可以讓我們獲取儲存在其中的值(如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

還有諸如SetIntSetFloat之類的方法,但是要使用它們,我們需要了解可沉降性,這是第三反射定律的主題,下面將進行討論。

反射庫具有幾個值得一提的屬性。 首先,為使API保持簡單,Value的“getter”和“setter”方法在可以容納該值的最大型別上執行:例如,所有有符號整數的int64。 也就是說,ValueInt方法返回一個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.

檢視結果:

type: uint8
kind is uint8:  true

第二個屬性是反射物件的Kind方法描述基礎型別,而不是靜態型別。如果反射物件包含使用者定義的整數型別的值,例如:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyInt int
    var t MyInt = 10
    v := reflect.ValueOf(t)
    fmt.Println(v.Type())
    fmt.Println(v.Kind())
}

檢視控制檯結果:

main.MyInt
int

即使t的靜態型別是MyInt而不是int,v的種類仍會反映int。換句話說,即使Type可以,Kind也不能將My和Int區別開。

反射第二定律

反射從反射物件到介面值

像物理反射一樣,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.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.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果您執行此程式碼,異常將會丟擲:

panic: reflect.Value.SetFloat using unaddressable value

問題不在於值7.1是不可定址的。這是v不可設定的。可設定性是反射值(Value)的屬性,並非所有反射值都具有它。

值的CanSet方法報告值的可設定性;就我們而言:

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

列印結果:

settability of v: false

在不可設定的值上呼叫Set方法是錯誤的。但是什麼是可設定性?

可設定性有點像可定址性,但是更嚴格。 它是反射物件可以修改用於建立反射物件的實際儲存的屬性。 可設定性由反射物件是否保留原始專案確定。 當我們說:

可設定性有點像可定址性,但是更嚴格。 它是反射物件可以修改用於建立反射物件的實際儲存的屬性。 可設定性由反射物件是否保留原始專案確定。 當我們說

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

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

v.SetFloat(7.1)

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

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

f(x)

我們不希望f能夠修改x,因為我們傳遞了x值的副本,而不是x本身。如果我們想讓f直接修改x,則必須將x的地址(即指向x的指標)傳遞給函式: f(&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

反射可能很難理解,但它確實在做語言的工作,儘管透過反射TypesValues可以掩蓋正在發生的事情。請記住,反射值需要某些內容的地址才能修改其表示的內容

struct

在我們前面的示例中,v本身並不是指標,它只是從一個指標派生的。 發生這種情況的常見方法是使用反射修改結構的場。 只要有了結構的地址,就可以修改其欄位。

這是一個分析結構值t的簡單示例。我們使用結構的地址建立反射物件,因為稍後將要對其進行修改。然後,將typeOfT設定為其型別,並使用簡單的方法呼叫對欄位進行迭代(有關詳細資訊,請參見包反射)。請注意,我們從結構型別中提取了欄位的名稱,但是欄位本身是常規的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}

如果我們修改程式,以便從t而不是&t建立s,則對SetInt和SetString的呼叫將失敗,因為t的欄位不可設定。

結論

這又是反射定律:

  1. 反射從介面值到反射物件。
  2. 反射從反射物件到介面值。
  3. 要修改反射物件,該值必須可設定。

一旦理解了這些定律,儘管Go中的反射仍然很微妙,但它變得更易於使用。這是一個功能強大的工具,應謹慎使用,除非絕對必要,否則應避免使用。

我們還沒有涉及到很多發射,包括在通道上傳送和接收,分配記憶體,使用切片和對映,呼叫方法和函式,但是這篇文章足夠長。 我們將在以後的文章中介紹其中一些主題。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
微信:okayGoHome

相關文章