Go的反射法则

本文翻译自 The Laws of Reflection

在计算机科学中,反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力,属于元编程的范畴。在这篇文章中, 我们将尝试解释反射在Go中如何工作。

类型和接口

因为反射构建于类型系统之上,所以我们首先回顾一下Go的类型系统。

Go是一种静态类型语言。每个变量都有一个静态类型,也就是说,在编译时每个变量有且仅有一个已知的类型:int, float32, *MyType, []byte 等等。假设程序中保护如下代码:

type MyInt int

var i int
var j MyInt

那么变量 i 具有类型 int,变量j具有类型MyInt。尽管变量ij具有相同的基础类型,但它们仍然具有不同的静态类型。他们不能在没有类型转换的情况下彼此赋值。

有一类重要的类型就是接口类型,它们包含固定的方法集合。只要该值实现了所有的接口方法,接口变量就可以存储任何具体的(非接口)值。众所周知的一个例子就是io.Readerio.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{}

它表示空白方法集合,任何类型都可以赋予该类型的变量,因为任何类型都包含零个或多个方法。

某些人可能认为Go的接口是动态类型,但这是误导。它们是静态类型的:一个接口类型的变量总是具有相同的静态类型,即使在运行时存储在接口变量中的值可能会改变类型,该类型也将始终满足接口。

我们需要对这些内容进行准确的描述,因为反射和接口密切相关。

接口的实现方式

一个接口类型的变量存储了一对元素:分配给该变量的具体值,以及该值的类型描述符。更确切地说,该值是实现接口的具体数据项,该类型则描述了值的真实类型。比如:

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方法以外的其他方法,即使接口的值(r)只提供了对Read方法的访问,但是值tty包含了类型*os.File的所有信息,这就是为什么我们可以折么做:

var w io.Writer
w = r.(io.Writer)

这个赋值表达式是一个类型断言,它声明了r中保存的类型也实现了io.Writer接口,所以我们可以将它复制给w。在赋值后,w将包含(tty, *os.File)对,这与r中的一样。接口的静态类型决定了接口变量可以调用哪些方法,即使它包含的具体值可能有更多的方法。

接下来,我们还可以这样做:

var empty interface{}
empty = w

这个空接口的值empty也将包含相同的对:(tty, *os.File),这很方便,一个空的接口可以保存任意值,并包含我们可能需要的的关于该值的所有信息。

(这里不需要类型断言,因为w同时也满足空接口。在我们将一个值从Reader移动到Writer的例子中,我们需要明确地使用类型断言,因为Writer的方法并不是Reader的子集)

一个重要的细节是,接口内部的对总是具有(值,具体类型)这样的形式,而不是(值,接口类型)这样的形式,接口不能包含接口值。

现在,我们来讨论一下反射。

1. 从接口值到反射对象
从根本上来说,反射只是一种检查存储在接口变量内的类型和值对的机制。为了开始使用反射,我们需要了解reflect包中的2类反射机制:Type和Value。这两类反射可以访问接口变量中的内容,而对应的两个简单的函数就是reflect.TypeOf和reflect.ValueOf,它们可以从接口变量中检索reflect.Type和reflect.Value。(另外,从reflect.Value中可以很容易得到reflect.Type,但现在让我们将Type和Value这两个概率分开)

让我们从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函数的签名包含一个空接口:

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type

当我们调用reflect.TypeOf(x)函数时,变量x首先保存在一个作为参数传递的空接口中,reflect.TypeOf解压这个空接口并还原类型信息。

同样的,reflect.ValueOf函数将会还原对应的值信息:

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))

程序将打印:

value: 3.4

类型reflect.Typereflect.Value都包含了许多方法,让我们可以检查和操作它们。一个重要的例子是reflect.Value包含一个Type方法可以返回reflect.Value对应的类型。另一个是reflect.Valuereflect.Type都具有的Kind方法,它返回一个枚举,表示存储了什么类型的项:Uint, Float64, Slice 等等。除此以外,reflect.Value中使用类型名字作为函数名的方法(Float, Int)可以让我们获取存储在里面的具体值:

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。也就是说,Value的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方法返回的是基础类型,而不是静态类型。如果一个反射的对象包含用户定义的整数类型值,如:

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

