本文目录
反射机制(Reflection)
何为反射
反射是光在两种物质分界面上改变传播方向又返回原来物质中的现象
反射是生物体对外界刺激做出应激行为的过程,根据产生的原因分为条件反射和非条件反射等,典型的实验案例包括巴甫洛夫的狗……
反射是一些面向对象程序设计语言提供的针对类对象元数据(Metadata)的一种访问机制。
元……数据??什么高深莫测的武功??
啊,诚然,一旦涉及到“元XXX”事情通常就开始变得无比抽象,以至于我不禁念叨起那句诀
太极生两仪,两仪生四象,四象生八卦……
不过元数据这个概念在数据库里还是比较常见的,比如,某个关系型数据库里有张表:
水果
编号 名字 数量 1 苹果 6 2 香蕉 3 3 梨 5 4 橘子 3 5 菠萝 2
数据,就是存在表里的一条一条的记录,(1,苹果,6),(3,梨,5)都是数据,那么,元数据就是凌驾于这些数据之上的用于描述数据的数据,对于这张表而言,也就是这张表的表头(关系数据理论里称之为关系模式):(编号,名称,数据)
。
划重点
元数据(Metadata):用于描述数据的数据
好像有些明朗了,但那关面向对象什么事呢
众所周知,类(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的一个对象,这个对象具有具体的数据。
而上面的类定义代码,包含的就是这个类的元数据。
说的再直白点吧
以人为例,数据注重的是这人的脸长啥样,而元数据注重的是这人有没有脸(好像不太对……)
好吧差不多了解了,但元数据和反射有什么关系呢
反射是一些面向对象程序设计语言提供的针对类对象元数据(Metadata)的一种访问机制。
本文一开始就说了,罚站20年!
不过在此之前先解释一件事,元数据在哪。
任何一个面向对象的程序设计语言,其类类型都具备一个元数据的存储,至少程序会使用这个元数据能够动态地构造此类的对象。但不同的语言机制不同,比如C++这种的,因为直接和系统进行愉♂快的互♂动,因此元数据就直接使用系统的内存地址了,这种数据使用是很不直观的,同时也不使用任何托管机制做后援(巨硬魔改的C++/CLI不在讨论范围内),因此这种贴近底层的语言不支持反射机制,虽然可以通过强行向程序代码中通过工厂类模式强行注入可读的元信息(方法参见这位大佬的文章)。
但是,正如前面所说的,如果元数据在托管编译或解释的状态下会保留一份可读的版本,这是提供给解释器或者托管平台用的,当然,这种情况下语言一般会提供一个较为完善的元数据访问机制,这就是反射。这类语言典型的代表就是C#(.NET托管)、Java(JVM虚拟机)、Python(解释器提供)等。
那……反射是如何运作的呢??
反射嘛。那还不容易,拿个镜子就可以了呀!
或者用羊角锤以偷袭的方式砸膝盖什么的也是很容易的呀!
不过这么说来,拿羊角锤偷袭镜子岂不是更棒!!
正如之前所说,反射机制是对类的元数据的获取和操纵,因此,一个重要的前提就是:
这个程序设计语言的运作机制当中,类的元数据必须是可见的,如果可读的话那更好
只有当类的元数据是可见的,反射机制才有访问它们的可能,但是元数据的可读性会决定反射机制访问它们的难易程度。
这里补充一句,有人会说,在使用IDE或者代码编辑器的时候,我们写object.property
这种访问方式的时候编译器不就直接告诉我们了么??
关于这一点,这里暂时只说一个前提:
反射机制的实际动作是聚焦于运行时(Runtime)的。
在程序代码编译之前我们恣意地书写这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。
这就好像,一群孩子进了幼儿园,一个托管老师全程进行看护。
把拔码麻区上办,我区悠贰园呐
当然,托管老师肯定是知道孩子叫什么名的,访问他们自然也是很容易的。同理,托管环境(或虚拟环境)也是一样的,因为衔接上下两层,因此把底层的元数据和上层的可读文本构造反射的桥梁是很容易办到的,因此,C#和Java都提供了一套非常完善的反射库,他们可以被用于使用这两种语言写的任意一个类当中。
好了,道理我都懂,但为什么要反射呢?
反射能干什么呢
举个最简单的例子
我……我有一个梦想,我想要这样一个函数,能够返回Person类是否有我所说的方法,但是我不知道Person类里有什么,比如我想问他有没有Eat()
方法,它返回true
,我问他有没有Fly()
方法,它能返回false
好了,换作是你,你会怎么实现这样一个函数呢??
而反射机制恰恰做到了!
你提供给反射机制一个字符串形式的函数名,反射机制不仅可以得知这个函数是否存在,甚至能帮助你去执行这个函数(Invoke
)。
什么,你不好问它有没有某个函数??好啊,反射机制甚至可以告诉你这个类都有哪些属性哪些函数,继承自谁,可见性如何,是否抽象等等。
那反射在什么时候比较好用呢
上面那个例子其实就是一个经典的用途。
或者,我们可以考虑另外一个场景。
你写了某个函数接受了一个抽象为
Object
的对象,你希望,如果Object的对象存在方法Grow
则调用之,否则什么也不做。
这个时候首先可以通过反射机制确定方法是否存在,但即便方法已经存在,我们是无法直接调用的,因为对象已经抽象为Object
,而Object
并不存在方法Grow
,所以直接调用就洗洗睡了。
我们不能具象回来么??
如果我们知道类在抽象之前是什么类型的时候,那当然可以具象化回来。
但是抽象虽然发生于编译时或运行时(动态创建的对象),但具象类型的获知却是在编译之前的代码源文件,而且还有些时候你根本无法知道原类型,那也没办法拆箱。
注
这里面我为了方便,也是想不出啥更好的词
这里我称派生类向基类的多态转化为抽象
反过来的过程称为具象
那我还怎么调用Grow
呢
反射机制可以获取到完整的可用方法的列表,我们在列表中找到了Grow
,存在形式为Method/MethodInfo
对象或干脆就是个字符串。
但无论是哪种,obj.Grow();
是不可能了,好在反射机制连这件事都考虑在内了——Invoke
调用!!
反射机制不仅知道你想要什么方法,还可以帮助你调用这个方法,这个调用就通过一个叫做Invoke
的方法完成。
不同语言对Invoke
的定义不尽相同但功能上大同小异,通过Invoke
调用某方法的过程实质上是转调和回调(或者是间接调用)。
间接调用比直接调用更加的强大灵活,但绕了远路。
当然,以上都是反射机制用途中小的不能再小的冰山一角,比如我还可以通过反射机制根据我的输入创建我想要的类型的对象等等。
哇,反射这么强大??我要满地反射!!
冷静点!任何事物都有多面性,反射也不例外,我们看看反射机制有什么特点,它到底是否适合所有情形。
极致灵动(Flexi Frenzy) 稀有属性
反射机制可以让你的代码非常灵活,以不变应万变。
这也正是反射机制带来的最大的好处。
未卜先知(Fortune Tell) 普通属性
反射机制是在运行时起作用,当然,运行期间发生什么,编译之前是无法获知的,反射就是处理这件事的。
效率捉急(Emaciated Efficiency) 糟糕属性
反射机制最大的问题!
反射机制的效率是十分低下的,首先在运行时获取元数据再转化成可读形式就不是一个很快的过程,而反射的Invoke调用是个不折不扣的间接调用。
不当地使用大量反射会导致程序效率的急剧下降。
代码膨胀(Code Expansion)
显然,用反射进行调用的代码往往比直接调用写起来复杂,所以除非你写代码是按行数计工资,否则能直接调用就不要反射。
健壮风险(Robustness Risk)
反射机制一般允许用户传入字符串……
然后就是万劫不复深渊之伊始
这时候用户传的字符串就可以非常的五花八门了,就好像一个动物园里,反射机制是一个可爱的小动物,而游客开始不分青红皂白地对它投各种食,良莠不齐,可是你的反射机制很脆弱,它可禁不起这折腾,吃到不好的东西就会生病罢工(抛异常,然后中止),因此你这当奶妈奶爸就要多操心,帮它收拾(捕获),告诉他如何分辨食物(预先判断)……
不过呢,有些时候引入反射机制恰恰就是出于健壮性的考虑……
如果我养的不是个反射机制而是一只熊猫的话我会上天的!!
总结
反射是个强大的武器,但使用应多加谨慎!
以上