Go 语言:The Laws of Reflection 中文版

栏目: Go · 发布时间: 5年前

内容简介:翻译了一篇 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)
}
复制代码

任何类型只要实现 ReadWrite 方法即实现 io.Readerio.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。这两个类型提供访问接口变量内部存储的能力,并提供两个简单的函数 TypeOfValueOf 从接口变量中获取 TypeValue (从 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)。

TypeValue 都包含许多检测和操纵它们方法,一个重要的方法是 ValueType 方法返回对应的 Type 类型值。另一个重要的方法是两者都拥有 Kind 方法返回常量基本类型( Uint、Float64、Slice 等)。通常 Value 上的 IntFloat 等函数作用是提取内部存储的值。

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
复制代码

也有一些 SetIntSetFloat 类方法,使用它们必须理解 可设置的 概念,下面的第三反射定律详细谈到了这些。

反射库中有几个概念值得单独拿出来讲一讲。

  1. 为了保持 API 简单,且 Value 类型的 gettersetter 方法集可以操作比较大的值,所有无符号整数都使用 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.
复制代码
  1. Kind 方法返回静态类型对应的基本类型,例如下面 x 的静态类型是 MyInt 类型,基本类型是 reflect.Int
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
复制代码

第二反射定律

反射从反射对象中提取接口值。

就像物理反射定律,与第一条定律相反,从反射对象逆向可以得到接口值。

通过 ValueInterface 方法可以恢复接口值,实际上这个方法打包 typevalue 信息放到空接口中返回。

// 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) ?因为 vreflect.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 指针的值,实际上我们想修改的是 *pp 指向的值。我们需要调用 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
复制代码

尽管反射有些难以理解,但反射所做的一切都是语言层面支持的,也许 ValueType 会掩饰所发生的事情。只要保持清醒,关注 Value 在被修改时需要指向某个地址。

Structs

在上面的例子中 v 只是指向一个基本类型,而更通用的问题是修改结构体的字段,当我们拥有结构体的指针,我们可以修改它的字段值。

下面是一个简单的例子用于分析一个结构体值。使用 T 类型的指针创建一个 Value ,因此在后续可以修改 t

声明并初始化 typeOfT 作为 t 的类型,并通过直接了当的方法 NumFieldField 提取出字段的名字、类型和值。

  • ValueField 还是 Value ,并且是可设置的;
  • TypeField 则是 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}
复制代码

关于


以上所述就是小编给大家介绍的《Go 语言:The Laws of Reflection 中文版》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

研究之美

研究之美

[美] Donald E. Knuth / 高博 / 电子工业出版社 / 2012-1-1 / 49.00元

《研究之美》是计算机科学大师、“算法分析之父”高德纳(Donald E.Knuth)在20世纪70年代旅居挪威时撰写的适用于计算机科学的一种全新基础数学结构的情景小品。全书以一对追求自由精神生活的青年男女为主人公,展开了一段对于该种全新结构的发现和构造的对白。在此过程中,本书充分展示了计算机科学的从业人员进行全新领域探索时所必备的怀疑、立论、构造、证明、归纳、演绎等逻辑推理和深入反思的能力。《研究......一起来看看 《研究之美》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码