本节摘要:程序集里的元数据不仅给编译器和 CLR 用,运行时也能读——这就是反射。特性则是嵌进元数据的自定义标注,让框架代码能"读懂"你的意图。本节讲 Type 对象的导航、动态实例化与调用的机制和代价、自定义特性的读取,以及 Source Generator 这个新时代替代方案。
第一章说过,程序集 = CIL + 元数据。元数据是一张张表:类型定义表、字段表、方法表、特性表……每个类型的名字、成员签名、基类与接口、标注的特性全在其中有据可查。反射 API 就是这些表的查询接口:
Type t = typeof(Order); Console.WriteLine(t.Name); // Order foreach (var p in t.GetProperties()) Console.WriteLine($"属性 {p.PropertyType.Name} {p.Name}"); foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Instance)) Console.WriteLine($"方法 {m.Name} 参数 {m.GetParameters().Length} 个");
typeof 与 GetType(1.4 节的分工)拿到的 Type 对象,就是运行时里那个类型级数据的句柄——方法表、字段布局、特性都从它出发导航。第一章讲对象布局时说方法表指针是对象的自证身份,反射读取的正是这张表背后的完整描述。

反射不止能读,还能"凭名字办事":
object obj = Activator.CreateInstance(typeof(Order)); // new object result = typeof(Order).GetMethod("Reset") .Invoke(obj, null); // 调用
插件系统靠它加载外部程序集里的类型(Assembly.LoadFrom 后 GetType 后 CreateInstance),配置系统靠它把配置节绑定到对象属性,dynamic(1.4 节)的底层也是反射缓存。但价格表要记牢:Invoke 每次都要做参数匹配与安全检查,比直接调用慢一到两个数量级;类型加载本身也要解析元数据。框架在初始化时用反射"搭好接线",运行期热路径换成委托缓存(MethodInfo.CreateDelegate 之后是正常委托调用速度)——这正是依赖注入容器启动慢、运行快的原因。
⚠️ 常见坑:原生 AOT 剪裁发布(第一章时间线里的新技术)会把"没有静态引用的类型"当死代码裁掉,重度依赖反射按名加载的框架会运行时找不到类型。需要在发布配置里显式保留,或改用下一小节的源生成器方案。
特性是把自定义信息编译进元数据的语法,本身是一个继承 Attribute 的类:
[AttributeUsage(AttributeTargets.Property)] public class ColumnAttribute : Attribute { public string Name { get; } public ColumnAttribute(string name) => Name = name; } public class Customer { [Column("cust_id")] public int Id { get; set; } }
读取时用泛型方法按成员查询:
var attr = typeof(Customer).GetProperty("Id") .GetCustomAttribute<ColumnAttribute>(); Console.WriteLine(attr.Name); // cust_id
写与读两端拼起来,就完成了"声明式编程"的闭环:业务开发者只贴标签,框架在启动时反射扫描所有标签建立映射——cust_id 对 Id、路由对控制器、测试方法对 Test 标注。你每天用的 [ApiController]、[HttpGet]、[Fact],机制上全是这一套。设计自定义特性时记得加 AttributeUsage 限定它能贴在哪些目标上,避免被误用。
特性构造函数的参数必须是编译期常量(它要在编译时写进元数据表)——传 DateTime.Now 会编译报错,这与 1.4 节 const 的限制同根:元数据是静态的。
C# 编译器(第一章介绍的 Roslyn)开放了编译期插件接口:源生成器在编译时分析你的代码,直接生成附加的 C# 源码参与编译。System.Text.Json 的上下文生成、正则表达式源生成,都在把"运行时反射扫描"改造成"编译期生成代码"——启动更快、无反射代价、剪裁安全。趋势很清晰:反射从运行期基础设施,逐渐退位给"开发期工具 + 编译期生成"的组合,但理解反射依然是理解这一切的钥匙,因为生成器输出的代码本身就在模拟反射要做的事。
以一个自检练习收束本节:打开你手头任何用了依赖注入的项目,试着回答三个问题——容器启动时如何发现你的服务注册(扫描了什么特性或调了什么反射方法)、每次解析服务时是反射调用还是缓存的委托、哪些类型若被剪裁发布会失效。答得上来,说明本节的机制已经落地成你的工具;答不上来,顺着这三个问题去读框架源码,是巩固本节内容最快的路径。反射与特性是框架与业务代码之间的翻译层,看懂翻译层,框架就不再黑盒。
下一章进入运行时深处:async 状态机、任务调度与 GC 分代——前面所有铺垫将在那里汇合。