主頁(yè) > 知識(shí)庫(kù) > 談?wù)凣o語(yǔ)言的反射三定律

談?wù)凣o語(yǔ)言的反射三定律

熱門標(biāo)簽:百度地圖標(biāo)注搜索關(guān)鍵詞 騰訊外呼系統(tǒng)價(jià)格 浙江人工智能外呼管理系統(tǒng) 谷歌便利店地圖標(biāo)注 成都呼叫中心外呼系統(tǒng)平臺(tái) 電梯外呼訪客系統(tǒng) 最短的地圖標(biāo)注 電銷機(jī)器人可以補(bǔ)救房產(chǎn)中介嗎 ?兓?

簡(jiǎn)介

Reflection(反射)在計(jì)算機(jī)中表示 程序能夠檢查自身結(jié)構(gòu)的能力,尤其是類型。它是元編程的一種形式,也是最容易讓人迷惑的一部分。

雖然Go語(yǔ)言沒(méi)有繼承的概念,但為了便于理解,如果一個(gè)struct A 實(shí)現(xiàn)了 interface B的所有方法時(shí),我們稱之為“繼承”。

類型和接口

反射建立在類型系統(tǒng)之上,因此我們從類型基礎(chǔ)知識(shí)說(shuō)起。

Go是靜態(tài)類型語(yǔ)言。每個(gè)變量都有且只有一個(gè)靜態(tài)類型,在編譯時(shí)就已經(jīng)確定。比如 int、float32、*MyType、[]byte。 如果我們做出如下聲明:

type MyInt int

var i int
var j MyInt

上面的代碼中,變量 i 的類型是 int,j 的類型是 MyInt。 所以,盡管變量 i 和 j 具有共同的底層類型 int,但它們的靜態(tài)類型并不一樣。不經(jīng)過(guò)類型轉(zhuǎn)換直接相互賦值時(shí),編譯器會(huì)報(bào)錯(cuò)。

關(guān)于類型,一個(gè)重要的分類是 接口類型(interface),每個(gè)接口類型都代表固定的方法集合。一個(gè)接口變量就可以存儲(chǔ)(或“指向”,接口變量類似于指針)任何類型的具體值,只要這個(gè)值實(shí)現(xiàn)了該接口類型的所有方法。一組廣為人知的例子是 io.Reader io.Writer, Reader 和 Writer 類型來(lái)源于 io包,聲明如下:

// 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)
}

任何實(shí)現(xiàn)了 Read(Write)方法的類型,我們都稱之為繼承了 io.Reader(io.Writer)接口。換句話說(shuō), 一個(gè)類型為 io.Reader 的變量 可以指向(接口變量類似于指針)任何類型的變量,只要這個(gè)類型實(shí)現(xiàn)了Read 方法:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

要時(shí)刻牢記:不管變量 r 指向的具體值是什么,它的類型永遠(yuǎn)是 io.Reader。再重復(fù)一次:Go語(yǔ)言是靜態(tài)類型語(yǔ)言,變量 r 的靜態(tài)類型是 io.Reader。

一個(gè)非常非常重要的接口類型是空接口,即:

interface{}

它代表一個(gè)空集,沒(méi)有任何方法。由于任何具體的值都有 零或更多個(gè)方法,因此類型為interface{} 的變量能夠存儲(chǔ)任何值。

有人說(shuō),Go的接口是動(dòng)態(tài)類型的。這個(gè)說(shuō)法是錯(cuò)的!接口變量也是靜態(tài)類型的,它永遠(yuǎn)只有一個(gè)相同的靜態(tài)類型。如果在運(yùn)行時(shí)它存儲(chǔ)的值發(fā)生了變化,這個(gè)值也必須滿足接口類型的方法集合。

由于反射和接口兩者的關(guān)系很密切,我們必須澄清這一點(diǎn)。

接口變量的表示

Russ Cox 在2009年寫了一篇文章介紹 Go中接口變量的表示形式,這里我們不需要重復(fù)所有的細(xì)節(jié),只做一個(gè)簡(jiǎn)單的總結(jié)。

