23

【译】Go反射的三个原则(官方博客) | seven的分享

 4 years ago
source link: https://sevenyu.top/2019/12/21/laws-of-reflection.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文翻译自Go官方博客,对翻译内容有疑问,请在评论指出。

计算机的反射是程序审查自身结构的能力,特别是通过类型来反射。反射也是元编程的一种形式。我们也总是被它搞得很迷惑。

这篇文章尝试通过介绍Go中的反射是如何工作的让大家理解反射。每个语言的反射模型是不一样的(甚至有很多语言是不支持反射的),这篇文章是关于Go的,所以下文中的反射特指 “Go中的反射”。

类型和接口

因为反射是建立在类型之上的,我们首先复习一下类型。

Go是静态类型语言。每一个变量都有一个静态类型,在编译时就可以清楚的知道每个变量的类型:int, float32, *MyType, []byte 等等。如果我们定义

type MyInt int

var i int
var j MyInt

i 的类型是intj的类型是MyInt,变量i和j用明确的静态类型,尽管他们的底层真实类型是一样的(int),但是如果不通过类型转换,两个变量是不能相互指派的。

interface 类型是一个很重要的类型,它代表一组固定的方法。一个接口变量可以接受任何一个实现该接口方法的值。我们熟知的一对例子就是 io.Readerio.WriterReaderWriter类型来自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: 是静态类型,r的静态类型是io.Reader

一个非常重要的接口类型是空接口

interface{}

它相当于是一组空方法,能满足于任何值。因为任何值都是拥有>=0个方法的。

一些人会说Go的接口是动态类型,这是误导。一个接口类型变量总是同一个静态类型,即使在运行时,存储在接口变量中的值可能会更改类型,但该值将始终满足接口要求。

我们必须清楚的知道这些,因为反射和接口是密切相关的。

接口的表示

Russ Cox 已经写了一个关于Go接口值表示的博客detailed blog post ,没有必要再此重复说。但是下面有一个简单的总结

接口类型的变量存储一对信息:给变量分配的具体值Value和类型描述符Type,确切的说,Value是实现接口的底层具体数据项,Type描述数据项的类型。例如:

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)。一个空接口能被赋予任意的值并且包含我们需要的所有信息。

(这里我们并不需要类型断言,因为w满足空接口要求,上一个例子中,我们将一个值从 Reader转换成 Writer,我们需要一个明确的类型断言是因为Writer的方法并不是Reader方法的子集)

一个重要的细节是接口内的信息对始终具有(值,具体类型)的形式,而不是(值,接口类型)。 接口不保存接口值。

以上介绍完毕,我们现在来看反射

反射第一原则

反射从接口值变为反射对象

在基本层面上,反射是检查一个接口变量内部存储的类型和值的机制,开始之前,需要了解这两个类型 package reflect: Type and Value。这两个类型提供访问接口变量的访问入口:两个简单的函数reflect.TypeOf and reflect.ValueOf,它们能获取接口变量对应的 reflect.Type and reflect.Value 。(通过reflect.Value可以很容易获取到reflect.Type,但是现在暂时将ValueType概念分开)

我们首先看TypeOf:

package main

import (
    "fmt"
    "reflect"
)

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

您可能想知道接口在哪里,因为程序看起来像是在传递float64变量x,而不是接口值,对reflect.TypeOf来说。接口在这里:godoc reportsreflect.TypeOf的方法签名参数包括一个接口变量

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

我们调用reflect.TypeOf(x)x首先存储在一个空接口上,然后通过参数传递;reflect.TypeOf解析并恢复类型信息

reflect.ValueOf方法,会获取值(从这里开始,我们将只关注可执行代码)

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

(我们明确地调用String方式是因为fmt包默认会挖掘reflect.Value来显示内部具体值。 String方法不会。)

