最近在项目开发中接触到了反射(Reflection),在看一些底层公共组件和基础框架时经常能遇到它。为了加深理解,我把反射的核心基础用法、实际应用场景以及需要注意的性能隐患做了一次梳理和记录。

一、 什么是反射?

在 C# 中,常规编写代码是在编译期就确定了类型和调用关系;而反射则是提供了一种在运行期动态窥探和操作程序集的能力。

程序运行起来后,其对应的类、方法、属性等元数据都会保存在程序集中。通过反射,我们可以在程序运行的过程中动态地获取这些类型信息,甚至直接实例化对象、调用里面的方法或读写属性。

二、 获取 Type 对象的三种基础用法

在 C# 中,使用反射的第一步永远是拿到目标类的 Type 对象。通常有以下三种最基础的获取方式:

1. 用法一:typeof(类名)

  • 说明:在编译期就已经明确知道要获取哪个类的类型信息。
  • 示例:
    1
    Type t = typeof(User);

2. 用法二:obj.GetType()

  • 说明:手里已经有了具体的对象实例,在运行期通过实例来获取其真实的类型信息。
  • 示例:
    1
    2
    User user = new User();
    Type t = user.GetType();
  • 特点:常用于编写通用工具方法,通过接收 object 对象来动态判断其具体类型。

3. 用法三:Type.GetType("全限定类名")

  • 说明:通过类的完整名称字符串(命名空间+类名)进行动态加载。
  • 示例:
    1
    Type t = Type.GetType("MyProject.Models.User");
  • 特点:灵活性最强。类名是一个纯字符串,可以从配置文件中读取,从而实现代码的解耦。即使代码中没有直接引用这个类,也能在运行时动态加载。

三、 反射的核心应用场景

拿到Type对象后,在实际开发中通常有以下三大核心操作:

1. 动态创建对象

不需要通过 new 关键字,直接在运行时根据 Type 实例化对象:

1
2
3
Type t = Type.GetType("MyProject.Models.User");
// 调用无参构造函数动态创建对象实例
object obj = Activator.CreateInstance(t);

2. 动态读取和修改属性

当需要遍历一个未知实体类的所有字段或属性时(例如做通用的数据导出或属性映射),可以使用反射:

1
2
3
4
5
6
PropertyInfo[] properties = t.GetProperties();
foreach (var prop in properties)
{
// 获取属性名和当前对象中该属性的实际值
Console.WriteLine($"属性名: {prop.Name}, 值: {prop.GetValue(obj)}");
}

3. 动态调用方法

在不知道具体类型的情况下,通过方法名字符串动态执行其方法:

1
2
3
MethodInfo method = t.GetMethod("CalculateSalary");
// 执行方法并传递参数,返回结果为 object
object result = method.Invoke(obj, new object[] { 2026 });

四、 反射的缺点与避坑指南

反射虽然功能强大,但也伴随着明显的代价,在实际使用中需要保持克制:

1. 明显的性能开销

普通的代码调用在编译后是非常直接的 CPU 指令,而通过反射调用时,CLR 必须去检索程序集的元数据表,进行字符串匹配、安全权限检查等一系列操作。

  • 避坑:反射的执行速度比原生代码慢很多。因此,在高频调用的核心业务循环(如每秒几万次的传感器数据轮询)中,绝对不能使用反射。

2. 编译器失去安全检查

如果是普通写法,方法名写错(如少个字母),在编译阶段 Visual Studio 就会直接报错报错提示。 而如果使用反射:t.GetMethod("Claculate")(拼写错误),编译器在编译时完全不会报错。程序能正常跑起来,直到运行到这一行时才会抛出 NullReferenceException 导致系统崩溃,隐蔽性极高。

五、 个人心得总结

  1. 框架的灵活性是有代价的:像我们平时用的许多第三方 ORM 框架、依赖注入(DI)容器或对象映射工具(AutoMapper),底层都重度依赖反射。为了通用性和灵活性,框架层牺牲部分性能是合理的,但在业务层编写代码时,能不用就尽量不用。
  2. 善用缓存优化性能:如果项目中有些地方(如读取自定义 Attribute 特性)非用反射不可,可以考虑利用 Dictionary 将第一次反射拿到的 TypePropertyInfo 缓存起来。后续直接从字典里取,避免重复翻阅元数据,这样能大幅降低反射带来的性能损耗。
  3. 自我思考:以前觉得很多框架的配置化操作很神秘,搞懂反射的底层机制后,再去看那些动态加载的代码,思路就会清晰很多。