Interface變量存儲(chǔ)一對(duì)值:賦給該變量的具體的值、值類型的描述符。更準(zhǔn)確一點(diǎn)來(lái)說(shuō),值就是實(shí)現(xiàn)該接口的底層數(shù)據(jù),類型是底層數(shù)據(jù)類型的描述。舉個(gè)例子:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
 return nil, err
}
r = tty

在這個(gè)例子中,變量 r 在結(jié)構(gòu)上包含一個(gè) (value, type) 對(duì):(tty, os.File)。注意:類型 os.File 不僅僅實(shí)現(xiàn)了 Read 方法。雖然接口變量只提供 Read 函數(shù)的調(diào)用權(quán),但是底層的值包含了關(guān)于這個(gè)值的所有類型信息。所以我們能夠做這樣的類型轉(zhuǎn)換:

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

上面代碼的第二行是一個(gè)類型斷言,它斷定變量 r 內(nèi)部的實(shí)際值也繼承了 io.Writer接口,所以才能被賦值給 w。賦值之后,w 就指向了 (tty, *os.File) 對(duì),和變量 r 指向的是同一個(gè) (value, type) 對(duì)。不管底層具體值的方法集有多大,由于接口的靜態(tài)類型限制,接口變量只能調(diào)用特定的一些方法。

我們繼續(xù)往下看:

var empty interface{}
empty = w

這里的空接口變量 empty 也包含 (tty, *os.File) 對(duì)。這一點(diǎn)很容易理解:空接口變量可以存儲(chǔ)任何具體值以及該值的所有描述信息。

細(xì)心的朋友可能會(huì)發(fā)現(xiàn),這里沒(méi)有使用類型斷言,因?yàn)樽兞?w 滿足 空接口的所有方法(傳說(shuō)中的“無(wú)招勝有招”)。在前一個(gè)例子中,我們把一個(gè)具體值 從 io.Reader 轉(zhuǎn)換為 io.Writer 時(shí),需要顯式的類型斷言,是因?yàn)?code> io.Writer 的方法集合 不是 io.Reader 的子集。

另外需要注意的一點(diǎn)是,(value, type) 對(duì)中的 type 必須是 具體的類型(struct或基本類型),不能是 接口類型。 接口類型不能存儲(chǔ)接口變量。

關(guān)于接口,我們就介紹到這里,下面我們看看Go語(yǔ)言的反射三定律。

反射第一定律:反射可以將“接口類型變量”轉(zhuǎn)換為“反射類型對(duì)象”。

注:這里反射類型指 reflect.Type reflect.Value。

從用法上來(lái)講,反射提供了一種機(jī)制,允許程序在運(yùn)行時(shí)檢查接口變量?jī)?nèi)部存儲(chǔ)的 (value, type) 對(duì)。在最開始,我們先了解下 reflect 包的兩種類型:Type 和 Value。這兩種類型使訪問(wèn)接口內(nèi)的數(shù)據(jù)成為可能。它們對(duì)應(yīng)兩個(gè)簡(jiǎn)單的方法,分別是 reflect.TypeOf reflect.ValueOf,分別用來(lái)讀取接口變量的 reflect.Type reflect.Value 部分。當(dāng)然,從 reflect.Value 也很容易獲取到 reflect.Type。目前我們先將它們分開。

首先,我們下看 reflect.TypeOf:

package main

import (
 "fmt"
 "reflect"
)

func main() {
 var x float64 = 3.4
 fmt.Println("type:", reflect.TypeOf(x))
}

這段代碼會(huì)打印出:

type: float64

你可能會(huì)疑惑:為什么沒(méi)看到接口?這段代碼看起來(lái)只是把一個(gè) float64類型的變量 x 傳遞給 reflect.TypeOf,并沒(méi)有傳遞接口。事實(shí)上,接口就在那里。查閱一下TypeOf 的文檔,你會(huì)發(fā)現(xiàn) reflect.TypeOf 的函數(shù)簽名里包含一個(gè)空接口:

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

我們調(diào)用 reflect.TypeOf(x) 時(shí),x 被存儲(chǔ)在一個(gè)空接口變量中被傳遞過(guò)去; 然后reflect.TypeOf 對(duì)空接口變量進(jìn)行拆解,恢復(fù)其類型信息。

函數(shù) reflect.ValueOf 也會(huì)對(duì)底層的值進(jìn)行恢復(fù)(這里我們忽略細(xì)節(jié),只關(guān)注可執(zhí)行的代碼):

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

