3.4 反射与特性:程序阅读自己


3.4 反射与特性:程序阅读自己

本节摘要:程序集里的元数据不仅给编译器和 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} 个");

typeofGetType(1.4 节的分工)拿到的 Type 对象,就是运行时里那个类型级数据的句柄——方法表、字段布局、特性都从它出发导航。第一章讲对象布局时说方法表指针是对象的自证身份,反射读取的正是这张表背后的完整描述。

图 程序集元数据与反射导航路径

图 程序集元数据与反射导航路径

动态实例化与调用:能力与价格

反射不止能读,还能"凭名字办事":

object obj = Activator.CreateInstance(typeof(Order)); // new object result = typeof(Order).GetMethod("Reset") .Invoke(obj, null); // 调用

插件系统靠它加载外部程序集里的类型(Assembly.LoadFromGetTypeCreateInstance),配置系统靠它把配置节绑定到对象属性,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_idId、路由对控制器、测试方法对 Test 标注。你每天用的 [ApiController][HttpGet][Fact],机制上全是这一套。设计自定义特性时记得加 AttributeUsage 限定它能贴在哪些目标上,避免被误用。

特性构造函数的参数必须是编译期常量(它要在编译时写进元数据表)——传 DateTime.Now 会编译报错,这与 1.4 节 const 的限制同根:元数据是静态的

Source Generator:编译期的反射替代

C# 编译器(第一章介绍的 Roslyn)开放了编译期插件接口:源生成器在编译时分析你的代码,直接生成附加的 C# 源码参与编译。System.Text.Json 的上下文生成、正则表达式源生成,都在把"运行时反射扫描"改造成"编译期生成代码"——启动更快、无反射代价、剪裁安全。趋势很清晰:反射从运行期基础设施,逐渐退位给"开发期工具 + 编译期生成"的组合,但理解反射依然是理解这一切的钥匙,因为生成器输出的代码本身就在模拟反射要做的事。

以一个自检练习收束本节:打开你手头任何用了依赖注入的项目,试着回答三个问题——容器启动时如何发现你的服务注册(扫描了什么特性或调了什么反射方法)、每次解析服务时是反射调用还是缓存的委托、哪些类型若被剪裁发布会失效。答得上来,说明本节的机制已经落地成你的工具;答不上来,顺着这三个问题去读框架源码,是巩固本节内容最快的路径。反射与特性是框架与业务代码之间的翻译层,看懂翻译层,框架就不再黑盒。

本节要点回顾

  • 反射是元数据的运行时查询接口,Type 对象是导航起点;
  • Invoke 慢一到两个数量级:初始化期接线、热路径缓存委托,是框架的标准姿势;
  • 特性是编译进元数据的标注,配合反射扫描构成声明式编程闭环;
  • AOT 剪裁与反射天然冲突:按名加载要显式保留,或改走源生成器;
  • 趋势是编译期生成替代运行期反射:机制不变,执行时机前移。

下一章进入运行时深处:async 状态机、任务调度与 GC 分代——前面所有铺垫将在那里汇合。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U