網站首頁 編程語言 正文
開始之前
在開始分析原理之前,有必要問一下自己一個問題:
反射是什么?以及其作用是什么?
不論在哪種語言中,我們所提到的反射功能,均指開發者可以在運行時通過調用反射庫來獲取到來獲取到指定對象類型信息,通常類型信息中會包含對象的字段/方法等信息。并且,反射庫通常會提供方法的調用, 以及字段賦值等功能。
使用反射可以幫助我們避免寫大量重復的代碼, 因此反射功能常見用于ORM框架, 以及序列化何反序列化框架,除此之外在Java中反射還被應用到了AOP等功能中。
了解完反射的功能之后,我們再引申一個問題:
假如你開發了一種語言, 該如何為開發者提供反射的功能?
首先,我們知道反射的核心的功能有:
- 類型信息獲取
- 對象字段訪問/賦值
- 方法調用
因此實際作為語言的開發者(假設),我們要解決的問題有:
- 如何存儲并獲取到對象類型信息?
- 如何定位到對象字段的內存地址?
注: 只要知道了對象字段的內存地址配合上類型信息,我們便可以實現賦值與訪問的操作。
- 如何定位到方法的內存地址?
注:代碼在內存中也是數據,因此只需要定位到代碼所在的地址,便可解決方法調用的問題
分析
從何處獲取類型信息
如果你熟悉Go的reflect(反射)庫, 相信你或多或少的聽過反射三原則, 即:
- 從
interface{}
可以反射出反射對象 - 從反射對象中可以獲取到
interface{}
- 要修改反射對象, 其值必須可設置
根據以上三原則不難看出interface{}
是實現反射功能的基石, 那么這是為什么呢?
要回答這個問題,我們了解interface{}
的本質是什么。
interface{}
本質上Go提供的一種數據類型, 與其他數據類型不同的是, interface{}
會為我們提供變量的類型信息以及變量所在的內存地址。
在Runtime
中使用結構體來表示interface{}
, 其結構如下所示:
type emptyInterface struct { typ *rtype word unsafe.Pointer }
該結構體只有兩個字段, 分別是:
-
typ
變量的類型信息, 這一步驟在編譯步驟便可確定下來 -
word
指向變量數據的指針, 這一步驟在運行時進行確定
接下來我們通過反編譯下文的代碼, 來觀察當把一個變量轉換成interface{}
的時候都發生了什么:
package main import "fmt" func main() { s := 1024 var a interface{} = &s fmt.Println(a) }
執行以下命令, 獲取匯編代碼
go tool compile -N -S .\main.go
以下代碼即為將字符串賦值給interface{}
類型的變量a
的對應匯編代碼
0x0057 00087 (.\main.go:7) MOVQ "".&s+104(SP), AX 0x005c 00092 (.\main.go:7) MOVQ AX, ""..autotmp_9+88(SP) 0x0061 00097 (.\main.go:7) LEAQ type.*int(SB), CX 0x0068 00104 (.\main.go:7) MOVQ CX, "".a+144(SP) 0x0070 00112 (.\main.go:7) MOVQ AX, "".a+152(SP)
相信即便你不熟悉匯編,但至少也發現了, 以上代碼做了如下操作:
- 獲取變量
s
的地址, 保存到AX
寄存器, 并往a+144
的地址寫入數據 - 獲取變量
s
的類型信息(type.*int
),保存到CX
寄存器, 并往a+152
的地址寫入數據
注:感興趣的讀者可以把取地址的操作去掉,再看看有什么不同
此外, 我們還可以通過指針數據類型轉換來獲取到interface{}
中的數據來側面驗證一下。
注: unsafe.Pointer 可以轉換成任意類型的指針
type EmptyInterface struct { typ unsafe.Pointer word unsafe.Pointer } func getWordPtr(i interface{}) unsafe.Pointer { eface := *(*EmptyInterface)(unsafe.Pointer(&i)) return eface.word } func Test_GetWordPtr(t *testing.T) { str := "Hello, KeSan" strPtr := &str //此處由編譯器做了類型轉換 *string -> interface{} wordPtr := getWordPtr(strPtr) t.Logf("String Ptr: %p", strPtr) t.Logf("Word Ptr: %p", wordPtr) }
輸入如下所示:
因此,不難推出reflect.TypeOf
的實現實際上就是獲取interface{}
中type
信息,并返回給開發人員。其代碼如下所示:
func TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ) } // 將 *rtype 轉成接口類型的Type func toType(t *rtype) Type { if t == nil { return nil } return t }
再進一步我們可以來看看類型信息中都包含了什么?
結構體rtype
描述了基礎的類型信息,其字段如下所示:
type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero }
rtype
結構體包含了Golang中所有數據類型的基礎類型信息, 對于不同的數據類型其類型信息會有略微的差異。
// 結構體的類型信息 type structType struct { rtype pkgPath name fields []structField // sorted by offset } // channel 的類型信息 type chanType struct { rtype elem *rtype // channel element type dir uintptr // channel direction (ChanDir) }
如何實現賦值操作?
賦值操作的本質上是往對應的內存地址寫入數據, 因此我們有必要簡單了解一下結構體在內存中的布局方式, 以一個最為簡單坐標的結構體為例,其結構體如下所示:
type Coordinate struct { X int64 Y int64 Z int64 }
其在內存中的表現為一段大小為24字節的連續內存,具體如下圖所示
因此,我們實際上要做的就是獲取到結構體的首地址之后,根據各個字段相對首字段的偏移地址計算出其在內存中地址。
實際上在Runtime
提供的類型信息中,已經包含了各個字段的偏移以及類型信息,我們可以具體的來看一下反射功能獲取字段Field
的實現。
func (v Value) Field(i int) Value { if v.kind() != Struct { panic(&ValueError{"reflect.Value.Field", v.kind()}) } // 獲取類型信息 tt := (*structType)(unsafe.Pointer(v.typ)) if uint(i) >= uint(len(tt.fields)) { panic("reflect: Field index out of range") } // 獲取字段信息 field := &tt.fields[i] typ := field.typ // 繼承結構體的部分flag信息 fl := v.flag&(flagStickyRO|flagIndir|flagAddr) | flag(typ.Kind()) if !field.name.isExported() { if field.embedded() { fl |= flagEmbedRO } else { fl |= flagStickyRO } } // 根據偏移地址計 + 結構體的首地址 計算出 字段在內存中的地址, 并返回Value對象 ptr := add(v.ptr, field.offset(), "same as non-reflect &v.field") return Value{typ, ptr, fl} }
了解到如何獲取字段在內存中的地址之后,我們再來看看賦值操作是如何實現。
如以下代碼SetInt
所示, 本質上還是一些指針的轉換以及解引用。
func (v Value) SetInt(x int64) { v.mustBeAssignable() switch k := v.kind(); k { default: panic(&ValueError{"reflect.Value.SetInt", v.kind()}) case Int: *(*int)(v.ptr) = int(x) case Int8: *(*int8)(v.ptr) = int8(x) case Int16: *(*int16)(v.ptr) = int16(x) case Int32: *(*int32)(v.ptr) = int32(x) case Int64: *(*int64)(v.ptr) = x } }
那么,肯定有同學會問,為啥你一直都在講結構體啊,那字符串(string
), 切片(slice
), map
呢?
實際上這些Go
的內建的數據類型,在Runtime
中的表現形式也是結構體, 我們可以在reflect
包中找到如下定義:
// 切片頭 type SliceHeader struct { Data uintptr // 數組的指針地址 Len int // 數組長度 Cap int // 數組容量 } // 字符串頭 type StringHeader struct { Data uintptr // 字節數組的指針地址 Len int // 字節數組的長度 }
因此,通過反射來操作切片和字符串本質上還是操作結構體。
總結
-
interface{}
是一種數據類型, 其存儲了變量的類型信息與數據指針,其中類型信息是在編譯期間確定下來的 -
Golang
反射的原理就是從interface{}
中獲取到類型信息以及變量的指針,從而實現類型獲取以及賦值的功能
原文鏈接:https://juejin.cn/post/7159797055060672543
相關推薦
- 2022-04-19 css隱藏??元素的幾種方式
- 2022-04-20 在Python反編譯中批量pyc轉?py的實現代碼_python
- 2022-11-19 python中celery的基本使用詳情_python
- 2023-07-02 oracle數據庫排序后如何獲取第一條數據_oracle
- 2022-06-23 Python實現希爾排序,歸并排序和桶排序的示例代碼_python
- 2022-07-08 C語言完整實現12種排序算法(小結)_C 語言
- 2022-07-25 python如何將自己的包上傳到PyPi并可通過pip安裝的方法步驟_python
- 2022-08-02 C#如何自定義multipart/form-data的解析器_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支