首页 / C# 入门教程 / 反射基础

C# 入门教程

反射基础

本教程共 100 篇 · 第 79 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程反射ReflectionType

79. 反射基础

本节目标:学完你会在程序运行起来之后,动态查看一个类型有哪些属性、方法,而不必在写代码时就写死。

前几章的特性被悄悄写进了元数据。怎么把它们读出来?靠反射(reflection)。反射让你在运行时”照镜子”——探查任意类型长什么样、有哪些成员。序列化器、ORM、依赖注入容器底层都靠它。

先打个比方:博物馆看展品

普通写代码,就像你提前拿到展品清单,按图索骥。反射则像你站在博物馆里,拿起一件展品翻来覆去地看:它叫什么、什么材质、有哪些按钮——这些信息不用别人预先告诉你,你自己看就能知道。在程序里,“展品”就是类型(Type),“翻看”就是反射。

什么是反射

反射是 .NET 提供的一组 API,让你在程序运行期间检查甚至操作类型、对象、程序集。普通写代码是”编译时就知道类型”,反射则是”运行时才知道类型是谁”。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public void Greet() => Console.WriteLine("hi");
}

拿到 Type:typeof 与 GetType

要反射,第一步是拿到 System.Type 对象。两种方式:

  1. typeof(类型名):编译期已知类型时用。
  2. 对象.GetType():手上有实例时用。
namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

Type t1 = typeof(Person);
Person p = new Person();
Type t2 = p.GetType();

Console.WriteLine($"类型名:{t1.Name}");
Console.WriteLine($"命名空间:{t1.Namespace}");
Console.WriteLine($"是否类:{t1.IsClass}");
Note

typeof 要你写死类型名;GetType() 能对一个未知对象”现问现查”。拿到 Type 后,两者接下来能干的事完全一样。

列出属性与方法

Type 身上有一组 GetXxx 方法,能把成员枚举出来。GetProperties() 拿属性,GetMethods() 拿方法(注意会包含从 Object 继承来的那几个)。

namespace CSharpDemo;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public void Greet() { }
}

Type t = typeof(Person);

Console.WriteLine("— 属性 —");
foreach (var prop in t.GetProperties())
    Console.WriteLine($"{prop.Name} : {prop.PropertyType.Name}");

Console.WriteLine("— 方法 —");
foreach (var method in t.GetMethods())
    Console.WriteLine(method.Name);
Tip

PropertyType.Name 给出的是类型简称(如 String、Int32),比全名短,打印日志更清爽。

还能拿字段和构造函数

除了属性、方法,反射同样能取字段、构造函数、事件。比如只看你自己声明的属性,可加 BindingFlags 过滤掉继承来的。

namespace CSharpDemo;

using System.Reflection;

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

Type t = typeof(Person);
var ownProps = t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
foreach (var prop in ownProps)
    Console.WriteLine(prop.Name);

反射为什么这样设计

按理说写代码时类型都确定了,为何还要反射?因为有些场景”写代码时真不知道类型”:

  1. 序列化:把任意对象转成 JSON,框架不可能为每个类都提前写好代码。
  2. 插件:主程序加载外部 dll,里面有哪些类型运行时才揭晓。
  3. 依赖注入:按配置决定 new 哪个实现类。
Note

反射很灵活,但也有代价:比直接调用慢,且绕过了编译期类型检查。日常业务别滥用,把它留给”框架级”的通用逻辑。

BindingFlags:精确控制”看哪些成员”

默认 GetProperties / GetMethods 只回公开实例成员,还会带上从 Object 继承的。要看到私有、静态,或只看你自己声明的,就得用 BindingFlags 组合。

namespace CSharpDemo;

using System.Reflection;

class Person
{
    public string Name { get; set; } = "";
    private int Secret { get; set; }
}

Type t = typeof(Person);
var all = t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var p in all)
    Console.WriteLine(p.Name);   // 包括私有 Secret
Note

BindingFlags 是一组标志位,用 | 组合。想”既看公开又看私有”就要把 Instance 和对应访问级别都写上。漏写 Instance 常常导致”什么都没查到”,这是新手最常见的困惑。

用反射调用方法

拿到 MethodInfo 后,用 Invoke(对象, 参数数组) 在运行时调用方法——连方法名都可以是字符串,实现真正的动态调用。

namespace CSharpDemo;

using System.Reflection;

class Calculator
{
    public int Add(int a, int b) => a + b;
}

object calc = new Calculator();
MethodInfo? m = calc.GetType().GetMethod("Add");
object? result = m?.Invoke(calc, new object[] { 2, 3 });
Console.WriteLine(result);   // 5
Tip

Invoke 的第二个参数是 object[],所有实参都要装箱成 object。参数类型、个数、顺序必须与方法签名严格匹配,否则运行时会抛 TargetParameterCountExceptionArgumentException

反射还能读特性

Type、成员上都挂着特性。用 GetCustomAttribute<T>() 就能在运行时把上一章讲的标签读出来——这正是第 80 章插件机制的基础,这里先埋个伏笔。

反射 vs 接口 vs 泛型:怎么选

方案何时用类型安全性能
接口/多态编译期已知类型关系
泛型算法与类型无关、但参数类型已知
反射编译期根本不知道类型弱(object)

一句话:能用接口/泛型就别用反射。反射是”实在没法在编译期确定类型”时的最后手段(如框架、插件、序列化)。它换来了极致灵活,代价是失去编译期保护和运行速度。

一个容易混的点:反射 vs 动态

有人把反射和 dynamic 搞混。dynamic 是编译期放宽类型检查、运行时照样走反射;它写法更简洁,但失去了编译期安全、性能也更差。简单动态场景可用 dynamic,复杂类型探查仍靠反射。

常见坑与注意事项

Warning

反射会绕过访问修饰符——私有成员也能读能改。这破坏了封装,用在生产代码里既危险又难维护。除非做序列化/测试框架等必要场景,否则别去碰私有成员。

Warning

反射调用没有编译期保护:方法名拼错、参数类型不对,不会在编译时报错,而是要等程序运行到那一行才炸。把”可能失败”的反射调用包在 try/catch 里更稳。

其它易错点:

  1. BindingFlags 漏写 Instance:只想查实例成员却忘了加 BindingFlags.Instance,结果返回空集合,误以为”类型没成员”。
  2. GetMethods 含继承方法:默认会列出从 Object 继承的 ToStringEquals 等,打印时一脸”这些是哪来的”。用 DeclaredOnly 只看自己声明的。
  3. 性能滥用:在循环里反复 GetType().GetMethod(...) 很慢。高频场景应把 MethodInfo 缓存起来复用。

反射典型用在哪

  • JSON / XML 序列化:把任意对象转成文本再还原。
  • ORM(如 EF):把数据库行映射成实体类。
  • 依赖注入容器:按类型自动 new 出实现。
  • 单元测试框架:自动发现并运行带特性的测试方法。

把”探查 + 调用”串起来

下面一段把列属性、读方法、动态调用连起来,体会反射的闭环:

namespace CSharpDemo;

using System.Reflection;

class Robot
{
    public string Name { get; set; } = "R2";
    public void Speak() => Console.WriteLine($"{Name}:哔哔");
}

Type t = typeof(Robot);
Console.WriteLine($"类型:{t.Name}");
object robot = Activator.CreateInstance(t)!;
t.GetMethod("Speak")!.Invoke(robot, null);

读取并修改属性值

光”看”成员还不够,反射还能在运行时读写属性的值:用 GetValue(对象) 取、SetValue(对象, 新值) 写。这正是一个对象能被序列化器”遍历所有属性并导出”的底层原理。

namespace CSharpDemo;

using System.Reflection;

class Person
{
    public string Name { get; set; } = "小明";
    public int Age { get; set; } = 18;
}

object p = new Person();
PropertyInfo? nameProp = p.GetType().GetProperty("Name");
Console.WriteLine($"旧值:{nameProp?.GetValue(p)}");
nameProp?.SetValue(p, "小红");
Console.WriteLine($"新值:{nameProp?.GetValue(p)}");
Note

GetValue / SetValue 操作的是 object,值类型会被装箱。属性不存在时返回 null,所以同样用 ?. 兜底,别裸调用。

反射的性能代价到底多大

前面多次说反射”慢”,具体慢在哪?每次 GetProperty/GetMethod 都要去元数据里查找、做字符串比对;Invoke 要把参数装箱、再在运行时做类型检查。相比编译期直接 obj.Add(2,3) 那条一步到位的调用,反射多了好几道中间工序。

Tip

高频场景的提速套路:把查到的 MethodInfo / PropertyInfo 缓存到静态字典里,下次直接用,省去重复查找。序列化、DI 容器内部都是这么干的。普通业务偶尔用一次,这点开销完全可忽略,不必过度优化。

小结

反射让你在运行时用 typeofGetType 拿到 Type,再用 GetProperties / GetMethods 等列出成员,甚至用 GetValue / SetValue 读写属性值。它是序列化、ORM、插件机制的地基,但性能不如直连调用。下一章我们看反射的进阶:加载程序集、动态创建对象、读取特性。