CSharp反射学习笔记
一、反射是什么(大白话)
先记住一句话:
反射,就是让程序在运行的时候,能够"看见自己、检查自己、修改自己"。
平时我们写代码,都是"编译前就知道类型长什么样"。比如写 ItemInfo item = new ItemInfo();,编译器早就知道 ItemInfo 有哪些属性、方法。
但有些场景,我们编译的时候根本不知道会用到什么类型,只能到程序跑起来才知道。这时候就需要反射:在运行时去读取一个类型的"说明书"(元数据),知道它有哪些属性、方法,甚至动态创建对象、调用方法。
一个生活化比喻
把"类型"想成一个人:
- 直接写代码调用,相当于你认识这个人,直接喊他名字让他干活;
- 反射,相当于你不认识这个人,但你拿到了他的身份证和档案,通过档案知道他的姓名、能力、会做什么,然后按档案去指挥他干活。
什么是元数据
程序集里的每一个类型(类、结构、委托、接口、枚举等)都带有一份"档案",记录着它的成员信息(构造函数、方法、字段、属性、事件等),这份档案就叫元数据(metadata)。
通过反射,就能在运行时读取这份元数据,于是"对每一个类型都了如指掌"。
二、反射用到的三个核心类
| 类 | 作用 |
|---|---|
System.Type |
类型的"信息类",获取类的构造函数、方法、字段、属性等信息 |
System.Reflection.Assembly |
程序集类,加载程序集,加载后用 Type 获取其中类的信息 |
System.Activator |
快速实例化对象,把 Type 对象实例化为对应类型的对象 |
三个类各司其职,配合起来用:
先用
Assembly加载程序集 → 再用Type获取某个类型的信息 → 最后用Activator创建出对象实例。
三、反射的用途
- 运行时查看类型元数据;
- 检查程序集中的各种类型,并动态实例化它们;
- 运行时动态读写对象属性值、调用方法、动态加载程序集;
- 运行时访问代码中的特性(Attribute)。
四、动态实例化(Activator)
正常情况下创建对象是 new ItemInfo()。但如果编译时拿不到类型(只知道类型的完整名字符串),就要用反射动态创建。
先定义一个测试类,后面都围绕它演示:
public class ItemInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Remark { get; set; }
public string itemType; // 公有字段
public string GetItemInfo()
{
return Id + "," + Name + "," + Remark;
}
}
方式一:通过 Type 对象创建
// 1. 根据"完整名称"拿到 Type 对象
// 完整名称 = 命名空间.类名,例如 "Zhaoxi.AdvancedCourse.Models.ItemInfo"
string fullName = "Zhaoxi.AdvancedCourse.Models.ItemInfo";
Type itemType = Type.GetType(fullName);
// 2. 调用无参构造函数动态创建实例
object itemInfo = Activator.CreateInstance(itemType);
// 3. 转成具体类型使用
ItemInfo item02 = (ItemInfo)itemInfo;
方式二:直接传程序集名 + 完整名称
// 返回值是个"包装对象",需要用 Unwrap() 打开
ObjectHandle objItem2 = Activator.CreateInstance("Zhaoxi.AdvancedCourse", fullName);
ItemInfo item2 = (ItemInfo)objItem2.Unwrap();
五、查看类型元数据
拿到 Type 对象后,就能读取它的属性、字段、方法、构造函数等信息。
5.1 获取类型对象的两种方式
// 方式一:typeof(已知类型)
Type itemType = typeof(ItemInfo);
// 方式二:Type.GetType("完整名称字符串")
Type itemType2 = Type.GetType("Zhaoxi.AdvancedCourse.Models.ItemInfo");
5.2 读写属性(PropertyInfo)
Type itemType = typeof(ItemInfo);
// 获取所有公有属性
PropertyInfo[] pros = itemType.GetProperties();
foreach (var pro in pros)
{
Console.WriteLine(pro.Name); // 输出 Id、Name、Remark
}
// 获取指定名称的属性
PropertyInfo proName = itemType.GetProperty("Name");
// 创建对象并赋值,然后读取属性值
ItemInfo itemInfo = new ItemInfo() { Id = 101, Name = "收入", Remark = "收入名目" };
string itemName = proName.GetValue(itemInfo).ToString(); // 读:得到 "收入"
// 修改属性值
proName.SetValue(itemInfo, "支出"); // 写:Name 改成 "支出"
5.3 读写字段(FieldInfo)
Type itemType = typeof(ItemInfo);
ItemInfo item = new ItemInfo();
// 获取所有公有字段
FieldInfo[] fields = itemType.GetFields();
// 获取指定名称的公有字段
FieldInfo fType = itemType.GetField("itemType");
fType.SetValue(item, "通过工资发放"); // 写
string typeStr = fType.GetValue(item).ToString(); // 读
// 获取私有字段,需要指定 BindingFlags
FieldInfo fPrivate = itemType.GetField("somePrivate",
BindingFlags.NonPublic | BindingFlags.Instance);
要点:默认的 GetFields() / GetProperties() 只返回公有(public)成员。要拿私有成员,需加 BindingFlags.NonPublic 等标志。
5.4 调用方法(MethodInfo)
Type itemType = typeof(ItemInfo);
ItemInfo itemInfo = new ItemInfo() { Id = 1, Name = "收入", Remark = "工资" };
// 获取所有公共方法
MethodInfo[] methods = itemType.GetMethods();
// 获取无参方法 GetItemInfo,并调用
MethodInfo getInfo = itemType.GetMethod("GetItemInfo");
object reStr = getInfo.Invoke(itemInfo, null); // 参数传 null
// 调用带一个参数的方法:先按参数类型找到对应重载
MethodInfo mCal = itemType.GetMethod("CalTotalAmount", new Type[] { typeof(decimal) });
decimal result = (decimal)mCal.Invoke(itemInfo, new object[] { 100m });
// 调用泛型方法:先获取再"补上"具体类型
MethodInfo createObj = typeof(GenericMethodClass).GetMethod("CreateObj");
MethodInfo createObjNew = createObj.MakeGenericMethod(typeof(IncomeInfo));
IncomeInfo income = (IncomeInfo)createObjNew.Invoke(null, null);
5.5 构造函数(ConstructorInfo)
Type incomeType = typeof(IncomeInfo);
// 获取所有公共构造函数
var constructors = incomeType.GetConstructors();
// 获取无参构造函数并创建实例
var cons01 = incomeType.GetConstructor(new Type[] { });
object obj1 = cons01.Invoke(null);
// 带一个 int 参数的构造函数
var cons02 = incomeType.GetConstructor(new Type[] { typeof(int) });
object obj2 = cons02.Invoke(new object[] { 1 });
// 带两个参数(int、decimal)的构造函数
var cons03 = incomeType.GetConstructor(new Type[] { typeof(int), typeof(decimal) });
object obj3 = cons03.Invoke(new object[] { 1, 2000m });
六、动态加载程序集(Assembly)
程序集(Assembly)就是编译出来的 .dll 或 .exe 文件。加载了程序集,就能用反射访问里面的类型。
Assembly ass = null;
// 方式一:不写 dll 后缀(.NET Framework 可行,.NET 8 需要先引用项目)
ass = Assembly.Load("Zhaoxi.Models");
// 方式二:写完整绝对路径
ass = Assembly.LoadFile(@"D:\工作\基础班Vip05\Zhaoxi.AdvancedCourse\bin\Debug\Zhaoxi.Models.dll");
// 方式三:要写 dll 后缀名
ass = Assembly.LoadFrom("Zhaoxi.Models.dll");
三种方式区别:
Assembly.Load("名称"):按程序集名称加载(从已引用的程序集里找);Assembly.LoadFile("完整路径"):按绝对路径加载;Assembly.LoadFrom("路径或文件名"):按路径加载,会自动处理依赖。
七、与泛型结合封装通用类
反射 + 泛型是黄金搭档。泛型负责"通用类型",反射负责"在运行时读取类型信息",两者结合就能写出非常灵活的工具类。
下面这个 BaseAct<T>,能对任意类型 T 做两件通用的事:把对象属性拼成字符串、按 id 查找对象。
public class BaseAct<T>
{
public List<T> list = new List<T>();
public void Add(T t)
{
list.Add(t);
}
// 把对象的所有属性拼成一行字符串,如 "Id:1,Name:收入,Remark:工资"
public string GetInfo(T t)
{
string str = "";
Type type = typeof(T);
var props = type.GetProperties();
foreach (var pro in props)
{
if (str != "") str += ",";
str += pro.Name + ":" + pro.GetValue(t).ToString();
}
str += "\r\n";
return str;
}
// 按指定的 id 字段名查找对象
public T FindModel(int id, string idName)
{
Type type = typeof(T);
PropertyInfo pId = type.GetProperty(idName);
if (pId != null)
{
foreach (T t in list)
{
if ((int)pId.GetValue(t) == id)
return t;
}
}
return default; // 找不到返回默认值(引用类型为 null)
}
}
// 使用
BaseAct<IncomeInfo> incomeBase = new BaseAct<IncomeInfo>();
string reStr = incomeBase.GetInfo(income);
IncomeInfo incomeObj02 = incomeBase.FindModel(1, "Id");
这里的巧妙之处:GetInfo 里不知道 T 具体是什么,但通过 typeof(T).GetProperties() 反射出它的所有属性,就能统一处理,不管 T 是 IncomeInfo 还是别的类型都能用。
八、反射的优缺点
优点:
- 提高程序的灵活性和扩展性;
- 降低耦合性,提高自适应能力;
- 允许程序创建和控制任意类的对象,无需提前硬编码目标类。
缺点:
- 性能问题:反射基本是"解释执行",访问字段、调用方法比直接代码慢很多。所以反射主要用在框架、ORM、依赖注入等对灵活性和扩展性要求高的系统上,普通业务代码不建议滥用;
- 可读性差:反射绕过了源代码,会让程序内部逻辑变模糊,维护起来比直接代码更复杂、更难理解。
九、一个完整可跑的综合示例
using System;
using System.Reflection;
class Program
{
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public string SayHello()
{
return "你好,我是 " + Name;
}
}
static void Main()
{
// 1. 通过反射动态创建对象
Type stuType = typeof(Student);
object stuObj = Activator.CreateInstance(stuType);
// 2. 反射读写属性
PropertyInfo propId = stuType.GetProperty("Id");
PropertyInfo propName = stuType.GetProperty("Name");
propId.SetValue(stuObj, 1);
propName.SetValue(stuObj, "小明");
// 3. 反射调用方法
MethodInfo sayHello = stuType.GetMethod("SayHello");
string msg = (string)sayHello.Invoke(stuObj, null);
// 4. 转成具体类型,用常规方式验证
Student stu = (Student)stuObj;
Console.WriteLine(stu.Id); // 输出 1
Console.WriteLine(stu.Name); // 输出 小明
Console.WriteLine(msg); // 输出 你好,我是 小明
}
}
十、面试题
1. 什么是反射?有什么作用?
答:反射是程序在运行时访问、检测和修改自身状态或行为的能力。通过反射可以读取类型的元数据(属性、方法、字段等),动态创建实例、调用方法、加载程序集、读写属性,以及访问特性。它主要用于框架、ORM、依赖注入、插件系统等需要高灵活性的场景。
2. 反射用到哪些核心类?分别做什么?
答:
System.Type:类型信息类,获取构造函数、方法、字段、属性、事件等信息;System.Reflection.Assembly:程序集类,加载程序集;System.Activator:快速实例化对象的类,把 Type 对象实例化为具体对象。
3. 如何获取一个类型的 Type 对象?
答:常用两种方式:
typeof(类名):编译时已知类型;Type.GetType("命名空间.类名"):根据完整名称字符串获取,适合类型在编译时未知的场景。
此外还有 obj.GetType():从已有对象实例获取其类型。
4. 反射如何动态创建对象?
答:用 Activator.CreateInstance(type) 调用无参构造函数创建;或用 type.GetConstructor(new Type[] { ... }) 拿到指定构造函数后,再 Invoke(参数数组) 创建。CreateInstance 返回 object,需要强转成具体类型使用。
5. GetFields() 和 GetProperties() 默认能拿到私有成员吗?怎么拿私有成员?
答:默认只能拿到公有(public)成员。要拿私有成员,需加 BindingFlags 标志,例如:
FieldInfo f = type.GetField("字段名", BindingFlags.NonPublic | BindingFlags.Instance);
6. 反射调用方法是怎么做的?带参数怎么处理?
答:先用 type.GetMethod("方法名") 拿到 MethodInfo,再调用 Invoke(对象实例, 参数数组)。
- 无参方法:
Invoke(obj, null); - 有参方法:先按参数类型数组定位重载
GetMethod("方法名", new Type[]{ typeof(int) }),再Invoke(obj, new object[]{ 参数 })。
7. 反射调用泛型方法怎么处理?
答:泛型方法需要先"填充"具体类型。先 GetMethod("方法名") 拿到 MethodInfo,再用 MakeGenericMethod(具体类型) 得到确定类型的 MethodInfo,最后 Invoke 调用。
MethodInfo m = typeof(X).GetMethod("CreateObj");
MethodInfo m2 = m.MakeGenericMethod(typeof(IncomeInfo));
IncomeInfo obj = (IncomeInfo)m2.Invoke(null, null);
8. 反射有哪些缺点?
答:
- 性能差:反射是解释执行,访问字段、调用方法远慢于直接代码;
- 可读性差:绕过源代码,逻辑模糊,维护困难、代码更复杂。
因此普通业务代码不建议滥用反射,主要用在框架、ORM 等对灵活性要求高的场景。
9. 反射和泛型结合有什么好处?举一个例子。
答:泛型负责"类型通用化",反射负责"运行时读取类型信息",结合后可写出高度通用的工具类。例如 BaseAct<T>:用 typeof(T).GetProperties() 反射出 T 的所有属性,于是 GetInfo 能对任意类型把属性拼成字符串、FindModel 能按任意 id 字段查找对象,而不用为每种类型单独写一套。
10. 什么是元数据?反射和元数据什么关系?
答:元数据是程序集里记录类型成员信息的数据(构造函数、方法、字段、属性、事件等)。反射就是在运行时读取元数据的机制,从而了解类型的结构并动态操作它。
11. Assembly.Load、LoadFile、LoadFrom 有什么区别?
答:
Assembly.Load("名称"):按程序集名称加载,从已引用的程序集里查找;Assembly.LoadFile("绝对路径"):按完整路径加载;Assembly.LoadFrom("路径或文件名"):按路径加载,会自动处理相关依赖。
12. 反射性能差,怎么优化或规避?
答:
- 能不用反射就不用,普通逻辑直接写代码;
- 缓存反射结果(如
PropertyInfo、MethodInfo、ConstructorInfo),避免重复反射查找; - 对高频调用场景,可用表达式树(Expression)编译成委托,或用
Emit生成动态方法,把反射的"解释执行"变成"直接调用"。

浙公网安备 33010602011771号