Golang 的反射法则

原文链接:https://blog.golang.org/laws-of-reflection

Introduction

反射是计算机学中程序检查自身结构的能力,特别是通过类型;它是一种元编程的形式。也是一个巨大的混淆源。

在这篇文章中,我们试着通过解释Go中的反射如何运作来澄清一些事情。每个语言的反射模型都不同(许多语言不支持),这篇文章仅仅介绍Go的反射,因此以后文章中提到的反射,都是指Go中的反射。

    Types and interfaces

因为反射是建立在类型系统上的,我们从复习Go的类型开始。

Go是静态类型的语言。每个变量有一个静态类型,意味着,在编译时只有一个固定的类型:int,float32,*MyType,[]byte等。如果我们声明:

type MyInt int

var i int
var j MyInt

i 是int类型,j是MyInt类型。变量 i 和 j 是不同的静态类型,即使他们底层类型相同,他们在没有转换的前提下不能赋值为另外一个类型。

一个重要的类型是interface类型,代表一组固定的方法。一个接口变量可以存储任何实体类型(非interface)值,只要那个变量实现了interface的所有方法。一个众所周知的例子是io.Read和io.Writer,Reader和Wirter类型来自io package:

// 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类型的变量可以hold任何 有Read方法的类型值:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

这点非常重要,不管 r hold什么实体类型的值,r 的类型永远是 io.Reader:Go是静态类型的 并且 r的静态类型是io.Reader。

一个及其重要的interface的例子是空interface:

interface {}

它表示一个空的方法集,并且匹配任何值,因为任何值都有0个或多个方法。

有些人表示,Go的interface是动态类型的,但那是误导。他们是静态类型:一个interface类型的变量总是相同的静态类型,并且即使在run time,存储在interface变量中的值可能会改变类型,但是那个值总是满足interface。

我们需要很清晰的了解这些,因为反射和interface极其相关。

    The representation of an interface

