C# 反射(Reflection)(保姆级教程)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言:揭开反射的神秘面纱

在编程世界中,有一种能力能让程序在运行时“看透自己”——这就是反射(Reflection)。它像一位精通解剖学的医生,能够动态分析程序的结构,访问类型信息、方法、属性,并在运行时动态创建对象或调用方法。对于初学者而言,反射可能显得抽象且难以掌握,但通过循序渐进的讲解和生动的案例,我们将逐步揭开它的奥秘。

反射在 C# 中的应用场景广泛,例如插件系统开发、日志框架、依赖注入(DI)以及单元测试工具。理解反射不仅能提升代码灵活性,还能帮助开发者设计更优雅的架构。本文将从基础概念出发,结合代码示例,带您一步步掌握反射的核心能力。


一、反射的核心概念:程序集、类型与成员

1.1 程序集(Assembly):程序的“身份证”

程序集是 .NET 程序的基本部署单元,包含编译后的代码、元数据及资源文件。在反射中,程序集是访问类型信息的起点。例如,获取当前程序集的名称:

using System.Reflection;  

var currentAssembly = Assembly.GetExecutingAssembly();  
Console.WriteLine(currentAssembly.FullName); // 输出当前程序集的全名  

1.2 类型(Type):对象的“蓝图”

每个 C# 类、接口或结构体在运行时都对应一个 Type 对象,它描述了该类型的元数据,如方法、属性、构造函数等。通过 Type 可以动态获取类型信息:

var type = typeof(string);  
Console.WriteLine($"类型名称:{type.Name}, 是否为引用类型?{type.IsClass}");  

1.3 成员(Member):方法、属性与字段的“索引卡”

反射允许开发者动态获取类型的成员信息:

  • 方法(MethodInfo):描述方法的名称、参数及返回类型。
  • 属性(PropertyInfo):表示公共属性,包含其访问器方法。
  • 字段(FieldInfo):直接访问非公共字段(需权限控制)。

示例:获取类型的所有公共方法

var type = typeof(Math);  
var methods = type.GetMethods();  

foreach (var method in methods)  
{  
    Console.WriteLine($"方法名:{method.Name}, 返回类型:{method.ReturnType.Name}");  
}  

二、反射的四大核心操作:获取、创建、调用与设置

2.1 获取类型信息:像“图书馆目录”一样搜索

反射最基础的操作是获取类型及其成员的信息。例如,检查类型是否实现某个接口:

var type = typeof(List<int>);  
bool isEnumerable = type.GetInterface(nameof(IEnumerable)) != null;  
Console.WriteLine($"是否为集合?{isEnumerable}");  

2.2 动态创建对象:无需“显式 new 关键字”

通过 Activator.CreateInstance 方法,反射可以动态实例化对象:

// 假设存在一个无参构造函数的类 MyClass  
var instance = Activator.CreateInstance(typeof(MyClass));  

2.3 调用方法:像“遥控器”一样触发行为

即使不知道方法名,反射也能在运行时调用方法:

var type = typeof(Math);  
var method = type.GetMethod("Abs");  
var result = method.Invoke(null, new object[] { -10 }); // 调用静态方法  
Console.WriteLine($"绝对值:{result}"); // 输出 10  

2.4 设置属性与字段:绕过封装的“万能钥匙”

反射可以绕过访问修饰符(如 private),直接操作对象成员:

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

// 动态设置私有字段  
var person = new Person();  
var field = typeof(Person).GetField("Age", BindingFlags.NonPublic | BindingFlags.Instance);  
field?.SetValue(person, 25);  

三、反射的高级技巧与常见场景

3.1 插件系统:动态加载外部程序集

反射常用于插件架构,例如加载并实例化外部 DLL 中的类型:

// 加载外部程序集  
var pluginAssembly = Assembly.LoadFrom("Plugin.dll");  

// 查找实现 IPlugin 接口的类型  
var pluginType = pluginAssembly.GetTypes()  
    .FirstOrDefault(t => t.GetInterface("IPlugin") != null);  

if (pluginType != null)  
{  
    var pluginInstance = Activator.CreateInstance(pluginType);  
    var executeMethod = pluginType.GetMethod("Execute");  
    executeMethod.Invoke(pluginInstance, null);  
}  

3.2 自动化配置:基于元数据的属性绑定

通过自定义特性(Attribute)和反射,可实现类似 ORM 框架的配置功能:

[AttributeUsage(AttributeTargets.Property)]  
public class ConfigAttribute : Attribute  
{  
    public string Key { get; set; }  
}  

public class Settings  
{  
    [Config(Key = "max_users")]  
    public int MaxUsers { get; set; }  
}  

// 反射读取属性上的 ConfigAttribute  
var property = typeof(Settings).GetProperty("MaxUsers");  
var attribute = property.GetCustomAttribute<ConfigAttribute>();  
Console.WriteLine($"配置键:{attribute.Key}");  

四、反射的性能与局限性

4.1 性能开销:像“慢动作镜头”一样谨慎使用

反射操作通常比直接调用慢,因为它涉及动态分析元数据。对于高频场景,建议缓存 MethodInfo 或使用 Delegate.CreateDelegate 优化:

// 缓存方法信息以提升性能  
var method = typeof(Math).GetMethod("Abs");  
var del = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), method);  
var result = del(-10); // 快速调用  

4.2 安全性与访问限制:绕过封装的“双刃剑”

反射可以访问 private 成员,这可能破坏封装原则,需谨慎使用。此外,若目标程序集未在当前 AppDomain 加载,反射操作会失败。


五、反射的典型应用场景

5.1 单元测试框架:模拟与验证

工具如 NUnit 或 xUnit 利用反射发现测试方法,并动态执行它们:

[TestFixture]  
public class MathTests  
{  
    [Test]  
    public void TestAbsoluteValue()  
    {  
        Assert.AreEqual(10, Math.Abs(-10));  
    }  
}  

5.2 依赖注入(DI):自动绑定依赖关系

DI 容器(如 Autofac)通过反射解析类型依赖关系:

var builder = new ContainerBuilder();  
builder.RegisterType<MyService>().As<IMyService>();  
var container = builder.Build();  
var service = container.Resolve<IMyService>(); // 动态创建实例  

六、常见问题与最佳实践

6.1 为什么我的反射代码抛出 MissingMethodException

  • 确保方法名称、参数类型与数量完全匹配。
  • 检查方法的访问修饰符(如 private 方法需使用 BindingFlags)。

6.2 如何避免反射导致的代码可读性下降?

  • 仅在必要时使用反射(如插件系统)。
  • 通过接口或抽象类定义契约,减少对具体类型的依赖。

结论:掌握反射,解锁程序的“元能力”

反射是 C# 中一项强大但需谨慎使用的功能。它允许程序在运行时动态分析自身结构,实现灵活的扩展与自动化操作。通过本文的讲解,您已掌握了反射的基本概念、核心操作及典型场景。在实际开发中,建议将反射作为“最后的手段”,优先使用面向接口编程或依赖注入等设计模式。

当您需要设计插件系统、实现配置绑定或构建工具框架时,反射将成为您的“瑞士军刀”。不过,请始终铭记:过度使用反射可能导致代码难以维护,合理权衡性能与灵活性,方能真正驾驭这一技术。

最新发布