Go 語言反射三定律

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

簡介

Reflection(反射)在電腦中表示 程式能夠檢查自身結構的能力,尤其是類型。它是元編程的一種形式,也是最容易讓人迷惑的一部分。

本文中,我們將解釋Go語言中反射的運作機制。每個程式設計語言的反射模型不大相同,很多語言索性就不支援反射(C、C++)。由於本文是介紹Go語言的,所以當我們談到“反射”時,預設為是Go語言中的反射。

閱讀建議

本文中,我們將解釋Go語言中反射的運作機制。每個程式設計語言的反射模型不大相同,很多語言索性就不支援反射(C、C++)。

由於本文是介紹Go語言的,所以當我們談到“反射”時,預設為是Go語言中的反射。

雖然Go語言沒有繼承的概念,但為了便於理解,如果一個struct A 實現了 interface B的所有方法時,我們稱之為“繼承”。

類型和介面

反射建立在類型系統之上,因此我們從類型基礎知識說起。

Go是靜態類型語言。每個變數都有且只有一個靜態類型,在編譯時間就已經確定。比如 int、float32、*MyType、[]byte。 如果我們做出如下聲明:

type MyInt intvar i intvar j MyInt

上面的代碼中,變數 i 的類型是 int,j 的類型是 MyInt。 所以,儘管變數 i 和 j 具有共同的底層類型 int,但它們的靜態類型並不一樣。不經過類型轉換直接相互賦值時,編譯器會報錯。

關於類型,一個重要的分類是 介面類型(interface),每個介面類型都代表固定的方法集合。一個介面變數就可以儲存(或“指向”,介面變數類似於指標)任何類型的具體值,只要這個值實現了該介面類型的所有方法。一組廣為人知的例子是 io.Reader 和 io.Writer, Reader 和 Writer 類型來源於 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)}

任何實現了 Read(Write)方法的類型,我們都稱之為繼承了 io.Reader(io.Writer)介面。換句話說, 一個類型為 io.Reader 的變數 可以指向(介面變數類似於指標)任何類型的變數,只要這個類型實現了Read 方法:

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

要時刻牢記:不管變數 r 指向的具體值是什麼,它的類型永遠是 io.Reader。再重複一次:Go語言是靜態類型語言,變數 r 的靜態類型是 io.Reader。

一個非常非常重要的介面類型是空介面,即:

interface{}

它代表一個空集,沒有任何方法。由於任何具體的值都有 零或更多個方法,因此類型為interface{} 的變數能夠儲存任何值。

有人說,Go的介面是動態類型的。這個說法是錯的!介面變數也是靜態類型的,它永遠只有一個相同的靜態類型。如果在運行時它儲存的值發生了變化,這個值也必須滿足介面類型的方法集合。

由於反射和介面兩者的關係很密切,我們必須澄清這一點。

介面變數的表示

Russ Cox 在2009年寫了一篇文章介紹 Go中介面變數的表示形式,具體參考文章末尾的連結“Go語言介面的表示”。這裡我們不需要重複所有的細節,只做一個簡單的總結。

Interface變數儲存一對值:賦給該變數的具體的值、實值型別的描述符。更準確一點來說,值就是實現該介面的底層資料,類型是底層資料類型的描述。舉個例子:

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 指向的是同一個 (value, type) 對。不管底層具體值的方法集有多大,由於介面的靜態類型限制,介面變數只能調用特定的一些方法。

我們繼續往下看:

var empty interface{}empty = w

這裡的空介面變數 empty 也包含 (tty, *os.File) 對。這一點很容易理解:空介面變數可以儲存任何具體值以及該值的所有描述資訊。

細心的朋友可能會發現,這裡沒有使用類型斷言,因為變數 w 滿足 空介面的所有方法(傳說中的“無招勝有招”)。在前一個例子中,我們把一個具體值 從 io.Reader 轉換為 io.Writer 時,需要顯式的類型斷言,是因為 io.Writer 的方法集合 不是 io.Reader 的子集。

另外需要注意的一點是,(value, type) 對中的 type 必須是 具體的類型(struct或基本類型),不能是 介面類型。 介面類型不能儲存介面變數。

關於介面,我們就介紹到這裡,下面我們看看Go語言的反射三定律。

反射第一定律:反射可以將“介面類型變數”轉換為“反射類型對象”。

