反射机制(Reflection)

何为反射



反射是一些面向对象程序设计语言提供的针对对象元数据(Metadata)的一种访问机制

元……数据??什么高深莫测的武功??

啊,诚然,一旦涉及到“元XXX”事情通常就开始变得无比抽象,以至于我不禁念叨起那句诀

不过元数据这个概念在数据库里还是比较常见的,比如,某个关系型数据库里有张表:

数据,就是存在表里的一条一条的记录,(1,苹果,6),(3,梨,5)都是数据,那么,元数据就是凌驾于这些数据之上的用于描述数据数据,对于这张表而言,也就是这张表的表头(关系数据理论里称之为关系模式):(编号,名称,数据)

好像有些明朗了,但那关面向对象什么事呢

众所周知,类(Class)是面向对象的一个重要概念,尽管,针对于数据库来说,对象模型和关系模型是不同的概念(上文提到的是关系模型的一个例子),但是,对象模型中的对象和关系模型中的关系,其级别是等同的。

关系……又对象……越来越听不懂了

好吧,我们先把关系放在一边,我们只把上边的东西看做一张表。

难道你就没有把它改写成如下形式的冲动吗??

public class Fruit
{
    public int no;
    public string name;
    public int count;

    public Fruit(int no, string name, int count)
    {
        // ...
    }
}

好了,上面的类定义的语义就是

那么,这样一来,我们就可以定义一个no为9,name叫做“西瓜”,count为5的一个对象,这个对象具有具体的数据。

而上面的类定义代码,包含的就是这个类的元数据

说的再直白点吧

以人为例,数据注重的是这人的脸长啥样,而元数据注重的是这人有没有脸(好像不太对……)

好吧差不多了解了,但元数据和反射有什么关系呢

本文一开始就说了,罚站20年

不过在此之前先解释一件事,元数据在哪

任何一个面向对象的程序设计语言,其类类型都具备一个元数据的存储,至少程序会使用这个元数据能够动态地构造此类的对象。但不同的语言机制不同,比如C++这种的,因为直接和系统进行愉♂快的互♂动,因此元数据就直接使用系统的内存地址了,这种数据使用是很不直观的,同时也不使用任何托管机制做后援(巨硬魔改的C++/CLI不在讨论范围内),因此这种贴近底层的语言不支持反射机制,虽然可以通过强行向程序代码中通过工厂类模式强行注入可读的元信息(方法参见这位大佬的文章)。

但是,正如前面所说的,如果元数据在托管编译或解释的状态下会保留一份可读的版本,这是提供给解释器或者托管平台用的,当然,这种情况下语言一般会提供一个较为完善的元数据访问机制,这就是反射。这类语言典型的代表就是C#(.NET托管)、Java(JVM虚拟机)、Python(解释器提供)等。

那……反射是如何运作的呢??



正如之前所说,反射机制是对类的元数据的获取和操纵,因此,一个重要的前提就是:

只有当类的元数据是可见的,反射机制才有访问它们的可能,但是元数据的可读性会决定反射机制访问它们的难易程度。

这里补充一句,有人会说,在使用IDE或者代码编辑器的时候,我们写object.property这种访问方式的时候编译器不就直接告诉我们了么??
关于这一点,这里暂时只说一个前提:

在程序代码编译之前我们恣意地书写这MyObject.id.hashCode.getFlush().balabala的时候,这是预编译的过程,预编译的时候当然这些元数据都是以字面形式给出的(因为你的代码里写了这个类的定义),你可以非常愉悦地Ctrl+C Ctrl+V或者享受着IntelliSense带给你的N倍快乐,这个时候再谈反射就没什么意义了,因此,反射机制访问元数据都是在编译后运行时发生的。

明明都是面向对象,为什么偏偏C++不支持这个东西呢

以C++为例,这些元数据是否可见?答案是肯定的,那为什么不支持反射机制呢,因为这些元数据是以指针的方式给出的,指针在已编译的C++程序中的存在形式就是地址,说的再粗暴点,就是4或8字节的二进制数……
也就是说,在已经编译完成的C++程序的眼里,类的元数据已经变成二进制的地址码了,如果某人在没有源代码的情况下想给这个项目写一个反射机制,那么他将不得不面对一大堆的:

0xb08dfe231a1c002e
0xb08dfe231bc128f6
0xb08dfe2417a90f5d
......

看到这些,他长舒了一口气,优雅地点燃了一根香烟,然后毫不犹豫地戳到电脑屏幕上:

如果原项目加个壳、模板元编一下再做个混淆加密的话那更没法看了,因此如果一定要实现反射机制,一般都是把反射机制直接囊括到项目开发过程当中(就像上面那位大佬的文章中提到的那样,原项目的作者也是反射机制的构造者)。
这样的话就会存在一个上上上个世纪汽车行业出现的问题:

当然,这样说可能有些绝对,但以C++的方式实现一个能够广泛用于所有项目的反射机制应该是极端困难的。
上面大佬的文章当中,这个C++的项目要使用反射机制,是借助工厂模式实现的,关于这些的实现方法,详见大佬文章(当然我自己也没完全看懂)

那托管语言又如何呢

C#、Java,这两种语言都是托管代码的(C#使用.NET进行托管,Java则交给了JVM虚拟机)。

