go語言中反射三定律是什麼

2023-01-18 18:01:05

反射三定律:1、反射可以將「介面型別變數」轉換為「反射型別物件」,這裡反射型別指「reflect.Type」和 「reflect.Value」;2、反射可以將「反射型別物件」轉換為「介面型別變數」;3、如果要修改「反射型別物件」其值必須是「可寫的」。

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

在反射的世界裡,我們擁有了獲取一個物件的型別,屬性及方法的能力。

1.png

兩種型別:Type 和 Value

在 Go 反射的世界裡,有兩種型別非常重要,是整個反射的核心,在學習 reflect 包的使用時,先得學習下這兩種型別:

  • reflect.Type

  • reflect.Value

它們分別對應著真實世界裡的 type 和 value,只不過在反射物件裡,它們擁有更多的內容。

從原始碼上來看,reflect.Type 是以一個介面的形式存在的

type Type interface {
    Align() int
    FieldAlign() int
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int
    Name() string
    PkgPath() string
    Size() uintptr
    String() string
    Kind() Kind
    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    Bits() int
    ChanDir() ChanDir
    IsVariadic() bool
    Elem() Type
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    In(i int) Type
    Key() Type
    Len() int
    NumField() int
    NumIn() int
    NumOut() int
    Out(i int) Type
    common() *rtype
    uncommon() *uncommonType
}
登入後複製

而 reflect.Value 是以一個結構體的形式存在,

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}
登入後複製

同時它接收了很多的方法(見下表),這裡出於篇幅的限制這裡也沒辦法一一介紹。

Addr
Bool
Bytes
runes
CanAddr
CanSet
Call
CallSlice
call
Cap
Close
Complex
Elem
Field
FieldByIndex
FieldByName
FieldByNameFunc
Float
Index
Int
CanInterface
Interface
InterfaceData
IsNil
IsValid
IsZero
Kind
Len
MapIndex
MapKeys
MapRange
Method
NumMethod
MethodByName
NumField
OverflowComplex
OverflowFloat
OverflowInt
OverflowUint
Pointer
Recv
recv
Send
send
Set
SetBool
SetBytes
setRunes
SetComplex
SetFloat
SetInt
SetLen
SetCap
SetMapIndex
SetUint
SetPointer
SetString
Slice
Slice3
String
TryRecv
TrySend
Type
Uint
UnsafeAddr
assignTo
Convert
登入後複製

通過上一節的內容(),我們知道了一個介面變數,實際上都是由一 pair 對(type 和 data)組合而成,pair 對中記錄著實際變數的值和型別。也就是說在真實世界裡,type 和 value 是合併在一起組成 介面變數的。

而在反射的世界裡,type 和 data 卻是分開的,他們分別由 reflect.Type 和 reflect.Value 來表現。

解讀反射的三大定律

Go 語言裡有個反射三定律,是你在學習反射時,很重要的參考:

  • Reflection goes from interface value to reflection object.

  • Reflection goes from reflection object to interface value.

  • To modify a reflection object, the value must be settable.

翻譯一下,就是:

  • 反射可以將「介面型別變數」 轉換為「反射型別物件」;

  • 反射可以將 「反射型別物件」轉換為 「介面型別變數」;

  • 如果要修改 「反射型別物件」 其型別必須是 「可寫的」;

第一定律

Reflection goes from interface value to reflection object.

為了實現從介面變數到反射物件的轉換,需要提到 reflect 包裡很重要的兩個方法:

  • reflect.TypeOf(i) :獲得介面值的型別

  • reflect.ValueOf(i):獲得介面值的值

這兩個方法返回的物件,我們稱之為反射物件:Type object 和 Value object。

2.png

舉個例子,看下這兩個方法是如何使用的?

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始介面變數的型別為 %T,值為 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 從介面變數到反射物件
    fmt.Printf("從介面變數到反射物件:Type物件的型別為 %T \n", t)
    fmt.Printf("從介面變數到反射物件:Value物件的型別為 %T \n", v)

}
登入後複製

輸出如下

原始介面變數的型別為 int,值為 25 從介面變數到反射物件:Type物件的型別為 *reflect.rtype 
從介面變數到反射物件:Value物件的型別為 reflect.Value 複製程式碼
登入後複製

如此我們完成了從介面型別變數到反射物件的轉換。

等等,上面我們定義的 age 不是 int 型別的嗎?第一法則裡怎麼會說是介面型別的呢?

關於這點,其實在上一節()已經提到過了,由於 TypeOf 和 ValueOf 兩個函數接收的是 interface{} 空介面型別,而 Go 語言函數都是值傳遞,因此Go語言會將我們的型別隱式地轉換成介面型別。

