美文网首页
2021-06-13

2021-06-13

作者: 陈光岚_强化班 | 来源:发表于2021-06-13 13:24 被阅读0次

    什么是反射

    直接看维基百科上的定义:

    在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

    那我就要问个问题了:不用反射就不能在运行时访问、检测和修改它本身的状态和行为吗?

    问题的回答,其实要首先理解什么叫访问、检测和修改它本身状态或行为,它的本质是什么?

    实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?可以的!使用汇编语言,直接和内层打交道,什么信息不能获取?但是,当编程迁移到高级语言上来之后,就不行了!就只能通过反射来达到此项技能。

    不同语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:

    Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

    为什么要用反射

    需要反射的 2 个常见场景:

    有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。

    有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。

    在讲反射的原理以及如何用之前,还是说几点不使用反射的理由:

    与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。

    Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。

    反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

    反射是如何实现的

    interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

    Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

    types 和 interface

    Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了的,比如 int, float64, []int 等等。注意,这个类型是声明时候的类型,不是底层数据类型。

    Go 官方博客里就举了一个例子:

    type MyInt int

    var i int

    var j MyInt

    尽管 i,j 的底层类型都是 int,但我们知道,他们是不同的静态类型,除非进行类型转换,否则,i 和 j 不能同时出现在等号两侧。j 的静态类型就是 MyInt。

    反射主要与 interface{} 类型相关。前面一篇关于 interface 相关的文章已经探讨过 interface 的底层结构,这里再来复习一下。

    type iface struct {

    tab  *itab

    data unsafe.Pointer

    }

    type itab struct {

    inter  *interfacetype

    _type  *_type

    link  *itab

    hash  uint32

    bad    bool

    inhash bool

    unused [2]byte

    fun    [1]uintptr

    }

    其中 itab 由具体类型 _type 以及 interfacetype 组成。_type 表示具体类型,而 interfacetype 则表示具体类型实现的接口类型。

    iface 结构体全景

    实际上,iface 描述的是非空接口,它包含方法;与之相对的是 eface,描述的是空接口,不包含任何方法,Go 语言里有的类型都 “实现了” 空接口。

    type eface struct {

        _type *_type

        data  unsafe.Pointer

    }

    相比 iface,eface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

    eface 结构体全景

    还是用 Go 官方关于反射的博客里的例子,当然,我会用图形来详细解释,结合两者来看会更清楚。顺便提一下,搞技术的不要害怕英文资料,要想成为技术专家,读英文原始资料是技术提高的一条必经之路。

    先明确一点:接口变量可以存储任何实现了接口定义的所有方法的变量。

    Go 语言中最常见的就是 Reader 和 Writer 接口:

    type Reader interface {

        Read(p []byte) (n int, err error)

    }

    type Writer interface {

        Write(p []byte) (n int, err error)

    }

    接下来,就是接口之间的各种转换和赋值了:

    var r io.Reader

    tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)

    if err != nil {

        return nil, err

    }

    r = tty

    首先声明 r 的类型是 io.Reader,注意,这是 r 的静态类型,此时它的动态类型为 nil,并且它的动态值也是 nil。

    之后,r = tty 这一语句,将 r 的动态类型变成 *os.File,动态值则变成非空,表示打开的文件对象。这时,r 可以用<value, type>对来表示为: <tty, *os.File>。

    r=tty

    注意看上图,此时虽然 fun 所指向的函数只有一个 Read 函数,其实 *os.File 还包含 Write 函数,也就是说 *os.File 其实还实现了 io.Writer 接口。因此下面的断言语句可以执行:

    var w io.Writer

    w = r.(io.Writer)

    之所以用断言,而不能直接赋值,是因为 r 的静态类型是 io.Reader,并没有实现 io.Writer 接口。断言能否成功,看 r 的动态类型是否符合要求。

    这样,w 也可以表示成 <tty, *os.File>,仅管它和 r 一样,但是 w 可调用的函数取决于它的静态类型 io.Writer,也就是说它只能有这样的调用形式: w.Write() 。w 的内存形式如下图:

    w = r.(io.Writer)

    和 r 相比,仅仅是 fun 对应的函数变了:Read -> Write。

    最后,再来一个赋值:

    var empty interface{}

    empty = w

    由于 empty 是一个空接口,因此所有的类型都实现了它,w 可以直接赋给它,不需要执行断言操作。

    empty=w

    从上面的三张图可以看到,interface 包含三部分信息:_type 是类型信息,*data 指向实际类型的实际值,itab 包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各种方法

    相关文章

      网友评论

          本文标题:2021-06-13

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