EnriqueMark
设计模式与架构

反射

2026-04 → 2026-09 更新 简体中文

反射在有些地方看上去有点像是委托,但两者的差异还是蛮大的。委托的关键是编译器安全,而反射则是牺牲了编译时的安全检查,而带来了运行时的灵活性:它通过字符串来调用或取到指定的对象,而可以让程序在运行时获取或修改任何代码内容——本质上来说,反射是一种「元操作」,用代码来操作代码,也是字面意义上的「反射」。

就拿我之前写的排程场景来说:假设我的排程是子类继承,下级只关心具体的job,但如果我的场景是判断有多少个job继承了基类——目前的做法是用反射去扫。在我刚了解到委托机制后,我的设想是,能不能在基类里面采用生命周期管理的模式,给子类包在一个run里,排程实际运行的是run,然后基类定义一个事件,这个run就是——到这一步时,我就突然发现了——事实上在基类里预定义的委托属性,在每次被继承并实例化时,这个属性都会重置,那这样的话事件队列就空了。

因此启动时直接扫描程序本身,去确认有多少子类继承了这个基类,才能交给排程框架去统一注册。否则要知道job有多少,就得先获取job,要获取job就得先注册,注册就得知道到底有多少job——最终就是没有任何job会被注册。实例化之前框架就得知道有几个,那我不可能在这之后才让job进事件,所以必须在启动前就知道,那这个只能靠反射去扫有多少类继承了base。这是一个类型发现问题,而不是委托所承载的对象之间的交互(订阅者通过发布者暴露的委托来操作)问题。

顺带一说,反射有一个地方在用法上跟泛型很像,而且更加灵活。比如在编译期,我可以完全不指定类型,反而是通过运行时给什么就接什么的形式来承接类型。但这里有代价,如果我写 where T : IConverter ,那好歹是规定了接口的结构到底啥样,写的有问题,编译期间就会报错。而反射的话,我只能进行类型转换——那就是我和你约定,这里需要是这个类型,但你到底是不是真用这个类型,我是没法硬性限制的(下面的接口来自 控制反转):

Type type = Type.GetType("MyApp.XmlConverter");
IConverter converter = (IConverter)Activator.CreateInstance(type);
// 如果 XmlConverter 根本沒實現 IConverter,這裡直接 InvalidCastException

这里万一给进来的就是不符合约定,那只有运行的时候才知道错误。但如果我用的是泛型,那编译期间就会直接报错提示,根本就不会到最后阶段。因此反射其实多少有了点动态语言的味道——就跟python一样,不到运行的时候你是不知道会不会报错的。这也是灵活性和稳健性的权衡,在有些场景下确实需要灵活性,但必须明确的知道其代价和边界。如果可以的话,提前在这种灵活性上做好错误封装:就算结果不可预料,也要控制损失范围,使其尽可能最小化。