反射规则 the law of reflection

在看golang.org上一篇《the law of flection》,写点自己的理解和笔记。


翻译完以后还是感觉看原始的文章比较大。原因有以下几点:

  • 很多名字找不到一个合适的翻译,比如settable
  • 个别句子读原句时能理解意思,但是不知道怎么合适的组织成汉字
  • 英语的句子结构和中文不同,在翻译的时候会不自居的用英语的结构写出中文来,让很多的句子读起来难以理解
  • 文章中很多地方用词严谨,特别是一些专有名词,只是一味的翻译成中文,会让人混淆。比如value,很多时候不知该理解成是变量还是值。还有很多类似item,storage,it等等,实在很难找一个合适的指代。
所以这篇文章只是更多的作为参考。


类型和接口(type and interfaces)

 

因为接口是基于类型系统的(type system),所以首先看下一下go的类型

go是静态类型的。每个变量都有一个静态的类型(static type),也就是说一种类型在编译的时候被声明和确定了(one type known and fixed at complie time)。如果我们定义:

type MyInt int


var i int
var j MyInt

i的类型是int,j的类型是MyInt,i和j的类型是不一样的。尽管他们有着相同的底层类型(?)(underlying type),但是它们是不能在不转换的情况下互相赋值的。

类型里面一个重要的分类是接口类型(interface types),它表示一系列的固定方法。一个接口变量可以存储任何实现这个结构的实际变量(? concrete type)。一个出名的例子就是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)
}

任何类型以上面的特征(? with this signature)实现了Read(或者 Writer)方法,就可以说该类型实现了io.Read(或者io.Writer)。这就意味着接口io.Read类型的变量可以存放任何有Read方法的变量:

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

有一点要搞清楚,不管r里面存放的具体值是什么,r的类型一直都是io.Read:go是静态类型的语言,r的静态类型是io.Read。

一个关于接口的非常重要的例子就是空接口:

interface{}

上面代表一个空的方法集,可以存放任何数据类型。因为每个类型都有零个或者更多的方法。

有人说go的接口是动态类型(dynamically typed),这是一种误导。他们其实是静态的类型:一个接口类型的变量一直都有同样的静态类型(static type),虽然在运行的时候存放在接口类型变量里的值可能会变,但是那些值都会是实现过这个接口的。

我们需要严谨对待上面这些概念,因为反射和接口是紧密相连的。


关于接口的说明(the representation of an interface)

Russ Cox写过一篇博文,是关于go语言里的接口类型的。在这里简单的介绍一下:

