Go语言反射定律

介绍

计算中的反射是指一段程序有能力检查它自己的结构,特别是数据类型。这是一个元编程的形式。这也是很多困惑的来源。

在这篇文章我们尝试通过解释反射在Go语言中的运作方式来阐明各个知识点。每个语言中的反射模型是有差异的(有许多语言甚至不支持反射),但是这篇文章是关于Go,本文剩余部分的反射是指Go语言中的反射。

类型和接口

因为反射建立在数据类型系统上,让我们从复习Go语言中的数据类型开始吧。

Go是静态类型语言。每个变量都有静态类型,换句话说就是在编译期间变量的类型就已知并固定下来了,例如int、float32、*MyType、[]byte等等。如果我们声明

type MyInt int

var i int

var j MyInt

这样i就有类型int,j就有类型MyInt。变量i和j有不同的静态类型。尽管他们有相同的底层类型,它们在不进行类型转化的情况下无法互相赋值。

一类重要的类型是接口(interface)类型。接口表示一组固定的方法。一个接口变量能够存储任何实体(非接口)值,只要此值实现了接口的方法。一对著名的例子是io.Reader和io.Writer。它们来自io代码包。

// 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存储了什么值,它的类型总是io.Reader:Go是静态类型语言,r的静态类型是io.Reader。

一个极其重要的接口类型是空接口

interface{}

它代表方法的空集合。由于任何值都有0个或更多的方法,所有空接口可以满足任何值。

一些人说Go接口是动态类型,但这是一种误解。它们是静态类型:接口类型的一个变量总是有相同的静态类型。就算运行时接口变量中的值发生了类型变化,这些值总会满足这个接口的要求。

我们需要准确理解这一点,因为反射和接口是紧密相关的。

一个接口的表示

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也会包含一对(tty,*os.File)。这非常便利:一个空接口可以包含任何值和与之对应的类型信息。

(这里不需要类型判断,因为w总是满足空接口。在把值从Reader移动到Writer的例子里,我们需要显式的类型判断,因为Writer的方法不是Reader的子集。)

一个重要的细节是接口内部必须保持(值,实体类型)这样的形式,而非(值,接口类型)这样的形式。接口不能包含接口的值。

现在我们准备好了解反射了。

反射第一定律

反射从接口值映射到反射对象

就基本功能而言,反射就是检查存储在接口变量内部(类型,值)对的一种机制。为了能够深入理解,我们需要知道代码包reflect里面的两种类型Type和Value以及两个函数reflect.TypeOf和reflect.ValueOf。这两种类型帮助我们了解接口变量的内容。这两个函数分别从接口值种取出reflect.Type和reflect.Value。(从Value类型也可以很容易地得到reflect.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.TyoeOf,而非接口值。但是它就在那里;如同godoc所示,reflect.TypeOf的声明包括了一个空接口

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用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展示内部实际数值。String则不如此。)

reflect.Type和reflect.Value都有很多方法来让我们检查和操作它们。一个重要的例子是Value有一个Type方法来返回reflect.Type。另外一个重要方法是Type和Value都有Kind方法来返回一个类型常量:Uint、Float64、Slice等等。像Int和Float这样Value的方法让我们提取内部数值:

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,而不是int。换句话说,Kind不能够区分MyInt和int,尽管Type能。

 反射第二定律

反射从反射对象映射到接口值

类似于物理里的反射,Go里面的反射也有反向操作。

给定reflect.Value,我们可以用Interface方法来获取其蕴含的值;实际上,这方法把类型和值信息打包回接口的表示,返回结果:

// 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等方法的参数,首先是作为空接口值传入内部,然后在fmt内部被打开,如我们之前的例子所示。因此打印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()的返回值转化为float64;空接口值包含实际值类型信息,Printf会恢复它。

简而言之,Interface方法是ValueOf函数的逆操作,除了它的结果总是静态类型interface{}。

复习:反射从interface值进行到反射对象,然后再次返回。

反射第三定律

为了修改反射对象,值必须具有可设置性(Settability)

第三定律是最微妙和令人困惑的,但是如果从第一定律出发是可以理解的。

下面是一些错误的代码,但是值得研究。

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你跑这段代码,它会报令人疑惑的错误

panic: reflect.Value.SetFloat using unaddressable value

这里的问题在于值7.1是没有地址;v是不具有可设置性。可设置性是反射Value的一项性质,不是所有的反射Value都具有这个性质。

Value的CanSet方法可以报告一个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,所以传递给reflect.ValueOf返回值内部包含的是x的副本。因此,如果

v.SetFloat(7.1)

是合法的,它不会更新x,尽管v看起来由x创建而来。实际上它会更新存储在reflect Value对象内部的x副本,原始的x不会受到影响。这既令人困惑,又无用,所以这是不合法的。可设置性就是用来避免这个问题的性质。

这看起来奇怪,实际上并非如此。这本质上和之前的情形类似。想想看把x传给函数:

f(x)

我们不会期望函数f能够修改x,因为我们传递了x的副本,而非x自己。如果我们想要函数f直接修改x,我们必须把x的地址传递给函数(x的指针):

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不是我们的目标,*p才是。为了得到p指向的对象,我们调用Elem方法,通过利用指针p,我们能够修改反射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

反射可能令人难以理解,但是它就如我们所说的运作,尽管反射的Type和Value的使用掩盖了它的本质。我们只需要记住,反射的Value需要变量的地址来修改它们的真实值。

结构体

我们以前的例子里,v本身不是指针,v是从指针而得到的反射Value。当我们需要反射来修改结构体的字段时,我们就需要这种使用反射的方法。只要我们有结构体的地址,我们就能修改它的字段。

下面是个分析结构体值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,那么对SetInt和SetString的调用就会失败,因为t的字段不可设置。

结论

这里重复一遍反射定律:

  1. 反射从接口值映射到反射对象;
  2. 反射从反射对象映射到接口值;
  3. 为了修改反射对象,其包含的值必须可设置。

一旦你理解了这些定律,Go语言中的反射会变得容易使用很多,尽管它仍然比较微妙。它是一个强大的工具,应该谨慎使用,如无必要就不要使用。

还有很多关于反射的知识我们还没有覆盖——频道(channel)的发送和接收、分配内存、使用切片(slice)和映射(map)、调用方法和函数——但是这篇博客已经足够长。我们会在后面的博客中讨论它们。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值