原始介面變數的型別為 int,值為 25 
從介面變數到反射物件:Type物件的型別為 *reflect.rtype 
從介面變數到反射物件:Value物件的型別為 reflect.Value
登入後複製

第二定律

Reflection goes from reflection object to interface value.

和第一定律剛好相反,第二定律描述的是,從反射物件到介面變數的轉換。

3.png

通過原始碼可知, reflect.Value 的結構體會接收 Interface 方法,返回了一個 interface{} 型別的變數(注意:只有 Value 才能逆向轉換,而 Type 則不行,這也很容易理解,如果 Type 能逆向,那麼逆向成什麼呢?

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//    var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}
登入後複製

這個函數就是我們用來實現將反射物件轉換成介面變數的一個橋樑。

例子如下

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始介面變數的型別為 %T,值為 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 從介面變數到反射物件
    fmt.Printf("從介面變數到反射物件:Type物件的型別為 %T \n", t)
    fmt.Printf("從介面變數到反射物件:Value物件的型別為 %T \n", v)

    // 從反射物件到介面變數
    i := v.Interface()
    fmt.Printf("從反射物件到介面變數:新物件的型別為 %T 值為 %v \n", i, i)

}
登入後複製

輸出如下

原始介面變數的型別為 int,值為 25 
從介面變數到反射物件:Type物件的型別為 *reflect.rtype 
從介面變數到反射物件:Value物件的型別為 reflect.Value 
從反射物件到介面變數:新物件的型別為 int 值為 25
登入後複製

當然了,最後轉換後的物件,靜態型別為 interface{} ,如果要轉成最初的原始型別,需要再型別斷言轉換一下,關於這點,我已經在上一節裡講解過了,你可以點此前往復習:()。

i := v.Interface().(int)
登入後複製

至此,我們已經學習了反射的兩大定律,對這兩個定律的理解,我畫了一張圖,你可以用下面這張圖來加強理解,方便記憶。

4.png

第三定律

To modify a reflection object, the value must be settable.

反射世界是真實世界的一個『對映』,是我的一個描述,但這並不嚴格,因為並不是你在反射世界裡所做的事情都會還原到真實世界裡。

第三定律引出了一個 settable (可設定性,或可寫性)的概念。

其實早在以前的文章中,我們就一直在說,Go 語言裡的函數都是值傳遞,只要你傳遞的不是變數的指標,你在函數內部對變數的修改是不會影響到原始的變數的。

回到反射上來,當你使用 reflect.Typeof 和 reflect.Valueof 的時候,如果傳遞的不是介面變數的指標,反射世界裡的變數值始終將只是真實世界裡的一個拷貝,你對該反射物件進行修改,並不能反映到真實世界裡。

因此在反射的規則裡

  • 不是接收變數指標建立的反射物件,是不具備『可寫性』的
  • 是否具備『可寫性』,可使用 CanSet() 來獲取得知
  • 對不具備『可寫性』的物件進行修改,是沒有意義的,也認為是不合法的,因此會報錯。
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go程式設計時光"

    v := reflect.ValueOf(name)
    fmt.Println("可寫性為:", v.CanSet())
}
登入後複製

輸出如下

可寫性為: false
登入後複製

要讓反射物件具備可寫性,需要注意兩點

  • 建立反射物件時傳入變數的指標

  • 使用 Elem()函數返回指標指向的資料

完整程式碼如下

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go程式設計時光"
    v1 := reflect.ValueOf(&name)
    fmt.Println("v1 可寫性為:", v1.CanSet())

    v2 := v1.Elem()
    fmt.Println("v2 可寫性為:", v2.CanSet())
}
登入後複製

輸出如下

v1 可寫性為: false
v2 可寫性為: true
登入後複製

知道了如何使反射的世界裡的物件具有可寫性後,接下來是時候瞭解一下如何對修改更新它。

反射物件,都會有如下幾個以 Set 單詞開頭的方法

5.png

這些方法就是我們修改值的入口。

來舉個例子

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go程式設計時光"
    fmt.Println("真實世界裡 name 的原始值為:", name)

    v1 := reflect.ValueOf(&name)
    v2 := v1.Elem()

    v2.SetString("Python程式設計時光")
    fmt.Println("通過反射物件進行更新後,真實世界裡 name 變為:", name)
}
登入後複製

輸出如下

真實世界裡 name 的原始值為: Go程式設計時光
通過反射物件進行更新後,真實世界裡 name 變為: Python程式設計時光
登入後複製

【相關推薦:Go視訊教學、】

以上就是go語言中反射三定律是什麼的詳細內容,更多請關注TW511.COM其它相關文章!