golang反射基本准则

原文

https://blog.golang.org/laws-of-reflection

介绍

在计算中,反射(reflection)是程序检索其自身结构的能力,特别是通过类型;这是元编程的一种形式。这也是造成混乱的重要原因。

在本文中,我们尝试通过解释反射在Go中的工作原理来澄清事物。

Types and interfaces

因为反射是基于类型系统,所以先说一下go的类型。

Go是静态类型。每个变量有一个静态类型,也就是说,类型是在编译时就清楚和确定的:int, float32, *MyType, []byte, 等等。如果我们定义了

type MyInt int
​
var i int
var j MyInt

然后变量i具有类型int,j具有类型MyInt(i has type int and j has type MyInt)。变量i和j拥有不同的类型,尽管他们底层的类型(same underlying type)是一样。他们不进行转换是不能直接相互赋值的。

接口类型(interface types,)是类型中的一个重要分类,它代表固定的方法集(fixed sets of methods)。一个接口变量可以存储任何类型的值,只要值的类型实现了接口中方法集。一对知名的接口是io.Reader和io.Writer,类型Reader和Writer来自io.package:

// Reader 是包含最基本 Read 方法的接口.
type Reader interface {
    Read(p []byte) (n int, err error)
}
​
// Writer 是包含最基本 Write 方法的接口.
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的接口是动态的,但是这个误导人的。它们是静态的,一个接口类型的变量总是拥有一个的静态类型,尽管在运行时,接口变量中存储的值可能会更改类型,该值将始终满足接口要求。

我们需要对所有这些事情都保持清楚,因为反射和接口密切相关。

接口的表示

Russ Cox写过一篇详细说明接口值表示的博客。下面只是简单描述一下,不重复讲述。

接口类型的变量存储了一对数据:变量的具体值和变量的类型描述。更准确地说,该值是实现接口的基础具体数据项,类型描述该项的完整类型。例如,

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)

上面是类型断言(type assertion)。它断言r内的项目也实现了io.Writer,因此我们可以将其分配给w。赋值后,w将包含对(tty,* os.File)。这与在r中持有的对相同。接口的静态类型确定可以使用接口变量调用哪些方法,即使内部的具体值可能具有更大的方法集。

继续,我们可以这样做:

var empty interface{}
empty = w

我们的空接口值empty将再次包含同一对(tty,* os.File).这很方便:一个空接口可以保存任何值,并包含我们可能需要的有关该值的所有信息。

我们这里不需要类型声明,因为它可以静态地知道w满足空接口。在将值从Reader移到Writer的示例中,我们需要明确并且使用类型断言,因为Writer的方法不是Reader的子集。

一个重要的细节是一个接口里面的对值总是以(value, concrete type) 的形式存在,不能以 (value, interface type)格式存在。接口不保存接口值。

反射的法则:

1.反射从接口值到反射对象

在基本层面上,反射是检索一个接口变量中存储的类型和值的机制。开始之前,我们需要知道package.reflect中有两个类型:TypeValue。这两种类型允许访问接口变量的内容,还有两个简单的函数,称为reflect.TypeOf和reflect.ValueOf,从接口值中检索reflect.Type和reflect.Value。(另外,从reflect.Value可以很容易地到达reflect.Type,但是现在让Value和Type概念分开。)

package main
​
import (
    "fmt"
    "reflect"
)
​
func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

输出

type: float64

你可能在想这里的接口在哪里,程序中看起来是给reflect.TypeOf传递了float64的变量,而非一个接口变量。但是这里,正如godoc反提及到,reflect.TypeOf方法签名中包含一个空接口:

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

当我们调用reflect.TypeOf(x)时,x首先存储在一个空接口中,然后将其作为参数传递;Reflection.TypeOf uppack该空接口以恢复类型信息。

当然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.Value的Type。另一个是Type和Value都具有Kind方法,该方法返回一个常量,指示存储的项目类型:Uint,Float64,Slice等。同样,使用诸如Int和Float之类的Value方法可以让我们获取存储在其中的值(如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保持简单,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.

第二个属性是反射对象的种类描述基础类型,而不是静态类型(the underlying type, not the static type)。如果反射对象包含用户定义的整数类型的值,例如

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v的King仍旧是reflect.Int,尽管x的静态类型是MyInit而不是int。换一种说法,即使Type可以,Kind也不能从MyInt区分int。

 

2.反射从反射对象到对口值

像物理反射一样,Go中的反射会生成自己的逆。(Like physical reflection, reflection in Go generates its own inverse.)

给定一个reflect.Value,我们可以使用Interface方法恢复接口值;实际上,该方法将类型和值信息打包回接口表示形式并返回结果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

打印反射对象v表示的float64值。

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

不过,我们可以做得更好。 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 {}。

重申:反射从接口值到反射对象,然后再返回。

3.要修改反射对象,值必须是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不可设置的(settable)。可设置性是反射值的属性,并非所有反射值都具有它。

值的CanSet方法可以查看值的可设置性;

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

在non-settable的值上调用Set方法是错误的。但是什么是settability?

可设置性有点像可寻址性,但是更严格。它是反射对象可以修改用于创建反射对象的实际存储的属性。Settability 由反射对象是否保留原始项目确定。

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

上面代码中我们传递x的副本给reflect.ValueOf,因此,作为reflect.ValueOf的参数创建的接口值是x的副本,而不是x本身。

普遍情况,考虑将x传递给函数:f(x) 中我们不希望f能够修改x,因为我们传递了x值的副本,而不是x本身。如果我们想让f直接修改x,则必须将x的地址(即指向x的指针)传递给函数:

f(&x)

这是直接且熟悉的,并且反射的工作方式相同。如果要通过反射修改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。通过Value中的方法Elem获得*p。它通过指针进行间接操作,并将结果保存在称为v的反射值中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

输出

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本身并不是指针,它只是从一个指针派生的。常见情形是使用反射来修改结构的字段。只要有了结构的地址,就可以修改其字段。

是一个分析结构值t的简单示例。我们使用结构的地址创建反射对象,因为稍后将要对其进行修改。请注意,我们从结构类型中提取了字段的名称,但是字段本身是常规的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.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
t is now {77 Sunset Strip}

如果我们修改程序,以便从t而不是&t创建s,则对SetInt和SetString的调用将失败,因为t的字段不可设置。

还有很多我们尚未涵盖的反射。

sending and receiving on channels

allocating memory

using slices and maps

calling methods and functions

下回再探讨。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FatherOfCodingMan

如果觉得有用的话,可以赏点饭钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值