註:這裡反射類型指 reflect.Typereflect.Value

從用法上來講,反射提供了一種機制,允許程式在運行時檢查介面變數內部儲存的 (value, type) 對。在最開始,我們先瞭解下 reflect 包的兩種類型:Type 和 Value。這兩種類型使提供者內的資料成為可能。它們對應兩個簡單的方法,分別是 reflect.TypeOf 和 reflect.ValueOf,分別用來讀取介面變數的 reflect.Type 和 reflect.Value 部分。當然,從 reflect.Value 也很容易擷取到 reflect.Type。目前我們先將它們分開。

首先,我們下看 reflect.TypeOf:

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

這段代碼會列印出:

type: float64

你可能會疑惑:為什麼沒看到介面?這段代碼看起來只是把一個 float64類型的變數 x 傳遞給 reflect.TypeOf,並沒有傳遞介面。事實上,介面就在那裡。查閱一下TypeOf 的文檔,你會發現 reflect.TypeOf 的函數簽名裡包含一個空介面:

// TypeOf returns the reflection Type of the value in the 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 都有很多方法,我們可以檢查和使用它們。這裡我們舉幾個例子。類型 reflect.Value 有一個方法 Type(),它會返回一個 reflect.Type 類型的對象。Type和 Value都有一個名為 Kind 的方法,它會返回一個常量,表示底層資料的類型,常見值有:Uint、Float64、Slice等。Value類型也有一些類似於Int、Float的方法,用來提取底層的資料。Int方法用來提取 int64, Float方法用來提取 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,在討論它們之前,我們要先理解“可修改性”(settability),這一特性會在“反射第三定律”中進行詳細說明。

反射庫提供了很多值得列出來單獨討論的屬性。首先是介紹下Value 的 getter 和 setter 方法。為了保證API 的精簡,這兩個方法操作的是某一群組類型範圍最大的那個。比如,處理任何含符號整型數,都使用 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.

第二個屬性是反射類型變數(reflection object)的 Kind 方法 會返回底層資料的類型,而不是靜態類型。如果一個反射類型對象包含一個使用者定義的整型數,看代碼:

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

上面的代碼中,雖然變數 v 的靜態類型是MyInt,不是 int,Kind 方法仍然返回 reflect.Int。換句話說, Kind 方法不會像 Type 方法一樣區分 MyInt 和 int。

反射第二定律:反射可以將“反射類型對象”轉換為“介面類型變數”。

和物理學中的反射類似,Go語言中的反射也能創造自己反面類型的對象。

根據一個 reflect.Value 類型的變數,我們可以使用 Interface 方法恢複其介面類型的值。事實上,這個方法會把 type 和 value 資訊打包並填充到一個介面變數中,然後返回。其函式宣告如下:

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

上面這段代碼會列印出一個 float64 類型的值,也就是 反射類型變數 v 所代表的值。

事實上,我們可以更好地利用這一特性。標準庫中的 fmt.Println 和 fmt.Printf 等函數都接收空介面變數作為參數,fmt 包內部會對介面變數進行拆包(前面的例子中,我們也做過類似的操作)。因此,fmt 包的列印函數在列印 reflect.Value 類型變數的資料時,只需要把 Interface 方法的結果傳給 格式化列印程式:

fmt.Println(v.Interface())

你可能會問:問什麼不直接列印 v ,比如 fmt.Println(v)? 答案是 v 的類型是 reflect.Value,我們需要的是它儲存的具體值。由於底層的值是一個 float64,我們可以格式化列印:

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

上面代碼的列印結果是:

3.4e+00

同樣,這次也不需要對 v.Interface() 的結果進行類型斷言。空介面值內部包含了具體值的類型資訊,Printf 函數會恢複類型資訊。

簡單來說,Interface 方法和 ValueOf 函數作用恰好相反,唯一一點是,傳回值的靜態類型是 interface{}。

我們重新表述一下:Go的反射機制可以將“介面類型的變數”轉換為“反射類型的對象”,然後再將“反射類型對象”轉換過去。

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

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

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

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

如果你運行這段代碼,它會拋出拋出一個奇怪的異常:

panic: reflect.Value.SetFloat using unaddressable value

這裡問題不在於值 7.1 不能被定址,而是因為變數 v 是“不可寫的”。“可寫性”是反射類型變數的一個屬性,但不是所有的反射類型變數都擁有這個屬性。