上面這段代碼打印出:

value: float64 Value>

類型 reflect.Typereflect.Value 都有很多方法,我們可以檢查和使用它們。這里我們舉幾個(gè)例子。類型 reflect.Value 有一個(gè)方法 Type(),它會(huì)返回一個(gè) reflect.Type 類型的對(duì)象。Type和 Value都有一個(gè)名為 Kind 的方法,它會(huì)返回一個(gè)常量,表示底層數(shù)據(jù)的類型,常見值有:Uint、Float64、Slice等。Value類型也有一些類似于Int、Float的方法,用來(lái)提取底層的數(shù)據(jù)。Int方法用來(lái)提取 int64, Float方法用來(lái)提取 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())

上面這段代碼會(huì)打印出:

type: float64
kind is float64: true
value: 3.4

還有一些用來(lái)修改數(shù)據(jù)的方法,比如SetInt、SetFloat,在討論它們之前,我們要先理解“可修改性”(settability),這一特性會(huì)在“反射第三定律”中進(jìn)行詳細(xì)說(shuō)明。

反射庫(kù)提供了很多值得列出來(lái)單獨(dú)討論的屬性。首先是介紹下Value 的 getter 和 setter 方法。為了保證API 的精簡(jiǎn),這兩個(gè)方法操作的是某一組類型范圍最大的那個(gè)。比如,處理任何含符號(hào)整型數(shù),都使用 int64。也就是說(shuō) Value 類型的Int 方法返回值為 int64類型,SetInt 方法接收的參數(shù)類型也是 int64 類型。實(shí)際使用時(shí),可能需要轉(zhuǎn)化為實(shí)際的類型:

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.

第二個(gè)屬性是反射類型變量(reflection object)的 Kind 方法 會(huì)返回底層數(shù)據(jù)的類型,而不是靜態(tài)類型。如果一個(gè)反射類型對(duì)象包含一個(gè)用戶定義的整型數(shù),看代碼:

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

上面的代碼中,雖然變量 v 的靜態(tài)類型是MyInt,不是 int,Kind 方法仍然返回 reflect.Int。換句話說(shuō), Kind 方法不會(huì)像 Type 方法一樣區(qū)分 MyInt 和 int。

反射第二定律:反射可以將“反射類型對(duì)象”轉(zhuǎn)換為“接口類型變量”。

和物理學(xué)中的反射類似,Go語(yǔ)言中的反射也能創(chuàng)造自己反面類型的對(duì)象。

根據(jù)一個(gè) reflect.Value 類型的變量,我們可以使用 Interface 方法恢復(fù)其接口類型的值。事實(shí)上,這個(gè)方法會(huì)把 type 和 value 信息打包并填充到一個(gè)接口變量中,然后返回。其函數(shù)聲明如下:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

然后,我們可以通過(guò)斷言,恢復(fù)底層的具體值:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

上面這段代碼會(huì)打印出一個(gè) float64 類型的值,也就是 反射類型變量 v 所代表的值。

事實(shí)上,我們可以更好地利用這一特性。標(biāo)準(zhǔn)庫(kù)中的 fmt.Printlnfmt.Printf 等函數(shù)都接收空接口變量作為參數(shù),fmt 包內(nèi)部會(huì)對(duì)接口變量進(jìn)行拆包(前面的例子中,我們也做過(guò)類似的操作)。因此,fmt 包的打印函數(shù)在打印 reflect.Value 類型變量的數(shù)據(jù)時(shí),只需要把 Interface 方法的結(jié)果傳給 格式化打印程序:

fmt.Println(v.Interface())

你可能會(huì)問(wèn):?jiǎn)柺裁床恢苯哟蛴?v ,比如 fmt.Println(v)? 答案是 v 的類型是 reflect.Value,我們需要的是它存儲(chǔ)的具體值。由于底層的值是一個(gè) float64,我們可以格式化打?。?/p>

fmt.Printf("value is %7.1e\n", v.Interface())

上面代碼的打印結(jié)果是:

3.4e+00

同樣,這次也不需要對(duì) v.Interface() 的結(jié)果進(jìn)行類型斷言??战涌谥祪?nèi)部包含了具體值的類型信息,Printf 函數(shù)會(huì)恢復(fù)類型信息。

