美文网首页
Go的反射法则

Go的反射法则

作者: 绝望的祖父 | 来源:发表于2018-03-04 13:49 被阅读93次

    在计算机科学中,反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力,属于元编程的范畴。在这篇文章中, 我们将尝试解释反射在Go中如何工作。

    类型和接口

    因为反射构建于类型系统之上,所以我们首先回顾一下Go的类型系统。

    Go是一种静态类型语言。每个变量都有一个静态类型,也就是说,在编译时每个变量有且仅有一个已知的类型:int, float32, *MyType, []byte 等等。假设程序中保护如下代码:

    type MyInt int
    
    var i int
    var j MyInt
    

    那么变量 i 具有类型 int,变量j具有类型MyInt。尽管变量ij具有相同的基础类型,但它们仍然具有不同的静态类型。他们不能在没有类型转换的情况下彼此赋值。

    有一类重要的类型就是接口类型,它们包含固定的方法集合。只要该值实现了所有的接口方法,接口变量就可以存储任何具体的(非接口)值。众所周知的一个例子就是io.Readerio.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)
    }
    

    任何实现具有此签名的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{}
    

    它表示空白方法集合,任何类型都可以赋予该类型的变量,因为任何类型都包含零个或多个方法。

    某些人可能认为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方法以外的其他方法,即使接口的值(r)只提供了对Read方法的访问,但是值tty包含了类型*os.File的所有信息,这就是为什么我们可以折么做:

    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的子集)

    一个重要的细节是,接口内部的对总是具有(值,具体类型)这样的形式,而不是(值,接口类型)这样的形式,接口不能包含接口值。

    现在,我们来讨论一下反射。

    1. 从接口值到反射对象
    从根本上来说,反射只是一种检查存储在接口变量内的类型和值对的机制。为了开始使用反射,我们需要了解reflect包中的2类反射机制:Type和Value。这两类反射可以访问接口变量中的内容,而对应的两个简单的函数就是reflect.TypeOf和reflect.ValueOf,它们可以从接口变量中检索reflect.Type和reflect.Value。(另外,从reflect.Value中可以很容易得到reflect.Type,但现在让我们将Type和Value这两个概率分开)

    让我们从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。但实际上,reflect.TypeOf函数的签名包含一个空接口:

    // TypeOf returns the reflection Type that represents the dynamic type of i.
    // If i is a nil interface value, TypeOf returns nil.
    func TypeOf(i interface{}) Type
    

    当我们调用reflect.TypeOf(x)函数时,变量x首先保存在一个作为参数传递的空接口中,reflect.TypeOf解压这个空接口并还原类型信息。

    同样的,reflect.ValueOf函数将会还原对应的值信息:

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

    程序将打印:

    value: 3.4
    

    类型reflect.Typereflect.Value都包含了许多方法,让我们可以检查和操作它们。一个重要的例子是reflect.Value包含一个Type方法可以返回reflect.Value对应的类型。另一个是reflect.Valuereflect.Type都具有的Kind方法,它返回一个枚举,表示存储了什么类型的项:Uint, Float64, Slice 等等。除此以外,reflect.Value中使用类型名字作为函数名的方法(Float, Int)可以让我们获取存储在里面的具体值:

    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这样的方法,但是为了使用它们,我们需要了解可设置性,这是第三个反射定律的主题,后面会讨论。

    反射库有一些值得特别指出的特性。首先,为了简化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.
    

    另一个特性是Kind方法返回的是基础类型,而不是静态类型。如果一个反射的对象包含用户定义的整数类型值,如:

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

    那么v.Kind()仍然是reflect.Int,即使x的静态类型是MyInt。换句话说,即使Type方法可以区分int和MyInt,Kind方法也不可以。

    2. 从反射对象到接口值
    就像物理反射一样,Go中的反射也可以产生它自己的反转。

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

    // Interface returns v's current value as an interface{}.
    func (v Value) Interface() (i 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)?因为v是一个reflect.Value对象,我们希望获取它表示的实际值)。由于v的实际值是float64,我们也可以使用浮点数格式字符串进行打印:

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

    结果是:

    3.4e+00
    

    同样,不需要将v.Interface()的结果断言为float64,空接口参数中包含了具体的类型信息,Printf会恢复它。

    简而言之,Interface方法与ValueOf方法互反,只不过它的结果始终是静态类型interface {}

    3. 为了修改反射对象,其值必须是可设置的
    第三条法则最容易引起混乱,不过假如你是从第一条法则开始学习的,那也很容理解。

    以下是一些无法正常工作的代码,但是值得研究:

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

    CanSet方法可以返回reflect.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的副本,而不是x本身。因此,如果语句:

    v.SetFloat(7.1)
    

    运行执行,它将不会更新x,即使v看起来像是通过x创建的。相反,它会更新存储在反射对象中的值而x本身不会受到影响。这会令人困惑,并且也是无意义的。因此这样的操作是非法的,而可设置性属性可用来避免此类问题。

    这看起来很奇怪,其实并不是。想象一下把参数x传递给一个函数:

    f(x)
    

    我们不希望函数f可以修改参数x,因为我们传递的是x的副本,而不是x本身。如果我们希望函数f可以直接修改x,我们必须将x的地址作为参数传递给f

    f(&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指向的地方,我们使用一个称之为Elem的方法,它通过指针进行间接寻址,并将结果保存在一个名为vreflect.Value中:

    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
    

    反射可能很难理解,尽管reflect.Typereflect.Value可以掩饰正在发生的事情,但它的确在做语言的工作。 请记住reflect.Value需要某些对象的地址才能修改它们代表的内容。

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

    这里有一个简单的例子,它分析一个结构体t。我们使用结构体的地址创建反射对象,因为我们稍后需要修改它。然后,我们将typeOfT设置为它的类型,并直接使用方法遍历它的字段。请注意,我们从结构体中提取字段名称,但是字段本身是常规的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时将会失败,因为此时t的字段将是不可设置的。

    相关文章

      网友评论

          本文标题:Go的反射法则

          本文链接:https://www.haomeiwen.com/subject/oguxfftx.html