与C++不同的是,他们并不直接接触系统底层,而是通过中间代码访问底层的。

中间代码由谁处理呢,C#是通过.NET提供的CLR,产生的中间语言是程序集,而Java靠的是JVM,其中间产物是class文件。
如果有幸使用一些IDE打开这两个文件往里窥探一遭的话,我们应当不难从中找到这些元数据的信息。

这就好像,一群孩子进了幼儿园,一个托管老师全程进行看护。

当然,托管老师肯定是知道孩子叫什么名的,访问他们自然也是很容易的。同理,托管环境(或虚拟环境)也是一样的,因为衔接上下两层,因此把底层的元数据和上层的可读文本构造反射的桥梁是很容易办到的,因此,C#和Java都提供了一套非常完善的反射库,他们可以被用于使用这两种语言写的任意一个类当中。

好了,道理我都懂,但为什么要反射呢?

反射能干什么呢

举个最简单的例子

好了,换作是你,你会怎么实现这样一个函数呢??

而反射机制恰恰做到了!
你提供给反射机制一个字符串形式的函数名,反射机制不仅可以得知这个函数是否存在,甚至能帮助你去执行这个函数(Invoke)。

什么,你不好问它有没有某个函数??好啊,反射机制甚至可以告诉你这个类都有哪些属性哪些函数,继承自谁,可见性如何,是否抽象等等。

那反射在什么时候比较好用呢

上面那个例子其实就是一个经典的用途。

或者,我们可以考虑另外一个场景。

这个时候首先可以通过反射机制确定方法是否存在,但即便方法已经存在,我们是无法直接调用的,因为对象已经抽象为Object,而Object并不存在方法Grow,所以直接调用就洗洗睡了。

我们不能具象回来么??

如果我们知道类在抽象之前是什么类型的时候,那当然可以具象化回来。
但是抽象虽然发生于编译时或运行时(动态创建的对象),但具象类型的获知却是在编译之前的代码源文件,而且还有些时候你根本无法知道原类型,那也没办法拆箱。

那我还怎么调用Grow

反射机制可以获取到完整的可用方法的列表,我们在列表中找到了Grow,存在形式为Method/MethodInfo对象或干脆就是个字符串。

但无论是哪种,obj.Grow();是不可能了,好在反射机制连这件事都考虑在内了——Invoke调用!!

反射机制不仅知道你想要什么方法,还可以帮助你调用这个方法,这个调用就通过一个叫做Invoke的方法完成。

不同语言对Invoke的定义不尽相同但功能上大同小异,通过Invoke调用某方法的过程实质上是转调回调(或者是间接调用)。
间接调用比直接调用更加的强大灵活,但绕了远路。

还有什么比较宏大一点的应用么??

宏大一点……好吧,其实每一个磅礴的工程都是从一点一滴做起的。

一个很经典的案例,就是上文那篇大佬文章里的一个常用功能——序列化(Serialization)
虽然C#和Java本身就有可以用于序列化的一些结构和功能库(Serializable接口之类的),但是有些时候我们对序列化机制如果有更高的可定制性要求的时候,我们往往倾向于自己构建一套心仪的序列化功能库。

于是乎就有一个最简单的问题摆在面前:

现在有了反射机制,问题就很容易解决了。三胞胎嫌分起来麻烦??反射机制可以把他们安排的明明白白!!你可以向反射类提供一个完整的类名,反射机制就能保证给你这个类对应的可用属性的列表,以及一整套处理方案(Get和Set),之后还不是想来啥来啥,美滋滋~~

当然,以上都是反射机制用途中小的不能再小的冰山一角,比如我还可以通过反射机制根据我的输入创建我想要的类型的对象等等。

哇,反射这么强大??我要满地反射!!

冷静点!任何事物都有多面性,反射也不例外,我们看看反射机制有什么特点,它到底是否适合所有情形。

极致灵动(Flexi Frenzy)

反射机制可以让你的代码非常灵活,以不变应万变。
这也正是反射机制带来的最大的好处。

未卜先知(Fortune Tell)

反射机制是在运行时起作用,当然,运行期间发生什么,编译之前是无法获知的,反射就是处理这件事的。

效率捉急(Emaciated Efficiency)

反射机制最大的问题!

反射机制的效率是十分低下的,首先在运行时获取元数据再转化成可读形式就不是一个很快的过程,而反射的Invoke调用是个不折不扣的间接调用。

不当地使用大量反射会导致程序效率的急剧下降。

代码膨胀(Code Expansion)

显然,用反射进行调用的代码往往比直接调用写起来复杂,所以除非你写代码是按行数计工资,否则能直接调用就不要反射。

健壮风险(Robustness Risk)

反射机制一般允许用户传入字符串……

这时候用户传的字符串就可以非常的五花八门了,就好像一个动物园里,反射机制是一个可爱的小动物,而游客开始不分青红皂白地对它投各种食,良莠不齐,可是你的反射机制很脆弱,它可禁不起这折腾,吃到不好的东西就会生病罢工(抛异常,然后中止),因此你这当奶妈奶爸就要多操心,帮它收拾(捕获),告诉他如何分辨食物(预先判断)……

不过呢,有些时候引入反射机制恰恰就是出于健壮性的考虑……

总结

反射是个强大的武器,但使用应多加谨慎!

以上

06-26 05:03