Russ Cox 写了一篇关于interface的文章。这里没必要重复一遍,按此简化。( 参见上篇翻译文章《golang interface的使用和实现》

一个interface类型的变量存储一对word值:一个指向实体类型的值,和值的类型描述符。更精确点,该值是实现接口的基础数据项,类型描述该数据项的完整类型。例如,

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类型也可以实现除Rad以外的其他方法;即使interface值仅仅提供对Rad方法的访问,这个值包含有关该值的所有类型信息。所以我们可以这么做:

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

上面的赋值语句是类型断言;断言的内容为,r内部也实现了io.Writer,所以我们可以将它赋值给w。赋值后,w将会包含(tty, *os.File)。与 r 中存储的数据一样。interface的静态类型决定了interface变量可以调用什么方法,即使实体类型可能有一组很大的方法表。

继续,我们可以这样做:

var empty interface{}
empty = w

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

(我们不需要在此做类型断言,因为w满足空interface。在我们从Reader移动值到Writer的例子中,我们需要类型断言,因为Writer的方法不是Reader方法的子集。)

一个重要的细节是,一个interface总是(value,concrete type)形式,不能是(value,interface type)形式。interface不能hold interface值。

准备学习反射。

    The first law of reflection

1. 反射从interface值到反射对象。(Reflection goes from interface value to reflection object)
在基本层面上,反射只是一种 检查 存储在interface值中的类型和值对 的 机制。在开始前,我们需要了解reflect package中的Type和Value。这两个类型可以访问interface变量的内容,另外两个简单的函数, reflect.TypeOf和reflect.ValueOf,从interface值中检索reflect.Type和reflect.Value。(同样,从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

你可能好奇,interface在哪,由于程序看起来像传递了一个类型为float64的变量x,而不是一个interface值,到reflect.TypeOf。 但是,reflect.TypeOf的函数签名,参数是为空interface:

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

当我们调用reflect.TypeOf(x)时,x 首先保存到一个空interface中;reflect.TypeOf 从空interface中取出类型信息。

reflect.ValueOf函数,当然,取出值(从这里开始,我们只关注可执行代码):

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

输出:
value: <float64 Value>

(我们明确调用String方法,因为默认fmt package会深入reflect.Value中,显示内部的实体值。而String方法不会。)

reflect.Type 和 reflect.Value 都有许多可以检测和操作的方法。一个重要的例子是,Value有一个Type方法,返回reflect.Value的类型。另一个是,Type和Value都有一个Kind方法,会返回存储的项目的种类的常量:Uint,Float64,Slice等等。同样,Value上的方法,如Int和Float可以让我们获取存储在内部的值(像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简单,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可以。


    The second law of reflection

2. 反射从反向对象到interface值(Reflection goes from reflection object to interface value)

像物理反射一样,Go中的反射生成其自身的反向。

给一个reflect.Value,我们可以使用Interface方法恢复一个interface的值;方法打包类型和值信息到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等等的参数,都是作为空interface值传入的,在fmt package内部被unpack(之前提到这个例子)。因此,正确输出reflect.Value 所要做的就是传递Interface方法的结果到 格式化输出例程:

fmt.Println(v.Interface())

(为什么不是fmt.Println(v)?因为v是一个reflect.Value;我们需要其hold的实体类型的值)因为我们的值是float64类型,我们甚至可以使用一个 floating-point 格式,如果我们想的话:

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

输出:
3.4e+00

这里再次无需做v.Interface()的类型断言float64;空interface的值内部有实体类型的值信息。Printf将会恢复它。

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

重申:反射是从interface值到反射对象,然后再反过来。

    The third law of reflection

3. 要修改一个反射对象,值必须是可以设置的(To modify a reflection object, the value must be settable)

第三个法则是最微妙和令人困惑的,但是我们从第一个法则学过来的话理解起来会比较简单。

下面的代码不能工作,但是值得学习下:

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 不能被设置。可设置性是反射值的一个属性,并不是所有的反射值都有。

值的CanSet方法报告值是否可以设置;

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

输出:
settability of v: false

可设置性有点类似 可寻址,但是更严格。是否可修改反射对象的实际存储的值(用来创建反射对象的实际的存储的值)是一个属性。可设置性由 是否反射对象hold原item决定。看下面的代码:

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

我们传递了一个copy 到 reflect.ValueOf,因此 传递给reflect.ValueOf 的 interface值是x的copy,而不是x本身。因此,如果下面的语句:

v.SetFloat(7.1)

允许成功的话,它不会更新x,即使 v 看起来是从 x 创建的。相反,它将更新存储在反射值内的 x的copy,x本身将不受影响。那样会让人困惑,并且没有什么用,因此是非法的,可设置性是为了避免这种情况的属性值。

如果这看起来很古怪,其实并不是。这是熟悉的情形,但是不同的形式。想象传递x到函数:

f(x)

我们不希望f可以修改x,因为我们传递了x的一个copy,不是x本身。如果我们想要f 直接修改x,我们必须传递x的地址(即,指向x的指针):

f(&x)

这样简单并且熟悉,反射类似这种方式工作。如果我们想通过反射修改x,我们必须传递想要修改的值的指针。

我们来这样做。首先我们像往常一样初始化x,创建一个反射值p指向x。

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方法(通过指针间接指向),保存反射值到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

反射很难理解,但是正确实现了语言的功能,尽管反射的类型和值掩盖了一些底层的实现。记住,要修改反射值需要指向该值的地址。

Structs
在上个例子中,v本身并不是一个指针,是从一个指针派生过来的。常见的情形是使用反射来修改struct的字段。只要我们有这个结构的地址,我们就可以修改它的字段。

有个简单的列子,可以分析struct值,t。我们使用struct的地址创建反射对象,因为后面要修改它的值。我们将typeOfT设置为其类型,并使用直接的方法调用 遍历字段。注意,我们从struct类型中取出字段名,但是字段本身是普通的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 struct的字段名都是大写的,因为只有暴露的字段才可以设置。

因为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 的字段不可设置。

    Conclusion

下面重复下三个法则:

1. 反射从interface 值 到 反射对象。
2. 反射从反射对象到interface 值。
3. 要修改反射对象,值必须可以设置。

一旦你理解了Go的这些反射法则,将会变得更好使用,尽管仍然有些微妙的地方。这是个强大的工具,应谨慎使用,如非必要避免使用。

还有一些内容这里没有介绍,在channel上发送和接收,分配内存,使用slice和map,调用方法和函数,但是这个文章已经很长了。。。我们将会在后续的主题中介绍
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值