簡(jiǎn)單來(lái)說(shuō),Interface 方法和 ValueOf 函數(shù)作用恰好相反,唯一一點(diǎn)是,返回值的靜態(tài)類型是 interface{}。

我們重新表述一下:Go的反射機(jī)制可以將“接口類型的變量”轉(zhuǎn)換為“反射類型的對(duì)象”,然后再將“反射類型對(duì)象”轉(zhuǎn)換過(guò)去。

反射第三定律:如果要修改“反射類型對(duì)象”,其值必須是“可寫的”(settable)。

這條定律很微妙,也很容易讓人迷惑。但是如果你從第一條定律開始看,應(yīng)該比較容易理解。

下面這段代碼不能正常工作,但是非常值得研究:

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

如果你運(yùn)行這段代碼,它會(huì)拋出拋出一個(gè)奇怪的異常:

panic: reflect.Value.SetFloat using unaddressable value

這里問(wèn)題不在于值 7.1 不能被尋址,而是因?yàn)樽兞?v 是“不可寫的”?!翱蓪懶浴笔欠瓷漕愋妥兞康囊粋€(gè)屬性,但不是所有的反射類型變量都擁有這個(gè)屬性。

我們可以通過(guò) CanSet 方法檢查一個(gè) reflect.Value 類型變量的“可寫性”。對(duì)于上面的例子,可以這樣寫:

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

上面這段代碼打印結(jié)果是:

settability of v: false

對(duì)于一個(gè)不具有“可寫性”的 Value類型變量,調(diào)用 Set 方法會(huì)報(bào)出錯(cuò)誤。首先,我們要弄清楚什么“可寫性”。

“可寫性”有些類似于尋址能力,但是更嚴(yán)格。它是反射類型變量的一種屬性,賦予該變量修改底層存儲(chǔ)數(shù)據(jù)的能力?!翱蓪懶浴弊罱K是由一個(gè)事實(shí)決定的:反射對(duì)象是否存儲(chǔ)了原始值。舉個(gè)代碼例子:

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

這里我們傳遞給 reflect.ValueOf 函數(shù)的是變量 x 的一個(gè)拷貝,而非 x 本身。想象一下,如果下面這行代碼能夠成功執(zhí)行:

v.SetFloat(7.1)

答案是:如果這行代碼能夠成功執(zhí)行,它不會(huì)更新 x ,雖然看起來(lái)變量 v 是根據(jù) x 創(chuàng)建的。相反,它會(huì)更新 x 存在于 反射對(duì)象 v 內(nèi)部的一個(gè)拷貝,而變量 x 本身完全不受影響。這會(huì)造成迷惑,并且沒(méi)有任何意義,所以是不合法的?!翱蓪懶浴本褪菫榱吮苊膺@個(gè)問(wèn)題而設(shè)計(jì)的。

這看起來(lái)很詭異,事實(shí)上并非如此,而且類似的情況很常見??紤]下面這行代碼:

f(x)

上面的代碼中,我們把變量 x 的一個(gè)拷貝傳遞給函數(shù),因此不期望它會(huì)改變 x 的值。如果期望函數(shù) f 能夠修改變量 x,我們必須傳遞 x 的地址(即指向 x 的指針)給函數(shù) f,如下:

f(x)

你應(yīng)該很熟悉這行代碼,反射的工作機(jī)制是一樣的。如果你想通過(guò)反射修改變量 x,就咬吧想要修改的變量的指針傳遞給 反射庫(kù)。

首先,像通常一樣初始化變量 x,然后創(chuàng)建一個(gè)指向它的 反射對(duì)象,名字為 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

反射對(duì)象 p 是不可寫的,但是我們也不像修改 p,事實(shí)上我們要修改的是 *p。為了得到 p 指向的數(shù)據(jù),可以調(diào)用 Value 類型的 Elem 方法。Elem 方法能夠?qū)χ羔樳M(jìn)行“解引用”,然后將結(jié)果存儲(chǔ)到反射 Value類型對(duì)象 v中:

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

在上面這段代碼中,變量 v 是一個(gè)可寫的反射對(duì)象,代碼輸出也驗(yàn)證了這一點(diǎn):

