阅读 992

Go 语言:The Laws of Reflection 中文版

翻译了一篇 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 包中的两个类型 TypeValue。这两个类型提供访问接口变量内部存储的能力,并提供两个简单的函数 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}
复制代码

关于