那么v.Kind()仍然是reflect.Int,即使x的静态类型是MyInt。换句话说,即使Type方法可以区分int和MyInt,Kind方法也不可以。

2. 从反射对象到接口值
就像物理反射一样,Go中的反射也可以产生它自己的反转。

给定一个reflect.Value,我们可以使用Interface方法恢复接口值。实际上该方法将类型和值信息从新打包回接口并返回结果:

// Interface returns v's current value as an interface{}.
func (v Value) Interface() (i interface{})

因此我们可以说:

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

将会打印由反射对象v表示的float64值。

不过,我们可以做得更好。fmt.Println, fmt.Printf方法的参数都是通过空接口进行传递,然后在fmt包内部进行解析。因此,正确地打印reflect.Value的内容需要将Interface方法返回的结果传递给格式化的打印例程:

fmt.Println(v.Interface())

(为什么不是fmt.Println(v)?因为v是一个reflect.Value对象,我们希望获取它表示的实际值)。由于v的实际值是float64,我们也可以使用浮点数格式字符串进行打印:

fmt.Printf("value is %7.1e\n", v.Interface())

结果是:

3.4e+00

同样,不需要将v.Interface()的结果断言为float64,空接口参数中包含了具体的类型信息,Printf会恢复它。

简而言之,Interface方法与ValueOf方法互反,只不过它的结果始终是静态类型interface {}

3. 为了修改反射对象,其值必须是可设置的
第三条法则最容易引起混乱,不过假如你是从第一条法则开始学习的,那也很容理解。

以下是一些无法正常工作的代码,但是值得研究:

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是无法寻址的,而是v是不可设置的。可设置性是reflect.Value的一个属性,并不是所有的reflect.Value都是可设置的。

CanSet方法可以返回reflect.Value的可设置性:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

程序打印:

settability of v: false

在不可设置的Value上调用Set方法是错误的,那么什么是可设置性?

可设置性有点像寻址能力,但是更严格。这表示了反射对象是否可以修改用于创建反射对象的实际值的能力,可设置性由反射对象是否保存了原始对象而决定(有点绕),让我们来看一个例子:

var x float64 = 3.4
v := reflect.ValueOf(x)

我们将x的拷贝作为参数传递给reflect.ValueOf,所以作为参数创建的接口值基于的是x的副本,而不是x本身。因此,如果语句:

v.SetFloat(7.1)

运行执行,它将不会更新x,即使v看起来像是通过x创建的。相反,它会更新存储在反射对象中的值而x本身不会受到影响。这会令人困惑,并且也是无意义的。因此这样的操作是非法的,而可设置性属性可用来避免此类问题。

这看起来很奇怪,其实并不是。想象一下把参数x传递给一个函数:

f(x)

我们不希望函数f可以修改参数x,因为我们传递的是x的副本,而不是x本身。如果我们希望函数f可以直接修改x,我们必须将x的地址作为参数传递给f

f(&x)

这是我们熟悉的方式,反射也是一样。如果我们想通过反射来修改x,我们必须给反射库一个指向我们想要修改的值的指针。

让我们来尝试一下。首先我们像往常一样初始化x,然后创建一个指向它的反射值,称为p

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指向的地方,我们使用一个称之为Elem的方法,它通过指针进行间接寻址,并将结果保存在一个名为vreflect.Value中:

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.Typereflect.Value可以掩饰正在发生的事情,但它的确在做语言的工作。 请记住reflect.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}

如果我们修改这个程序,让s基于t进行创建,而不是&t,那么当调用SetIntSetString时将会失败,因为此时t的字段将是不可设置的。

你可能感兴趣的:(Go的反射法则)