原文来自The Go Bloghttp://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拥有相同的基类型,然而他们是不同的静态类型,不通过转换将不能相互赋值。
接口类型是一类十分重要的类型,表示了一堆固定的方法集合。一个接口类型可以存储任意的混合值(非接口),只要该类型实现了接口定义的方法集。一对广为人知的例子是io.Reader和io.Writer。下面的例子中接口Reader和Writer来自于包 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类型的变量可以存储任意实现了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{}
它包含了空方法集。由于任何类型都至少实现了0个或多个方法,所以空接口可以承接任意类型。
有些人以为Go的接口类型是动态类型,实际上是不对的。接口类型仍旧是静态类型:某个接口类型的变量的类型始终不变,即使在运行时其内部存储的接口变量(confusing?没关系,继续看)在变换值,他们始终是该接口类型。
我们需要明确这一点,因为反射机制和接口类型密切相关。
The representation of an interface(接口的表示)
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语义上可以理解为一个(value, type)对,在这里是(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)。这很方便,因为空接口可以承接任意的类型,并将该类型的变量信息完全保留。
(在这里我们不需要类型断言,由于空接口一定能够承接成功。在上面的例子中我们将值从一个Reader类型的接口变量中传递到Writer接口变量。需要注意的是必须显示的使用类型断言,因为Writer的方法集不是Reader的方法集的子集)
有一个重要的细节是接口变量逻辑上存储的值是(value, concrete type)而不是(value, interface type),接口类型变量不能存储接口类型变量值。
需要注意的被接口承接的值是值传递,从Russ Cox的博客中我们知道接口类型对内部值得存储是值传递,即一个变量赋值给了一个接口变量,如果改变了原始的变量,其由接口存储的值也不会改变。
好啦,终于可以开始将反射机制了!!!
The laws of reflection(反射机制)
1. Reflection goes from interface value to reflection object.(反射可以从接口类型到反射类型对象)
基本的来说,反射仅仅是一种在接口中校验类型和值的机制。我们先来引入package reflect中的两种基本类型:Type和Value。这两种类型得以分析接口类型变量的值与类型。同时引入两个简单函数reflect.TypeOf
以及reflect.ValueOf
。用来检索reflect.Type
和reflect.Value
变量。(当然,从reflect.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类型呢。实际上,Typeof函数接受的参数是:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
Go语言传参的时候先做了类型转换再传参压栈的哈。
同样的,reflect.ValueOf
函数用来取出对应的值(但仍然是reflect.Value类型的变量)
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
输出:
value: 3.4
reflect.Type和reflect.Value变量都有一大堆的方法用来操纵它们。比如Value类型变量就有一个名曰Type的方法用来返回一个reflect.Type类型的变量。同时,Type和Value类型的变量都有一个Kind方法用来返回一个指示变量类型的常量。而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
的方法用来设置内部的值。但是使用的时候要小心,这关系到一个叫做 settability(可设置性)的一个东西,细纹会细将。
反射库中有很多属性值得单独拎出来细讲。首先,为了保持API的简洁性,Get方法和Set方法有一定的简化考虑:比如用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,而不是MyInt这个静态类型。Type可以表示静态类型,而Kind不可以。
2. Reflection goes from reflection object to interface value.(反射可以从反射类型对象到接口类型)
就像物理里的反射定律一样,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)
实际上,由于fmt.Println()接受的是接口类型的变量,我们并不需要类型断言,可以直接将接口传入。
fmt.Println(v.Interface())
(为什么不直接 fmt.Println(v)?因为v的类型是reflect.Value,我们需要的是内部的具体值)。甚至,我们可以直接用float64类型的格式控制:
fmt.Printf("value is %7.1e\n", v.Interface())
得到:
3.4e+00
简单来说,interface方法是ValueOf方法的反函数。其结果总是静态类型interface{}
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: reflect.Value.SetFloat using unaddressable value
此处的问题在于v变量并不是可设置的。并不是所有的Value类型的变量都是可设置的。
CanSet方法可以用来检测Value类型的可设置性:
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
输出:
settability of v: false
不可以对不可设置变量调用Set方法。
可设置性有点像可寻址性,但更加严格一点。这是反射对象可以修改实际存储的被反射对象的能力。可设置性由反射对象是否能寻址原始对象来决定。
var x float64 = 3.4
v := reflect.ValueOf(x)
上面的代码中我们将x值做了一份拷贝传给reflect.ValueOf方法,所以传入的参数仅仅是拷贝,而不是x本身。如果我们允许下面的操作成功:
v.SetFloat(7.1)
这并不会更新x。这好像真的没有啥意义(设计者这样认为)。所以干脆定义它非法好了。
在平时的值传递的函数中我们也会遇到:
f(x)
这样的调用时不会期待它能修改x值得。如果我们想要修改x,就这样传好了:
f(&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和当初你上面的例子有何区别,要取它的指向的值才可以:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
现在v终于是一个可设置的反射对象了:
settability of v: true
由于它指代了x变量,我们可以通过v.SetFloat方法来修改它:
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
输出:
7.1
7.1
其实也蛮简单,记住一点,指针就是。
struct(结构体的特殊情况)
上面的例子中v是由一个对象引出的。一个常见的情况是使用反射机制去修改结构体的域。只要我们有结构体的地址,我们就能修改这个其中的域。
下面这个例子中我们可以提取域的名字,但是提取出的域本身也是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
注意结构体中的域名只有以大写字母开头的域才是可设置的。如下:
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
结果是:
t is now {77 Sunset Strip}
Conclusion(结论)
Go的反射机制总结就是:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
理解以上3条反射机制就很简单啦^^
当然还有很多反射内容没有讲,包括channel中的收发,分配内存,使用分片和map,调用方法和函数。这些都以后再讨论吧。
By Rob Pike(translator: xiaohu)