哈Ha! 我向您介绍语言创建者的文章
“反射定律”的译文。
反思是程序探索自身结构的能力,尤其是通过类型。 这是元编程的一种形式,也是造成混乱的主要原因。
在Go中,反射被广泛使用,例如在test和fmt软件包中。 在本文中,我们将通过解释反射在Go中的工作原理来摆脱“魔力”。
类型和接口
由于反射基于类型系统,因此让我们重新了解Go中的类型知识。
Go是静态类型的。 每个变量在编译时只有一个固定的静态类型:
int, float32, *MyType, []byte
...如果声明:
type MyInt int var i int var j MyInt
那么
i
的类型是
int
而
j
的类型是
MyInt
。 变量
i
和
j
具有不同的静态类型,尽管它们具有相同的基本类型,但是如果不进行转换就无法将它们彼此分配。
接口是重要的类型类别之一,它们是固定的方法集。 接口可以存储任何特定(非接口)值,只要该值实现接口的方法即可。 一对著名的示例是
io.Reader和io.Writer ,它们是
io包中的Reader和Writer类型:
据说,任何使用此签名实现
Read()
或
Write()
方法的
io.Writer
分别实现
io.Reader
或
io.Writer
。 这意味着
io.Reader
类型的变量可以包含Read()类型的任何值:
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
重要的是要了解可以为
r
分配实现
io.Reader
任何值。 Go是静态类型的,而静态类型
r
是
io.Reader
。
接口类型的一个非常重要的例子是空接口:
interface{}
它是∅方法的空集,并且可以通过任何值实现。
有人说Go接口是动态类型化的变量,但这是一个谬论。 它们是静态类型的:具有接口类型的变量始终具有相同的静态类型,并且尽管在运行时存储在接口变量中的值可以更改类型,但此值将始终满足接口。 (没有
undefined
,
NaN
或其他破坏程序逻辑的东西。)
必须理解这一点-反射和接口密切相关。
接口的内部表示
Russ Cox写了一篇详细的
博客文章,内容涉及在Go中设置界面。
关于哈布雷的文章
也不少 。 无需重复整个故事,主要要点已提及。
接口类型变量具有一对:分配给该变量的特定值,以及该值的类型描述符。 更确切地说,值是实现接口的基本数据元素,类型描述了此元素的完整类型。 例如,之后
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
示意地包含一对
(, ) --> (tty, *os.File)
。 注意
*os.File
类型实现了
Read()
之外的方法; 即使接口值仅提供对Read()方法的访问,该值也包含有关该值类型的所有信息。 这就是为什么我们可以做这样的事情:
var w io.Writer w = r.(io.Writer)
此分配中的表达式是一个类型语句; 它声称
r
内的元素也实现了
io.Writer
,因此我们可以将其分配给
w
。 分配后,
w
将包含一对
(tty, *os.File)
。 这与
r
对相同。 接口的静态类型确定可以在接口变量上调用哪些方法,尽管更广泛的方法集可以在内部具有特定值。
继续,我们可以执行以下操作:
var empty interface{} empty = w
并且empty字段的empty值将再次包含同一对
(tty, *os.File)
。 这很方便:空接口可以包含任何值以及我们从中需要的所有信息。
这里我们不需要类型断言,因为已知
w
满足空接口。 在将值从
Reader
传输到
Writer
的示例中,我们需要显式使用类型断言,因为
Writer
方法不是
Reader
的子集。 尝试转换与接口不匹配的值会引起恐慌。
一个重要的细节是,接口内的一对始终具有形式(值,特定类型),而不能具有形式(值,接口)。 接口不支持将接口作为值。
现在我们准备研究反思。
反射的第一定律反映
从根本上讲,反射只是一种检查存储在接口变量内的类型和值对的机制。 首先,我们需要了解两种类型:
reflect.Type
和
reflect.Value
。 这两种类型提供对接口变量内容的访问,并分别由简单函数reflect.TypeOf()和reflect.ValueOf()返回。 它们从接口的含义中提取部分。 (此外,
reflect.Value
容易获得
reflect.Type
,但是现在我们不要混合使用
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
传递给
reflect.TypeOf()
。 您看到界面了吗? 它是-根据函数声明,
reflect.TypeOf()
接受一个空接口:
当我们调用
reflect.TypeOf(x)
,
x
首先存储在一个空接口中,然后将其作为参数传递;
reflect.TypeOf()
解压缩此空接口以恢复类型信息。
当然,
reflect.ValueOf()
函数会还原值(在下文中,我们将忽略模板,并专注于代码):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String())
将打印
value: <float64 Value>
(我们明确地调用
String()
方法,因为默认情况下,fmt包会解压缩以
reflect.Value
值并输出特定值。)
reflect.Type
和
reflect.Value
都有很多方法,使您可以探索和修改它们。 一个重要的示例是
reflect.Value
具有一个
Type()
方法,该方法返回值的类型。
reflect.Type
和
reflect.Value
有一个
Kind()
方法,该方法返回一个常量,该常量指示要存储的原始元素:
Uint, Float64, Slice
...这些常量在反射包中的枚举中声明。 名称为
Int()
和
Float()
Value
方法使我们能够提取包含在其中的值(例如int64和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())
将打印
type: float64 kind is float64: true value: 3.4
还有一些方法,例如
SetInt()
和
SetFloat()
,但是要使用它们,我们需要了解可沉降性,这是反射第三定律的主题。
反射库具有几个需要突出显示的属性。 首先,为了使API保持简单,“ getter”和“ setter”
Value
方法作用于可以包含值的最大类型:所有有
int64
整数的
int64
。 也就是说,
Value
值的
Int()
方法返回
int64
,而
SetInt()
值采用
int64
; 可能需要转换为实际类型:
var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) x = uint8(v.Uint())
将是
type: uint8 kind is uint8: true
这里
v.Uint()
将返回
uint64
,需要一个显式的类型声明。
第二个属性是对象的
Kind()
反映的是基本类型,而不是静态类型。 如果反射对象包含用户定义的整数类型的值,例如
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
v.Kind() == reflect.Int
,尽管
x
的静态类型是
MyInt
,而不是
int
。 换句话说,
MyInt
Type()
MyInt
,
Kind()
无法将
int
与
MyInt
区分开。
Kind
只能接受内置类型的值。
反射第二定律反映
就像物理反射一样,Go中的反射也会产生相反的效果。
有了
reflect.Value
,我们可以使用
Interface()
方法恢复接口的值; 该方法将类型和值信息打包回接口,并返回结果:
bvt
例如:
y := v.Interface().(float64)
打印由反射对象
v
表示的
float64
的值。
但是,我们可以做得更好。 如前面的示例一样,
fmt.Println()
和
fmt.Printf()
中的参数作为空接口传递,然后由fmt包在内部对其进行解压缩。 因此,正确打印
reflect.Value
内容所需
reflect.Value
就是将
Interface()
方法的结果传递给格式化的输出函数:
fmt.Println(v.Interface())
(为什么不使用
fmt.Println(v)
?因为
v
的类型为
reflect.Value
;我们希望获取包含在其中的值。)由于我们的值是
float64
,因此,如果需要,我们甚至可以使用浮点格式:
fmt.Printf("value is %7.1e\n", v.Interface())
将在特定情况下输出
3.4e+00
同样,无需
v.Interface()
结果类型
v.Interface()
为
float64
; 空接口值包含有关特定值的信息,而
fmt.Printf()
还原该值。
简而言之,
Interface()
方法与
ValueOf()
函数相反,但其结果始终是静态类型
interface{}
。
重复:反射从接口值扩展到反射对象,反之亦然。
反射第三定律反射
第三定律是最微妙和令人困惑的。 我们从第一个原则开始。
此代码无效,但值得关注。
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
如果运行此代码,它将因紧急消息而崩溃,并显示一条关键消息:
panic: reflect.Value.SetFloat
问题不在于字面意义
7.1
没有得到解决。 这是
v
无法安装的内容。 可设置性是
reflect.Value
的属性,并非每个
reflect.Value
都有它。
reflect.Value.CanSet()
方法
reflect.Value.CanSet()
正在设置
reflect.Value.CanSet()
; 在我们的情况下:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
将打印:
settability of v: false
在非托管值上调用
Set()
方法时发生错误。 但是什么是可安装性?
可持续性有点像寻址能力,但是更严格。 这是反射对象可以更改用于创建反射对象的存储值的属性。 可持续性由反射对象包含源元素还是仅包含源元素来确定。 当我们写:
var x float64 = 3.4 v := reflect.ValueOf(x)
我们将
x
的副本传递给
reflect.ValueOf()
,因此将接口创建为
reflect.ValueOf()
的参数-这是
x
的副本,而不是
x
本身。 因此,如果声明:
v.SetFloat(7.1)
如果执行,它将不会更新
x
,尽管
v
看起来好像是从
x
创建的。 相反,他将更新存储在
v
值内的
x
的副本,并且
x
本身不会受到影响。 禁止这样做以免引起问题,可安装性是用于防止问题的属性。
这似乎并不奇怪。 这是穿不寻常衣服的普遍情况。 考虑将
x
传递给函数:
f(x)
我们不希望
f()
能够更改
x
,因为我们传递了
x
值的副本,而不是
x
本身。 如果我们希望
f()
直接更改
x
,则必须将指向
x
的指针传递给我们的函数:
f(&x)
这是直接且熟悉的,并且反射的工作方式与此类似。 如果要使用反射来更改
x
,则必须为反射库提供指向要更改的值的指针。
来吧 首先,我们照常初始化
x
,然后创建一个指向它的
reflect.Value p
。
var x float64 = 3.4 p := reflect.ValueOf(&x)
将输出
type of p: *float64
settability of p: false
不能设置反射对象
p
,但是它不是我们要设置的
p
,而是指针
*p
。 为了获得
p
指向的内容,我们调用
Value.Elem()
方法,该方法通过指针间接获取值,并将结果存储在
reflect.Value v
:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
现在
v
是一个可安装的对象;
settability of v: true
并且由于它表示
x
,我们最终可以使用
v.SetFloat()
来更改
x
的值:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
结论如预期
7.1
7.1
Reflect可能很难理解,但是它确实可以完成语言的工作,尽管可以借助
reflect.Type
和
reflect.Type
来隐藏正在发生的事情。 请记住,
reflection.Value
需要变量的地址才能对其进行更改。
结构体
在我们之前的示例中,
v
不是指针,而是从其派生的。 创建这种情况的一种常见方法是使用反射来更改结构字段。 只要有了结构的地址,就可以更改其字段。
这是一个简单的示例,它分析结构
t
的值。 我们使用结构的地址创建一个反射对象,以便以后对其进行修改。 然后将typeOfT设置为其类型,并使用简单的方法调用遍历字段(
有关详细说明,请参见
软件包 )。 请注意,我们正在从结构类型中提取字段名称,但是字段本身是常规的
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 = 23
1: B string = skidoo
这里显示了有关可安装性的另一点:大写字母
T
字段的名称(已导出),因为只能设置导出的字段。
由于
s
包含可安装的反射对象,因此我们可以更改结构字段。
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
结果:
t is now {77 Sunset Strip}
如果更改程序以便从
t
而不是
&t
创建
&t
,则对
SetInt()
和
SetString()
的调用
SetInt()
紧急方式结束,因为无法设置字段
t
。
结论
回顾反射定律:
- 反射从界面延伸到对象的反射。
- 反射从对象的反射延伸到界面。
- 要更改反射对象,必须设置该值。
罗伯·派克 (
Rob Pike)发表。