settability of v: true

由于變量 v 代表 x, 因此我們可以使用 v.SetFloat 修改 x 的值:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

上面代碼的輸出如下:

7.1
7.1

反射不太容易理解,reflect.Typereflect.Value 會(huì)混淆正在執(zhí)行的程序,但是它做的事情正是編程語(yǔ)言做的事情。你只需要記住:只要反射對(duì)象要修改它們表示的對(duì)象,就必須獲取它們表示的對(duì)象的地址。

結(jié)構(gòu)體(struct)

在前面的例子中,變量 v 本身并不是指針,它只是從指針衍生而來(lái)。把反射應(yīng)用到結(jié)構(gòu)體時(shí),常用的方式是 使用反射修改一個(gè)結(jié)構(gòu)體的某些字段。只要擁有結(jié)構(gòu)體的地址,我們就可以修改它的字段。

下面通過(guò)一個(gè)簡(jiǎn)單的例子對(duì)結(jié)構(gòu)體類型變量 t 進(jìn)行分析。

首先,我們創(chuàng)建了反射類型對(duì)象,它包含一個(gè)結(jié)構(gòu)體的指針,因?yàn)楹罄m(xù)會(huì)修改。

然后,我們?cè)O(shè)置 typeOfT 為它的類型,并遍歷所有的字段。

注意:我們從 struct 類型提取出每個(gè)字段的名字,但是每個(gè)字段本身也是常規(guī)的 reflect.Value 對(duì)象。

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

這里還有一點(diǎn)需要指出:變量 T 的字段都是首字母大寫的(暴露到外部),因?yàn)閟truct中只有暴露到外部的字段才是“可寫的”。

由于變量 s 包含一個(gè)“可寫的”反射對(duì)象,我們可以修改結(jié)構(gòu)體的字段:

f.Interface())s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

上面代碼的輸出如下:

t is now {77 Sunset Strip}

如果變量 s 是通過(guò) t ,而不是 t 創(chuàng)建的,調(diào)用 SetInt 和 SetString 將會(huì)失敗,因?yàn)?t 的字段不是“可寫的”。

結(jié)論

最后再次重復(fù)一遍反射三定律:

    1.反射可以將“接口類型變量”轉(zhuǎn)換為“反射類型對(duì)象”。

    2.反射可以將“反射類型對(duì)象”轉(zhuǎn)換為“接口類型變量”。

    3.如果要修改“反射類型對(duì)象”,其值必須是“可寫的”(settable)。

一旦你理解了這些定律,使用反射將會(huì)是一件非常簡(jiǎn)單的事情。它是一件強(qiáng)大的工具,使用時(shí)務(wù)必謹(jǐn)慎使用,更不要濫用。

關(guān)于反射,我們還有很多內(nèi)容沒(méi)有討論,包括基于管道的發(fā)送和接收、內(nèi)存分配、使用slice和map、調(diào)用方法和函數(shù),這些話題我們會(huì)在后續(xù)的文章中介紹。請(qǐng)大家繼續(xù)關(guān)注腳本之家。

原作者 Rob Pike,翻譯Oscar

您可能感興趣的文章:
  • golang之反射和斷言的具體使用
  • 詳解Golang利用反射reflect動(dòng)態(tài)調(diào)用方法
  • 淺談Go語(yǔ)言中的結(jié)構(gòu)體struct & 接口Interface & 反射
  • Go語(yǔ)言學(xué)習(xí)筆記之反射用法詳解
  • Go語(yǔ)言中反射的正確使用
  • go語(yǔ)言通過(guò)反射獲取和設(shè)置結(jié)構(gòu)體字段值的方法
  • Go語(yǔ)言中使用反射的方法
  • 圖文詳解go語(yǔ)言反射實(shí)現(xiàn)原理

標(biāo)簽:邢臺(tái) 七臺(tái)河 雅安 紹興 眉山 盤錦 上海 宜昌

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《談?wù)凣o語(yǔ)言的反射三定律》,本文關(guān)鍵詞  談?wù)?語(yǔ)言,的,反射,三,定律,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《談?wù)凣o語(yǔ)言的反射三定律》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于談?wù)凣o語(yǔ)言的反射三定律的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章