---
title: "反射"
date: "2026-04"
updated: "2026-09"
category: "设计模式与架构"
tags: ["设计模式与架构"]
description: "反射在有些地方看上去有点像是委托，但两者的差异还是蛮大的。委托的关键是编译器安全，而反射则是牺牲了编译时的安全检查，而带来了运行时的灵活性：它通过字符串来调用或取到指定的对象..."
source: "https://enriquemark.com/zh-hans/posts/reflection/"
---

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

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

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

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

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

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