zoukankan      html  css  js  c++  java
  • The Laws of Reflection

    翻译自

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

    介绍


    电脑运算中的反射是程序检查其自身结构的能力,尤其是通过类型。这是元编程的一种形式。这也是造成混乱的重要原因。

    在本文中,我们试图通过解释反射在Go中的工作原理来澄清事物。每种语言的反射模型是不同的(许多语言根本不支持它),但是本文是关于Go的,因此对于本文的其余部分,应将“反射”一词理解为“ Go中的反射”。

    类型和接口


    由于反射建立在类型系统上,因此让我们从Go语言中的类型复习开始。

    Go是静态类型的。每个变量都有一个静态类型,也就是在编译时已知并固定的一种类型:int,float32,* MyType,[] byte等。如果我们声明

    type MyInt int
    var i int
    var J MyInt

    那么我的类型为int,j的类型为MyInt。变量i和j具有不同的静态类型,尽管它们具有相同的基础类型,但是如果不进行转换就无法将它们彼此分配。

    类型的一个重要类别是接口类型,它表示固定的方法集。接口变量可以存储任何具体(非接口)值,只要该值实现接口的方法即可。一对著名的示例是io.Reader和io.Writer,它们是io包中的Reader和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的接口是动态类型的,但这是误导的。它们是静态类型的:接口类型的变量始终具有相同的静态类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也将始终满足接口的要求。

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

    接口的表示

    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 包含(值,类型)对,(ty,*os.File)。注意,类型*os.File实现了Read以外的方法;尽管接口值只提供了对Read方法的访问,但里面的值携带了关于该值的所有类型信息。这就是为什么我们可以做这样的事情。

    var w io.Writer
    w = r.(io.Writer)

    这个赋值中的表达式是一个类型断言,它所断言的是r里面的项也实现了io.Writer,所以我们可以把它赋值给w。接口的静态类型决定了一个接口变量可以调用哪些方法,尽管里面的具体值可能有更大的方法集,但接口的静态类型决定了接口变量可以调用哪些方法。

    继续说下去,我们可以这样做。

    var empty interface{}
    empty = w

    和我们的空接口值空将再次包含同样的对,(ty,*os.File)。这很方便:一个空接口可以容纳任何值,并且包含了我们可能需要的关于该值的所有信息。

    (我们在这里不需要类型断言,因为静态地知道w满足空接口。在我们把一个值从Reader移到Writer的例子中,我们需要显式地使用类型断言,因为Writer的方法不是Reader的子集。)

    一个重要的细节是,接口里面的对子总是具有形式(值,具体类型),不能具有形式(值,接口类型)。接口不能持有接口值。

    现在我们准备开始反射了。


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


    从最基本的层面上看,反射只是一种检查存储在接口变量内部的类型和值对的机制。为了入门,我们需要了解包reflection中的两种类型。类型和值。这两种类型赋予了对接口变量内容的访问权,而两个简单的函数 reflect.TypeOf 和 reflect.ValueOf,可以从接口值中检索出 reflect.Type 和 reflect.Value 两种类型。(另外,从response.Value中很容易得到response.Type,但我们暂时把Value和Type的概念分开来处理)。

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

    您可能想知道接口在这里,因为该程序看起来像在传递float64变量x而不是接口值来反映.TypeOf。但是它在godoc中,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 {
        eface := *(*emptyInterface)(unsafe.Pointer(&i))
        return toType(eface.typ)
    }

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

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

    反射库具有几个值得一提的属性。首先,为使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
        fmt.Println(reflect.TypeOf(x))
        fmt.Println(reflect.ValueOf(x).Kind())
    
        // main.Myint
        // int

    v的Kind仍然是reflect.Int,即使x的静态类型是MyInt,而不是int。换句话说,即使 Type 可以区分出 int 和 MyInt,但 Kind 不能区分出 int 和 MyInt。

    2. 反射从反射对象到接口值。
    和物理反射一样,Go中的反射也会产生自己的反转。

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

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

    不过,我们可以做得更好。fmt.Println、fmt.Printf等的参数都是以空接口值的形式传递,然后由fmt包内部解压,就像我们在前面的例子中所做的那样。因此,要正确地打印出reflect.Value的内容,只需将接口方法的结果传递给格式化的打印例程即可。

        var x int = 2
        fmt.Println(reflect.ValueOf(x).Interface())      // 2

    同样,不需要将v.Interface()的结果类型断定为float64;空的interface值里面有具体的值的类型信息,Printf会将其恢复。

    简而言之,Interface方法是ValueOf函数的逆向函数,只是它的结果总是静态类型interface{}。

    反射从接口值到反射对象,再回到反射对象。


    3. 要修改一个反射对象,必须是可设置的值。


    第三条定律是最精妙的,也是最容易混淆的,但如果我们从第一条原则出发,就很容易理解了。

    下面是一些代码,虽然不能用,但值得研究。

    var x float64 = 3.4
    v := reflect.ValueOf(x)
    v.SetFloat(7.1) // Error: will panic.

    如果你运行这段代码,它将会以隐秘的信息发出警报,这时你将会出现以下信息

    panic: reflect: reflect.flag.mustBeAssignable using unaddressable value


    问题不在于7.1的值不能寻址,而在于v是不能设置的。Settability是反射值的一个属性,不是所有的反射值都有这个属性。

    Value的CanSet方法报告的是Value的可设置性;在我们的例子中。

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

    在一个不可设置的值上调用Set方法是错误的。但是,什么是可设置性?

    Settability有点像寻址性,但更严格。它是指反射对象可以修改反射对象的实际存储的属性。Settability是由反射对象是否持有原始项来决定的。当我们说

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


    我们向 reflect.ValueOf 传递一个 x 的副本,所以作为参数创建的接口值是 x 的副本,而不是 x 本身。因此,如果语句

    v.SetFloat(7.1)


    成功,它不会更新x,即使v看起来像是从x创建的,但它不会更新x。这样做会让人困惑,也没有什么用处,所以是不合法的,可设置性是用来避免这个问题的属性。

    如果说这看起来很奇怪,其实不然。实际上,这是一个熟悉的情况,披着不寻常的外衣。想一想,把x传给一个函数。

    f(x)
    我们不会期望f能够修改x,因为我们传递的是x的值的副本,而不是x本身。如果我们想让f直接修改x,我们必须给函数传递x的地址(也就是指向x的指针)。

    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,而是(实际上)*p。为了得到p所指向的对象,我们调用Value的Elem方法,它通过指针间接的方式,将结果保存在一个叫v的反射Value中。

        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.Elem().CanSet())
    type of p: *float64
    settability of p: true

    反射可能很难理解,但它做的正是语言所做的事情,尽管是通过反射类型和值来掩盖正在发生的事情。只是要记住,反射值需要有东西的地址才能修改它们所代表的东西。

    Structs


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

    下面是一个简单的例子,分析一个结构值t,我们用结构的地址创建反射对象,因为我们以后要修改它。然后,我们将typeOfT设置为它的类型,并使用直接的方法调用迭代这些字段(详情请参见包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
    ", 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,那么SetInt和SetString的调用就会失败,因为t的字段不能被设置。

    总结


    这里再来说说反射的规律。

    反射从接口值到反射对象,反射从接口值到反射对象。

    反射从反射对象到接口值,反射从反射对象到接口值。

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

    一旦你理解了这些定律,Go中的反射就会变得更容易使用,尽管它仍然很微妙。它是一个强大的工具,应该谨慎使用,除非严格必要,否则应该避免使用。

    反射还有很多我们没有涉及到的东西--通道上的发送和接收、分配内存、使用分片和映射、调用方法和函数--但这篇文章已经够长了。我们将在后面的文章中讨论其中的一些主题。

    一个没有高级趣味的人。 email:hushui502@gmail.com
  • 相关阅读:
    如何提高逻辑思维的能力 思维的层次 逻辑思维能力练习方式
    淘宝JAVA中间件Diamond详解之简介&快速使用 管理持久配置的系统
    java 取模运算% 实则取余 简述 例子 应用在数据库分库分表
    SpringMVC+Spring+mybatis项目从零开始--Spring mybatis mysql配置实现
    SpringMVC+Spring+mybatis项目从零开始--分布式项目结构搭建
    JS密码校验规则前台验证(不能连续字符(如123、abc)连续3位或3位以上)(不能相同字符(如111、aaa)连续3位或3位以上)
    12 个非常实用的 jQuery 代码片段
    html增加和删除div
    css改变hr颜色
    jquery怎么在点击li标签之后添加一个在class,点击下一个li时删除上一个class?
  • 原文地址:https://www.cnblogs.com/CherryTab/p/12826255.html
Copyright © 2011-2022 走看看