reflect.Typereflect.Value 都有很多方法让我们来操作。一个重要的例子是Value有一个Type方法返回一个reflect.ValueType。还有一个是,Type and Value都有一个叫Kind的方法,改方法返回对应存储的一个常量标示:Uint, Float64, Slice等等。Value里面还有一些像IntFloat 的方法让我们可以获取存储在其中的值(如int64 and 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,但是要使用它们,我们需要了解一些规则,这是下面讨论的第三反射原则的主题。

反射库有几个值得特别指出的属性。首先,为了使应用编程接口简单,Value的“getter”和“setter”方法在最大的类型上操作,该类型可以保存所有有符号整数的值:例如,int64。也就是说,ValueInt方法返回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不能将intMyInt区别开。虽然Type可以。

反射第二原则

反射从反射对象变为接口值

就像物理反射一样,Go的反射会生成自己的逆函数

利用reflect.ValueInterface方法可以恢复接口值,此方法将类型和值信息打包返回到接口值

// 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包在内部对其进行解析。 因此,正确打印reflect.Value的内容所要做的就是将Interface方法的结果传递给fmt.Println(v interface{})

fmt.Println(v.Interface())

(为什么不使用fmt.Println(v)?因为vreflect.Value;我们想要它拥有具体的值。)因为值是一个float64类型的,我们可以用浮点数格式打印结果:

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

会得到如下结果

3.4e+00

再次声明,这里不需要通过类型断言将 v.Interface()的结果转变成float64类型;空接口值内部含有具体值的类型信息,Printf可以恢复类型信息。

简而言之,Interface()ValueOf()的逆方法,除非Interface()的结果总是静态类型interface{}

重申: 接口值<===>反射对象是一个可逆过程

反射第三原则

要修改反射对象,该值必须是可设置的

第三个原则最微妙且让人迷惑,但是我们如果是从第一个原则开始的是很容易理解的

这里有些代码不能正常运行,但是值得我们学习

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

Value对应的CanSet方法会指出Value是否可设置

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

调用一个不可设置的Value值对应的Set方法会得到一个错误,那么什么是可设置性呢?

可设置性是有点像可寻址行,但是更严格。它是反射对象可以修改实际存储值的属性。可设置性取决于反射对象是否拥有原始的数据项,当我们说

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

我们传递x的值拷贝给reflect.ValueOf,并非x本身

v.SetFloat(7.1)

因此如果上述语句被允许成功执行,x的值并不会更新。更新的值是x的拷贝数据,这个操作是没有意义的而且会给人带来困惑。所以此操作是非法的,可设置性是用于避免此问题的属性

考虑将x传递给一个方法:

f(x)

我们不会指望f会去更新x因为我们传递的是x的值拷贝,并不是x本身。如果我们想要更新的话,我们可以将x的地址传递过去(x的指针)

f(&x)

这看起很直接且很熟悉,反射也是同样的工作方式。如果我们想要通过反射更新x,我们必须给反射库x对应的指针

我们这么做,首先初始化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(具体值)。我们调用ValueElem方法获取p对应的具体值。Elem方法通过指针返回一个反射Value 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

反射可能很难理解,但是它确实在做语言所做的事情,尽管通过反射TypesValues可以掩盖发生的事情。但是 请记住,反射值需要内容的地址才能修改其表示的内容。

在我们前面的例子中,v本身不是指针,它只是从一个指针派生出来的。出现这种情况的常见方式是使用反射来修改结构的字段。只要我们有结构的地址,我们就可以修改它的字段。

这里有一个简单的例子来分析结构值t。我们用结构的地址创建反射对象,因为我们希望以后修改它。然后,我们将typeOfT设置为它的类型,并使用简单的方法调用遍历字段(请参见package reflect )。请注意,我们从结构类型中提取字段的名称,但是字段本身是常规的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包含可设置的反射对象,所以我们可以修改此结构体的字段

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

如果我们修改程序,将s的来源从&t修改成t,再调用SetIntSetString方法将会得到一个失败的结果

反射原则如下:

  • 反射从接口值变为反射对象
  • 反射从反射对象变为接口值
  • 要修改反射对象,该值必须是可设置的

一旦你理解这些原则,反射将很容易使用。尽管它还是很微妙。反射是一个强大的工具,除非绝对必要,否则应该小心使用并避免使用。

还有很多关于反射的知识在这里没有涉及到——在channels中接受/发送、内存分配、使用slices和maps、调用函数和方法——但是这篇文章内容已经足够多了,我们将在后面的文章中讨论其他的主题


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK