1. 介绍
在 C# 中,任何类(或结构体 struct)都是一组成员。包括:
- 字段 (fields):存数据的变量。
- 属性 (properties):“聪明”的封装,背后有 get/set。
- 方法 (methods):功能逻辑。
- 构造函数 (constructors):创建实例的方式。
- 事件 (events):一种聪明的发布-订阅机制。
想象你有一个魔法放大镜,可以查看任何类型的内部,了解它的结构,甚至修改它。这就是反射。
命名空间和反射的主要类型
开始前别忘了引用命名空间:
using System.Reflection;
关键类和接口:
| 类/接口 | 说明 |
|---|---|
|
主要入口 —— 一切从它开始 |
|
字段信息 |
|
属性信息 |
|
方法信息 |
|
构造函数信息 |
|
事件信息 |
|
上述所有的通用“父类” |
2. 通过反射获取类型成员
获取字段和属性
示例:列举字段和属性
假设我们有这样的类:
public class Person
{
public string Name { get; set; }
public int Age;
private string Secret = "秘密词";
}
获取字段:
Type personType = typeof(Person);
FieldInfo[] fields = personType.GetFields(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
Console.WriteLine($"字段: {field.Name}, 类型: {field.FieldType.Name}, 访问: {field.Attributes}");
}
获取属性:
PropertyInfo[] properties = personType.GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var property in properties)
{
Console.WriteLine($"属性: {property.Name}, 类型: {property.PropertyType.Name}");
}
说明
- BindingFlags 是一组“标志”,告诉你要查找哪些成员(public/private、instance/static 等)。
- 字段和属性不一样:字段是数据片段,属性是背后的 get/set 方法,用来封装字段。
获取方法
MethodInfo[] methods = personType.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in methods)
{
Console.WriteLine($"方法: {method.Name}");
}
你会惊讶会看到很多方法。里面会有属性的 getter/setter,还有来自 object 的一些辅助方法!
实用过滤:如果只想拿你自己的“显式”方法,可以按名字或按 attribute 过滤。
用哪些 BindingFlags 去找什么
| 查找 | 调用示例 | 说明 |
|---|---|---|
| 公共字段 | |
对外可见的所有成员 |
| 私有字段 | |
隐藏的内部成员 |
| 只有实例成员 | |
仅非静态的 |
| 只有静态成员 | |
仅静态的 |
| 全部 | |
完整列表 |
3. 动态创建类型实例
为啥这很酷?
可以在编译时不知道确切类型的情况下创建对象。
简单方法:用 Activator.CreateInstance
// 以某种方式获取 Type,例如:
Type myType = typeof(Person);
// 创建对象:
object obj = Activator.CreateInstance(myType);
// 现在 obj 是 Person 的实例,但类型是 object
如果类没有无参构造函数,可以传参数:
public class Animal
{
public string Name { get; }
public Animal(string name) { Name = name; }
}
// ...
Type animalType = typeof(Animal);
object dog = Activator.CreateInstance(animalType, "沙里克");
重要!
Console.WriteLine(dog.GetType().Name); // Animal
更定制的方法:通过 ConstructorInfo
ConstructorInfo ctor = animalType.GetConstructor(new Type[] { typeof(string) });
object dog = ctor.Invoke(new object[] { "沙里克" });
在实际开发中通常用 Activator,但有时候需要精确参数或操作私有构造函数 —— 这时 ConstructorInfo 就派上用场了。
4. 通过名字访问字段和属性
获取并修改字段
Person p = new Person();
Type t = p.GetType();
FieldInfo field = t.GetField("Age");
field.SetValue(p, 42);
Console.WriteLine(field.GetValue(p)); // 42
操作私有字段:
FieldInfo secret = t.GetField("Secret", BindingFlags.NonPublic | BindingFlags.Instance);
if (secret != null)
{
secret.SetValue(p, "哎呀,秘密被揭露了");
Console.WriteLine(secret.GetValue(p));
}
else
{
Console.WriteLine("没找到字段 Secret");
}
注意!不要滥用对私有字段的访问——这是破坏封装,可能引入 bug。
获取并修改属性
PropertyInfo pi = t.GetProperty("Name");
pi.SetValue(p, "瓦西娅");
Console.WriteLine(pi.GetValue(p)); // 瓦西娅
属性不是字段,而是背后的 get/set 方法。
为什么有时不起作用?
如果你的字段或属性是私有的,记得加上合适的 BindingFlags(例如,NonPublic)。如果属性没有 setter,你就无法通过反射写入——这个规则依然生效。
5. 按名字调用方法
调用无参数的简单方法
public class Greeter
{
public void SayHello()
{
Console.WriteLine("你好!");
}
}
Greeter g = new Greeter();
Type t = g.GetType();
MethodInfo mi = t.GetMethod("SayHello");
mi.Invoke(g, null); // 你好!
调用带参数的方法
public class MathOp
{
public int Add(int x, int y) => x + y;
}
MathOp calc = new MathOp();
Type t = calc.GetType();
MethodInfo mi = t.GetMethod("Add");
object result = mi.Invoke(calc, new object[] { 7, 5 });
Console.WriteLine(result); // 12
如果方法是静态的,在 Invoke 的第一个参数传入 null。
处理重载方法
MethodInfo mi = t.GetMethod(
"Add",
new Type[] { typeof(int), typeof(int) }
);
或者遍历并手动过滤:
foreach(var method in t.GetMethods()) {
if (method.Name == "Add" && method.GetParameters().Length == 2)
{
// ... 我们的方法!
}
}
6. 有用的细节
用于分析类型的主要成员、方法和属性
| 类成员 | 获取信息的方法 | 返回类 | 如何读取/修改值 |
|---|---|---|---|
| 字段 (field) | |
|
|
| 属性 (prop) | |
|
|
| 方法 (method) | |
|
|
| 构造函数 | |
|
|
这些在真实世界中有什么用?
- 在 ORM 框架(例如,Entity Framework 根据你的模型构建 SQL 查询)。
- 在序列化和解析器系统中(例如,JSON 序列化器)。
- 用于通用库:日志、测试、UI 生成器。
- 面试题常问:你会如何写一个通用的对象复制器?
- 写插件时也常用,当你的程序加载并调用别人写的代码。
即使现在觉得这像黑客电影里的魔法,信我——会有用的!
7. 常见错误和注意点
开发者经常把 BindingFlags 搞混 —— 看不到私有字段,或者反而得到太多方法。始终检查标志的组合:对私有字段使用 NonPublic,对静态使用 Static,等等。
通过 Invoke 调用方法时,按声明的顺序和类型传入参数。错误的顺序/类型会导致 TargetParameterCountException 或 ArgumentException。返回值会以 object 的形式返回——别忘了转换类型(或者用 pattern matching)。
别忘了性能:反射比直接调用慢。在热点代码里缓存找到的 Type/MethodInfo/PropertyInfo,或者提前生成 delegate。
你经常会看到像 get_PropertyName 和 set_PropertyName 这样的辅助方法。这是属性的访问方法——不需要自己去创建它们。
如果你修改私有字段或属性——你是在破坏封装。误用可能导致难排查的错误。正如一句话所说:“能力越大,责任越大” —— 小心使用,尤其是在库项目里。
GO TO FULL VERSION