内容简介:翻译了一篇 Go 官方博客介绍反射的文章:在计算机科学中,反射是一种在运行时检测自身结构(类型)的能力,反射构成元编程的基础,也是混乱的来源。在这篇文章中我们会尝试澄清 Go 语言中的反射如何运作,每个语言的反射模型都不一样(典型如 Java),很多语言甚至不支持反射,因此在这篇文章中说明的只是 Go 语言反射。
翻译了一篇 Go 官方博客介绍反射的文章:
简介
在计算机科学中,反射是一种在运行时检测自身结构(类型)的能力,反射构成元编程的基础,也是混乱的来源。
在这篇文章中我们会尝试澄清 Go 语言中的反射如何运作,每个语言的反射模型都不一样(典型如 Java),很多语言甚至不支持反射,因此在这篇文章中说明的只是 Go 语言反射。
类型和接口
因为整个反射模型构建在类型系统之上,我们先复习一遍 Go 中的类型。
Go 是静态类型语言,任何变量在编译时都有明确的类型,如 int、float32、*MyType, []byte
等类型...
type MyInt int var i int var j MyInt 复制代码
变量 i
的类型为 int
,变量 j
的类型为 MyInt
。它们两个明显有着不同的静态类型,除此之外又有着相同的基本类型 int
。因为静态类型不同,所以两者必须在转换后才能进行赋值。
接口类型是类型系统中非常重要的一个分类,其代表约定的方法集。接口变量可以存储任意的值,只要该值实现对应的接口方法集。 io
包中的 io.Reader 和 io.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{}
:
interface{} 复制代码
其方法集为空表示任何类型都实现空接口, 任何类型的值都可以对其赋值 。
有些人说接口是动态类型,这种说法是不对的,它们是静态类型。一个接口类型变量总是拥有固定的静态类型,即使在运行时存储在接口中的值有不同的类型(类型实现接口的方法集)。
我们需要理解这些概念是因为反射和接口密切相关。
接口值
Russ Cox 写了一篇文章 Go Data Structures: Interfaces 详细解释了 Go 语言种的接口值。再次不必重复文章中的概念,下面对文章的简单总结:
接口类型变量存储一对值:
- value:赋值给接口类型变量的实际值;
- type:实际值的类型信息。
var r io.Reader tty, 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
方法,接口中的类型值(the value inside)携带所有关于值的类型信息,因此我们可以实现下面的操作(类型信息都有自然可以断言):
var w io.Writer w = r.(io.Writer) 复制代码
上面的赋值表达式称为类型断言,其断言 r
接口变量内部存储的 (value, type)
实现 io.Writer
接口,所以我们可以将其赋值给 w
。在赋值结束后, w
包含 (tty, *os.File)
,与我们之前在 r
中看到的一样。接口的静态类型决定了哪些方法可以通过该接口变量调用,即使内部存储的 (value, type)
拥有更大的方法集。
继续,我们还可以这样做:
var empty interface{} empty = w 复制代码
我们的空接口 empty
依然会在内部存储相同的 (tty, *os.File)
。这意味着空接口可以存储任何值并拥有我们需要的所有信息。
在对空接口赋值时没有使用类型断言,因为任何值都满足空接口, w
显然实现空接口(方法集是空接口的超集)。而上面的 Reader
转换的 Writer
则不一样,我们需要显式使用类型断言是因为 Reader
接口不是 Writer
接口的超集。
一个重要的细节是接口内部总是存储 (value, concrete type),并不能存储 (value, interface type),接口内部并不存储接口值!
现在我们准备好研究反射了。
第一反射定律
反射从接口值中提取反射对象。
在最基本的概念上, 反射只是一种检测存储在接口中的 type 和 value 的机制 。因此我们需要理解reflect 包中的两个类型Type 和Value。这两个类型提供访问接口变量内部存储的能力,并提供两个简单的函数 TypeOf
和 ValueOf
从接口变量中获取 Type
和 Value
(从 Value
得到 Type
也是一件很简单的事情,我们暂时保持两者在概念上的分离)
让我们从 TypeOf
开始:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) } 复制代码
输出:
type: float64 复制代码
你也许会想接口在哪里,看起来只传递 float64
类型的 x
变量作为参数给 TypeOf
函数,而不是接口变量。实际上 TypeOf
函数签名中的参数是空接口, x
会先赋值给空接口,然后作为函数参数传递, TypeOf
函数内部处理空接口恢复类型信息 Type
。
// TypeOf returns the reflection Type of the value in the interface{}. func TypeOf(i interface{}) Type 复制代码
ValueOf
函数也是通过类似的方法得到 Value
类型变量。
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) 复制代码
输出:
value: <float64 Value> 复制代码
直接调用 String
方法是因为在默认情况下 fmt
包直接深入 Value
显示内部真正的值(3.4)。
Type
和 Value
都包含许多检测和操纵它们方法,一个重要的方法是 Value
的 Type
方法返回对应的 Type
类型值。另一个重要的方法是两者都拥有 Kind
方法返回常量基本类型( Uint、Float64、Slice
等)。通常 Value
上的 Int
、 Float
等函数作用是提取内部存储的值。
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
作为参数和返回值。如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. 复制代码
-
Kind
方法返回静态类型对应的基本类型,例如下面x
的静态类型是MyInt
类型,基本类型是reflect.Int
。
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) 复制代码
第二反射定律
反射从反射对象中提取接口值。
就像物理反射定律,与第一条定律相反,从反射对象逆向可以得到接口值。
通过 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) 复制代码
通过反射对象 v
打印 float64
值。
我们可以使用 fmt.Println、fmt.Printf
等函数做得更好,这些函数接收空接口值作为参数,并像上面学到的一样对这些参数进行解包。因此如果要直接打印 reflect.Value
的内容需要使用 Interface
函数获取接口值后传递。
fmt.Println(v.Interface()) 复制代码
为什么不直接使用 fmt.Println(v)
?因为 v
是 reflect.Value
类型的值,我们想要的是实际存储的值。
fmt.Printf("value is %7.1e\n", v.Interface()) 复制代码
输出
3.4e+00 复制代码
再次强调,这里不需要使用类型断言 v.Interface()
到 float64
是因为空接口内部存储的值和类型在 Printf
函数内部会被恢复。
简而言之, Interface
方法是 ValueOf
方法的逆方法,除了返回值总是静态类型 interface{}
。
第三反射定律
要修改反射对象,值必须是可设置的。
第三条定律是非常容易使人迷惑的,如果我们从第一条原则开始理解就简单多了。
下面是一些不能工作但值得学习的代码:
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) // Error: will panic. 复制代码
如果你允许这些代码,会产生 panic
错误。
panic: reflect.Value.SetFloat using unaddressable value 复制代码
这个错误不是说值 7.1 是 not addressable
的,而是说 v
是不可设置的,可设置(settability) 是 Value
的重要属性,并不是所有 Value
都是可设置的。
CanSet
方法检测值是否可设置。
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) 复制代码
输出:
settability of v: false 复制代码
在不可设置的 Value
上调用 Set
方法会出错,那什么是可设置?
可设置(settability)有一点像地址可达(addressability),严格上说:**这是一个反射对象可以修改实际创建该反射对象的值的属性,可设置与否取决于反射对象是否持有原始值(指针)。
var x float64 = 3.4 v := reflect.ValueOf(x) 复制代码
当我们传递一个 x 的拷贝给 reflect.ValueOf
,所以参数的空接口值内部持有 x
的拷贝而不是 x
本身。
v.SetFloat(7.1) 复制代码
因此,如果这个语句执行成功,也不会更新 x
,即使 v
看起来是通过 x
创建的。反而会更新存储在 Value
中的复制值,真正的 x
并不受影响。上述情况容易产生混乱和困扰,因此在语言层面讲这种行为定义为非法的,通过判断可设置属性避免这个问题。
如果上面看起来有些奇怪,实际上并非如此,这只是熟悉情境的奇怪包装罢了(值传递和指针传递,拿到指针才可以修改原始的值)。
思考传递 x
给函数。
f(x) 复制代码
我们不会期望 f
能给修改 x
的值,因为我们传递给 f
的是 x
值的拷贝,而不是 x
本身。如果我们想要直接修改 x
,我们必须传递 x
的地址(指针)。
f(&x) 复制代码
上面的方式非常简单和直接,并且反射的工作原理也是一样的。如果我们想通过反射修改 x
,我们必须传递指针给 Value
。
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
指向的值。我们需要调用 Elem
方法, 其间接通过指针取到原始值,并将结果存储到新的 Value
值中返回 。
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.1 7.1 复制代码
尽管反射有些难以理解,但反射所做的一切都是语言层面支持的,也许 Value
和 Type
会掩饰所发生的事情。只要保持清醒,关注 Value
在被修改时需要指向某个地址。
Structs
在上面的例子中 v
只是指向一个基本类型,而更通用的问题是修改结构体的字段,当我们拥有结构体的指针,我们可以修改它的字段值。
下面是一个简单的例子用于分析一个结构体值。使用 T
类型的指针创建一个 Value
,因此在后续可以修改 t
。
声明并初始化 typeOfT
作为 t
的类型,并通过直接了当的方法 NumField
和 Field
提取出字段的名字、类型和值。
-
Value
的Field
还是Value
,并且是可设置的; -
Type
的Field
则是StructField
。
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 复制代码
还有一个关于可设置的知识点:只有以大写开头的字段才是可设置的(可导出字段)。
因为 s
包含可设置的反射对象(Elem 获得原始对象),我们可以修改结构体的字段。
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) 复制代码
输出:
t is now {77 Sunset Strip} 复制代码
关于
- My Blog
- My Wechat:
以上所述就是小编给大家介绍的《Go 语言:The Laws of Reflection 中文版》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。