一个接口类型的变量是成对存储的(stores a pair):分配给变量的具体值(concrete value)和该值的类型描述(value's type descriptor)。更准确说,这个值是实现了该接口的底层具体数据(? the underlaying concrete data item),类型(type)描述表示这个数据的所有类型信息(the type describes the full type of that item)。有例为证:

var r io.Reader
    tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    if err != nil {
        return nil, err
    }
    r = tty

r包含了值对(the pair),【value,type】,【tty,*os.File】。这里要注意类型*os.File继承了不止read一个方法。尽管这里的接口类型只提供Read一种方法,但是值(value)里面带有这个关于这个数值的所有信息。这就是为什么我们这么做:

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

这里的赋值表达式是一个类型声明(type assertion)。这个所声明里,r中的元素同时也继承了io.Writer,所以我们可以赋值给w。赋值以后,w就会存有值对(pair):【tty,*os.File】。和存放在r中的值对是一样的。接口的静态类型要求接口变量只能使用接口里所定义的方法,尽管接口变量中存放的值(value)可能拥有大量额方法。

继续上面的例子,我们还可以这么做:

var empty interface{}
empty = w

这的空接口变量,empty,一样的包含有同上面相同的值对(pair),【tty,*os.File】。也就是说一个空的接口能存放所有的变量,包含有关于这个变量我们所需要的所有信息。


(我们这不需要一个类型声明,是因为我们都知道w是符合空接口的。在这个例子里,我们把Read转换成Writer,我们是需要特别的类型声明的,这是因为Writer的方法不是Reader方法的子集。)


注意一个重要的细节,存放在接口变量中的pai存放的类型一直都是【value,concrete type】,而不是【value,interface type】。接口中不保存接口变量。

ok,我们现在可以了解一下反射了。


反射的第一法则(the first law of reflection)

1、从接口变量到反射对象的反射(reflection goes from interface value to reflection object)

简单的说,反射只是检查存放在一个接口变量里的类型和值对(type and value pair)的机制。要了解这些,我们需要知道reflect包里的两个类型:Type和Value。这个两个类型提供了访问结构变量里内容的途径,两个函数,reflect.TypeOf和reflect.ValueOf,可以获得从一个接口数据里面获取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

你可能会好奇,这里哪有接口?程序里里面我们传递给reflect.TypeOf的x变量时float64,而不是接口啊。但是更具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拆开(unpacks)这个空的接口,获取类型的信息(type information)。

那么reflect.ValueOf函数自然是用来获取值(value)的(这里我们忽略样板(? boilerplate),先直关注可执行的代码):

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

value: <float64 Value>

reflect.Type和reflect.ValueOf都拥有很多的方法供我们使用。一个非常重要的例子就是Value有一个Type的方法,这个方法可以从refelct.Value中获得Type。另外一个Type和Value都有的方法是Kind,这个方法可以返回一个常量说明(? a constant indicating),类似:Uint和Float64等等。同时Value里卖弄还有类似Int和Float的函数,能让我们获取其中获取的值:

    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的方法,但是使用这些方法前我们需要了解settability(?)这个是下面第三部分才能谈到的,待会再讨论。


反射库里面有两个属性需要特别指出来。第一,为了让api简单,Value的setter和getter方法都是基于可以用来保存数据的最大数据类型的:比如所有的有符号整形操作都是基于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方法描述的是底层类型(underlying type),不是静态类型。如果一个反射对象包含有一个用户自定义类型,如下:

    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
v的Kind方法返回值依旧是reflect.Int,尽管x的静态类型是MyInt而不是int。也就是说,Kind方法不能识别基于int的MyInt类型,尽管Type是可以的。



反射的第二法则(the second law of reflection)

2、从反射对象到接口变量的反射(reflection goes from reflection object to interface value)

就像物理世界中的反射,Go中的反射也有自己反转(inverse)。

给我一个reflect.Value,我们就可以通过Interface这个方法重新得到一个接口。实际上,这个方法吧type和value信息打包成一个接口引用,并且返回:

// 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包里被解包(unpacked)了。所以只要给上面的函数直接传递Interface方法的返回值就能正确的输出内容:

fmt.Println(v.Interface())

(为什么不直接使用fmt.Println(v)?这是因为v其实是一个reflect.Value;我们需要的是里面保存的确定的值(? concrete value))。既然我们的值的类型是float64,我们甚至可以使用浮点数类型:

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

然后我们得到了:

3.4e+00

再说一遍,我们不需要把v.Interface的结果类型转换(? type-assert)。这个空的接口值李阿敏包含有具体数据的类型信息,Printf可以从中获取的到。


简单的说,Interface是函数ValueOf的反转,只可惜的是它的结果都是静态类型interface{}

小结:从接口反射到反射变量,然后又反射回来。


反射的第三法则(the third law of reflection)

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

第三条法则是很为微妙(subtle)和让人困惑的(confusing),但是我们从第一法则(principles)来看的话的,也是很容搞懂的。

下面这些代码无法运行,但是值得我们学习:

 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是不可设置的(not settable)。可设置(settableility)是反射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

对一个不可设置(non-settable)的Value使用set就会报错。那么什么是可设置性(settabiliy)?


可设置性有点像可寻址(addressability),但是更加严格,它是表示一个反射对象可不可修改创建这个反射对象原始存储数据(storage)的属性

。可设置性取决于反射对象中保存的是不是原始的数据。当我们输入一下代码:

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

我们传递给reflection.ValueOf的是一个x的拷贝,所以作为 reflect.ValueOf的参数用来反射对象的接口,其实x的拷贝,而不是x本身。因此如果下面的代码:

v.SetFloat(7.1)

是允许运行成功的,也不睡更新x的,尽管看起来v是由x创建的一样。其实这样操作会更新x的拷贝在内存中的值,而x本身是不会受到影响的。这样的话会显得让人困惑,而且一点用的都没有。所以这种方法就被规定为非法的,可设置属性是不允许这么操作的。


可能这些看起来有些怪异(bizarre),但其实是很简单的。这其实是一种很常见的情况,只是有个不同的外在而已。想想当我们把x传递给一个函数的时候:

f(x)
因为我们传递给函数的是x的拷贝而不是x的本身,所以我们知道f不能修改x的值。如果我们想f能修改x的值,那么我们就必须给这个函数传递x的的地址了(也就是x的指针)

f(&x)

这样看起来就显得很简单很熟悉了,反射也是同样的运作方式的。如果我们也想让反射修改x的话,我们也必须给反射库(reflection library)一个我们想修改的值得指针。


让我们这操作一下试试。首先我们像往常一样初始化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所指向的数据,我们使用Value的Elem方法。这个方法可以通过指针间接访问数据,并且保存在反射Value的变量v中:

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

尽管反射执行着语言的功能,通过反射中Type和Values可以掩盖住具体的细节,但是可能还是难以去理解。你只需要记住了,Values需要一个数据的地址才能修改反射所代表的数据。


结构体(Structs)

在我们之前的例子里,v并不是指针本身,它只是指针的派生而已。这种方法适用于用反射来修改结构体的域(field)。只要我们有这个结构体的体质,我们就能修改修改这个结构体的域。


这里有一个简单额例子来分析一下一个结构体变量t。因为我们待会要修改这个结构体,所以我们先用结构体的指针创建反射对象。我们用typeOf保存它的类型,用直接的方法(? straightforward method calls)迭代出所有的域【参考反射包获取更多的细节】。注意我们这里导出了结构体域的名字,但是域本身还是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

需要注意的是我们这里使用的可设置性(? there's one more point about settability introduced in passing here):这里把T的域名(field name)导出来,是因为一个结构体只有导出的域才能设置。


因为s中保存着一个可设置的反射对象,所以我们可以修改结构体的域:


    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)

结果就是:

 is now {77 Sunset Strip}

如果你把程序改成s是由t创建的而不是&t,那么在你调用SetInt和SetString时程序就报错,因为此时t的域是不可修改的。


小结(conclusion)

反射的规则:

  1. 由接口到反射变量的反射
  2. 由反射对象到接口的反射
  3. 要修改一个反射对象,value必须是可设置的
一旦说你了解了这些规则,尽管难以理解,但是Go会变的更容易使用。这是一个强大的工具,在使用的时候要注意,在不是绝对必要的必要的时候不要使用。


关于反射我们还有很多没有涉及——在channels中的发送和接收、分配内存、使用slice和map、调用方法和函数——但是这篇文章内容已经足够多了。我们会在后面文章里涉及这些内容中的部分。










  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值