介紹
計算反射是程式檢查其自身結構的能力,尤其是透過型別。這是超程式設計的一種形式。這也是造成混亂的重要原因。
在本文中,我們嘗試透過解釋反射在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的子集。)
一個重要的細節是,介面內的對始終具有形式(值,具體型別),而不能具有形式(值,介面型別)。介面不儲存介面值。 現在我們準備好反射了。
反射的第一條定律
反射從介面值到反射物件。
在基本級別上,反射只是一種檢查儲存在介面變數中的型別和值對的機制。首先,我們需要在反射包中瞭解兩種型別:Type和Value。這兩種型別允許訪問介面變數的內容,還有兩個簡單的函式,稱為reflect.TypeOf
和reflect.ValueOf
,從介面值中檢索reflect.Type
和reflect.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.Type
和reflect.Value
都有很多方法可以讓我們檢查和操作它們。一個重要的示例是Value
具有Type
方法,該方法返回reflect.Value
的Type
。另一個是Type
和Value
都有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
還有諸如SetInt
和SetFloat
之類的方法,但是要使用它們,我們需要了解可沉降性,這是第三反射定律的主題,下面將進行討論。
反射庫具有幾個值得一提的屬性。 首先,為使API保持簡單,Value
的“getter”和“setter”方法在可以容納該值的最大型別上執行:例如,所有有符號整數的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 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
反射可能很難理解,但它確實在做語言的工作,儘管透過反射Types
和Values
可以掩蓋正在發生的事情。請記住,反射值需要某些內容的地址才能修改其表示的內容。
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的欄位不可設定。
結論
這又是反射定律:
- 反射從介面值到反射物件。
- 反射從反射物件到介面值。
- 要修改反射物件,該值必須可設定。
一旦理解了這些定律,儘管Go中的反射仍然很微妙,但它變得更易於使用。這是一個功能強大的工具,應謹慎使用,除非絕對必要,否則應避免使用。
我們還沒有涉及到很多發射,包括在通道上傳送和接收,分配記憶體,使用切片和對映,呼叫方法和函式,但是這篇文章足夠長。 我們將在以後的文章中介紹其中一些主題。
本作品採用《CC 協議》,轉載必須註明作者和本文連結