Golang的反射机制(The Laws of Reflection)

时间:2022-10-26 21:27:45

原文来自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.Readerio.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中的两种基本类型:TypeValue。这两种类型得以分析接口类型变量的值与类型。同时引入两个简单函数reflect.TypeOf以及reflect.ValueOf。用来检索reflect.Typereflect.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.Typereflect.Value变量都有一大堆的方法用来操纵它们。比如Value类型变量就有一个名曰Type的方法用来返回一个reflect.Type类型的变量。同时,TypeValue类型的变量都有一个Kind方法用来返回一个指示变量类型的常量。而Value类型变量还拥有一些名如IntFloat的方法用来获取内部存储的值(返回的是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

当然,我们还有类似SetIntSetFloat的方法用来设置内部的值。但是使用的时候要小心,这关系到一个叫做 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)