1. 介绍
反射(来自英文 Reflection)是程序在运行时检查自身结构的能力。想象你的程序可以问自己:'我有哪些方法?这个类型有什么属性?'并在运行时得到答案。
现实中反射的应用示例
- 创建插件和模块:你不需要事先知道要操作哪个类——只要加载 DLL,在运行时查看它的方法即可。
- 测试自动化:测试框架,比如 xUnit 和 NUnit,都是通过反射找到测试方法的。
- 序列化:用反射可以把任意对象自动转换成 JSON/XML,而不需要手动遍历属性。
- 基于属性的数据验证:比如你给某个属性加了 [Required],可以自动检测所有这样的属性。
面试里为啥要懂这些
反射相关的能力在面试中常被问到,因为它展示了你对 .NET 内部机制的理解和处理动态问题的能力。如果你想写自己的框架或扩展,反射是必备技能。
2. 命名空间和反射的基本类
C# 中所有反射功能都在命名空间 System.Reflection 里。别忘了在文件开头:
using System.Reflection;
反射的主要类型
| 类/类型 | 说明 |
|---|---|
|
表示 .NET 中的类型描述(例如 class、interface、enum 等) |
|
类型的属性 |
|
类型的方法 |
|
类型的字段 |
|
类型的构造函数 |
|
表示一个程序集(EXE 或 DLL) |
Type 类的常用方法
| 方法 | 说明 |
|---|---|
|
返回所有 public 属性 |
|
返回所有 public 字段 |
|
返回所有 public 方法 |
|
返回所有 public 构造函数 |
|
返回类型实现的所有接口 |
|
返回应用在类型上的所有属性(attributes) |
通常可以给这些方法传入 BindingFlags 来访问 private 或 static 成员,但我们先只看基本场景。
3. 第一步:获取 Type 对象
所有反射的操作都以 Type 对象为基础。
有几种方式可以拿到这个对象,我们来看看每种:
方式 1:通过对象
string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String
方式 2:通过类型字面量
Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32
方式 3:通过字符串(动态)
Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double
注意!对于来自其他程序集的自定义类,需要带上程序集信息的完整类型名。
4. 探索类型结构:属性、方法、字段和构造函数
现在到了关键时刻:我们有了 Type 对象——可以查任何想知道的东西了。
获取属性列表
Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}
输出:
Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...
获取方法列表
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}
大型类型的方法可能非常多!通常你不需要全部方法,而是只关心“自己的”方法(比如排除从 object 继承来的)。我们稍后会处理这个问题。
获取字段列表
FieldInfo[] fields = type.GetFields();
foreach (var field in fields)
{
Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}
但大多数常规类没有 public 字段,因为封装很重要 :)
获取构造函数
ConstructorInfo[] constructors = type.GetConstructors();
foreach (var ctor in constructors)
{
Console.WriteLine($"构造函数: {ctor}");
}
5. 在实践中用反射:分析我们的应用
提醒一下,我们的练习项目里有个类 TaskItem,用来存任务。我们来用反射看看它的结构。
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime DueDate { get; set; }
public bool IsCompleted;
}
下面是运行时查看这个类型内部信息的做法:
Type taskType = typeof(TaskItem);
Console.WriteLine("属性:");
foreach (var prop in taskType.GetProperties())
Console.WriteLine($" {prop.PropertyType.Name} {prop.Name}");
Console.WriteLine("字段:");
foreach (var field in taskType.GetFields())
Console.WriteLine($" {field.FieldType.Name} {field.Name}");
Console.WriteLine("方法:");
foreach (var method in taskType.GetMethods())
Console.WriteLine($" {method.ReturnType.Name} {method.Name}()");
你可以试着在类里添加新属性,观察反射如何发现它们。这就是通用序列化器和对象检查器的工作方式。
6. 可视化:Type 对象的结构(示意)
+-------------------------+
| Type |
+-------------------------+
| .Name |
| .FullName |
| .Namespace |
| .Assembly |
+-------------------------+
| .GetProperties() |
| .GetFields() |
| .GetMethods() |
| .GetConstructors() |
| .GetInterfaces() |
| .GetCustomAttributes() |
+-------------------------+
这就是我们的“类型画像”——它有名字、程序集、一组成员和 attributes。
获取类型的基本信息
Type 对象可以告诉你这是哪种类型:
Type t = typeof(double);
Console.WriteLine(t.Name); // Double
Console.WriteLine(t.FullName); // System.Double
Console.WriteLine(t.Namespace); // System
Console.WriteLine(t.IsClass); // False
Console.WriteLine(t.IsValueType); // True
Console.WriteLine(t.IsEnum); // False
Console.WriteLine(t.IsPrimitive); // True
7. 按名称查找具体的属性、方法或字段
属性
var prop = taskType.GetProperty("Title");
if (prop != null)
{
Console.WriteLine($"属性 'Title' 的类型: {prop.PropertyType}");
}
方法
var method = taskType.GetMethod("ToString");
if (method != null)
{
Console.WriteLine($"方法 'ToString' 返回: {method.ReturnType}");
}
字段
var field = taskType.GetField("IsCompleted");
if (field != null)
{
Console.WriteLine($"字段 'IsCompleted' 的类型: {field.FieldType}");
}
8. 用反射动态访问值
现在不仅查看,还可以去“动”值!
读取属性值
TaskItem item = new TaskItem { Id = 1, Title = "测试反射", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");
if (titleProp != null)
{
object value = titleProp.GetValue(item);
Console.WriteLine($"标题: {value}");
}
设置属性值
titleProp.SetValue(item, "修改后的标题");
Console.WriteLine(item.Title);
操作字段
FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true
访问私有字段和属性需要特殊的 flags,比如 BindingFlags.NonPublic。稍后会详细讲。
9. 通过反射调用方法
即使在编写代码时不知道方法,也可以调用它们!
TaskItem task = new TaskItem { Title = "反射很酷!" };
MethodInfo method = task.GetType().GetMethod("ToString");
if (method != null)
{
object result = method.Invoke(task, null);
Console.WriteLine(result);
}
如果方法有参数,把它们作为对象数组传入:
public class Calculator
{
public int Add(int a, int b) => a + b;
}
var calc = new Calculator();
var addMethod = typeof(Calculator).GetMethod("Add");
object[] parameters = { 5, 3 };
object sumResult = addMethod.Invoke(calc, parameters);
Console.WriteLine(sumResult); // 8
10. 通过构造函数动态创建对象
反射允许创建对象,甚至不用 new 操作符!
Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// 这是同一个 TaskItem(但类型是 object)
Console.WriteLine(newTask.GetType().Name); // TaskItem
也可以传构造函数参数:
public class User
{
public string Name { get; }
public User(string name) { Name = name; }
}
Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "爱丽丝");
Console.WriteLine(((User)user).Name); // 爱丽丝
11. 使用 System.Reflection 时的常见错误
错误 №1:在没有 BindingFlags 的情况下访问私有成员。
默认情况下,反射只能看到 public 成员。没有 BindingFlags.NonPublic 就无法访问私有字段或方法。
错误 №2:忽略对 null 的检查。
方法 GetProperty、GetMethod 等可能返回 null(成员没找到)。不做检查会导致 NullReferenceException。
错误 №3:选择错误的方法重载。
如果存在多个方法重载,GetMethod 可能返回不是你想要的那个。可以使用带参数类型的 GetMethod 来明确选择。
错误 №4:在性能敏感代码中滥用反射。
反射比直接调用慢。对性能有要求的代码要缓存元数据或使用委托(delegates)。
GO TO FULL VERSION