我們可以通過 CanSet 方法檢查一個 reflect.Value 類型變數的“可寫性”。對於上面的例子,可以這樣寫:

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

上面這段代碼列印結果是:

settability of v: false

對於一個不具有“可寫性”的 Value類型變數,調用 Set 方法會報出錯誤。首先,我們要弄清楚什麼“可寫性”。

“可寫性”有些類似於定址能力,但是更嚴格。它是反射類型變數的一種屬性,賦予該變數修改底層儲存資料的能力。“可寫性”最終是由一個事實決定的:反射對象是否儲存了原始值。舉個代碼例子:

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

這裡我們傳遞給 reflect.ValueOf 函數的是變數 x 的一個拷貝,而非 x 本身。想象一下,如果下面這行代碼能夠成功執行:

v.SetFloat(7.1)

答案是:如果這行代碼能夠成功執行,它不會更新 x ,雖然看起來變數 v 是根據 x 建立的。相反,它會更新 x 存在於 反射對象 v 內部的一個拷貝,而變數 x 本身完全不受影響。這會造成迷惑,並且沒有任何意義,所以是不合法的。“可寫性”就是為了避免這個問題而設計的。

這看起來很詭異,事實上並非如此,而且類似的情況很常見。考慮下面這行代碼:

f(x)

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

f(&x)

你應該很熟悉這行代碼,反射的工作機制是一樣的。如果你想通過反射修改變數 x,就咬吧想要修改的變數的指標傳遞給 反射庫。

首先,像通常一樣初始設定變數 x,然後建立一個指向它的 反射對象,名字為 p:

var x float64 = 3.4p := 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: *float64settability of p: false

反射對象 p 是不可寫的,但是我們也不像修改 p,事實上我們要修改的是 *p。為了得到 p 指向的資料,可以調用 Value 類型的 Elem 方法。Elem 方法能夠對指標進行“解引用”,然後將結果儲存到反射 Value類型對象 v中:

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

在上面這段代碼中,變數 v 是一個可寫的反射對象,代碼輸出也驗證了這一點:

settability of v: true

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

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

上面代碼的輸出如下:

7.17.1

反射不太容易理解,reflect.Type 和 reflect.Value 會混淆正在執行的程式,但是它做的事情正是程式設計語言做的事情。你只需要記住:只要反射對象要修改它們表示的對象,就必須擷取它們表示的對象的地址。

結構體(struct)

在前面的例子中,變數 v 本身並不是指標,它只是從指標衍生而來。把反射應用到結構體時,常用的方式是 使用反射修改一個結構體的某些欄位。只要擁有結構體的地址,我們就可以修改它的欄位。

下面通過一個簡單的例子對結構體類型變數 t 進行分析。

首先,我們建立了反射類型對象,它包含一個結構體的指標,因為後續會修改。

然後,我們設定 typeOfT 為它的類型,並遍曆所有的欄位。

注意:我們從 struct 類型提取出每個欄位的名字,但是每個欄位本身也是常規的 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 的欄位都是首字母大寫的(暴露到外部),因為struct中只有暴露到外部的欄位才是“可寫的”。

由於變數 s 包含一個“可寫的”反射對象,我們可以修改結構體的欄位:

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 是通過 t ,而不是 &t 建立的,調用 SetInt 和 SetString 將會失敗,因為 t 的欄位不是“可寫的”。

結論

最後再次重複一遍反射三定律:

  1. 反射可以將“介面類型變數”轉換為“反射類型對象”。

  2. 反射可以將“反射類型對象”轉換為“介面類型變數”。

  3. 如果要修改“反射類型對象”,其值必須是“可寫的”(settable)。
    一旦你理解了這些定律,使用反射將會是一件非常簡單的事情。它是一件強大的工具,使用時務必謹慎使用,更不要濫用。

關於反射,我們還有很多內容沒有討論,包括基於管道的發送和接收、記憶體配置、使用slice和map、調用方法和函數,由於本文已經非常長了,這些話題在後續的文章中介紹。

原作者 Rob Pike,翻譯Oscar

相關連結:

  • 原文連結:https://blog.golang.org/laws-...

  • reflect 包:https://golang.org/pkg/reflect/

掃碼關注公眾號“深入Go語言”

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.