-
反射基础——C#演示动态加载程序集
第一部分:C#基础入门
实例9:反射基础——动态加载程序集
实例介绍
在软件开发中,插件系统是常见的需求:主程序不需要提前知道插件的存在,运行时可以动态添加/移除插件,扩展功能。比如VS Code的插件、Photoshop的滤镜等。这种场景的核心技术就是反射中的动态加载程序集:
反射:允许程序在运行时获取自身或外部程序集的类型信息(类、接口、方法等),并动态创建实例、调用方法;
动态加载程序集:主程序通过反射加载外部.dll文件(插件),无需在编译时引用插件项目,实现“热插拔”。
本实例通过实现一个简单的插件系统,带你掌握反射的核心用法:加载程序集、获取类vb.net教程C#教程python教程SQL教程access 2010教程
型、创建实例、调用方法,理解反射的底层原理和应用场景,学会在实际项目中使用反射实现动态扩展。
需求分析
1.定义插件接口:IPlugin,包含插件的核心方法Execute();
2.创建插件实现:HelloPlugin类(实现IPlugin接口),作为外部插件程序集;
3.主程序功能:
动态加载插件程序集(HelloPlugin.dll);
从程序集中找到实现IPlugin接口的类型;
创建插件实例,调用Execute()方法;
处理反射过程中的异常(如加载失败、找不到类型等);
4.验证场景:主程序无需引用插件项目,运行时动态加载并执行插件功能。
代码实现
步骤1:定义插件接口(Shared项目)
首先创建一个共享类库项目(Shared),定义插件接口:
csharp
// Shared/IPlugin.cs
namespace Shared
{
/// <summary>
/// 插件接口,所有插件必须实现此接口
/// </summary>
public interface IPlugin
{
/// <summary>
/// 插件执行方法
/// </summary>
void Execute();
}
}
步骤2:创建插件实现(HelloPlugin项目)
创建另一个类库项目(HelloPlugin),引用Shared项目,实现IPlugin接口:
csharp
// HelloPlugin/HelloPlugin.cs
using Shared;
using System;
namespace HelloPlugin
{
/// <summary>
/// 插件实现类
/// </summary>
public class HelloPlugin : IPlugin
{
public void Execute()
{
Console.WriteLine("=== HelloPlugin 执行成功 ===");
Console.WriteLine("插件名称:HelloPlugin");
Console.WriteLine("插件功能:输出欢迎信息");
}
}
}
编译此项目,生成HelloPlugin.dll文件(路径记为./Plugins/HelloPlugin.dll)。
步骤3:主程序动态加载插件(MainApp项目)
创建控制台项目(MainApp),引用Shared项目,编写动态加载代码:
csharp
// MainApp/Program.cs
using Shared;
using System;
using System.Reflection;
namespace MainApp
{
class Program
{
static void Main(string[] args)
{
// 插件程序集路径
string pluginPath = "./Plugins/HelloPlugin.dll";
try
{
// 1. 动态加载程序集
Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
Console.WriteLine($"成功加载程序集:{pluginAssembly.FullName}");
// 2. 获取程序集中所有实现IPlugin接口的类型
Type pluginType = null;
foreach (Type type in pluginAssembly.GetTypes())
{
// 判断类型是否实现了IPlugin接口,且不是接口或抽象类
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
pluginType = type;
break;
}
}
if (pluginType == null)
{
Console.WriteLine("未找到实现IPlugin接口的类型");
return;
}
Console.WriteLine($"找到插件类型:{pluginType.FullName}");
// 3. 创建插件实例(反射方式)
object pluginInstance = Activator.CreateInstance(pluginType);
if (pluginInstance == null)
{
Console.WriteLine("创建插件实例失败");
return;
}
// 4. 转换为IPlugin接口,调用Execute方法
IPlugin plugin = pluginInstance as IPlugin;
plugin?.Execute();
// 【拓展】直接调用方法(不通过接口)
// MethodInfo executeMethod = pluginType.GetMethod("Execute");
// executeMethod?.Invoke(pluginInstance, null);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"程序集文件未找到:{ex.Message}");
}
catch (BadImageFormatException ex)
{
Console.WriteLine($"程序集格式错误(非.NET程序集):{ex.Message}");
}
catch (MissingMethodException ex)
{
Console.WriteLine($"找不到无参构造函数:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"反射过程出错:{ex.Message}");
}
}
}
}
逐行讲解
核心反射代码解析
动态加载程序集:
csharp
Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
Assembly.LoadFrom():从指定路径加载程序集文件(.dll或.exe);
返回Assembly对象:代表加载的程序集,包含程序集的所有类型信息;
注意:LoadFrom会加载程序集及其依赖项(如果依赖项在同一目录)。
查找插件类型:
csharp
foreach (Type type in pluginAssembly.GetTypes())
{
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
pluginType = type;
break;
}
}
pluginAssembly.GetTypes():获取程序集中所有的公共类型;
typeof(IPlugin).IsAssignableFrom(type):判断类型是否实现了IPlugin接口;
!type.IsInterface && !type.IsAbstract:排除接口和抽象类,只保留可实例化的具体类。
创建实例:
csharp
object pluginInstance = Activator.CreateInstance(pluginType);
Activator.CreateInstance():通过反射创建类型的实例;
要求类型必须有无参公共构造函数(否则会抛出MissingMethodException);
如果需要带参数的构造函数,可以用CreateInstance(type, param1, param2)。
调用接口方法:
csharp
IPlugin plugin = pluginInstance as IPlugin;
plugin?.Execute();
转换为IPlugin接口:利用接口的多态性,避免直接使用反射调用方法(更安全、性能更高);
plugin?.Execute():空安全调用,防止实例转换失败导致空引用异常。
直接调用方法(拓展):
csharp
MethodInfo executeMethod = pluginType.GetMethod("Execute");
executeMethod?.Invoke(pluginInstance, null);
Type.GetMethod("Execute"):获取类型中名为Execute的无参方法;
MethodInfo.Invoke():调用方法,参数为实例对象和方法参数数组(无参则为null);
适用场景:当没有接口时,直接调用类型的方法(但不推荐,因为耦合度高)。
基础知识拓展
-
程序集加载方式对比
.NET提供了多种加载程序集的方法,核心区别在于加载上下文和依赖处理:
方法 作用 特点
Assembly.Load() 从GAC、应用程序目录或配置文件中加载程序集(按程序集名称) 优先从GAC加载,依赖项自动加载,适合加载系统程序集或已知名称的程序集
Assembly.LoadFrom() 从指定路径加载程序集 加载程序集及其依赖项(依赖项需在同一目录),会缓存程序集
Assembly.LoadFile() 从指定路径加载程序集(不加载依赖项) 不缓存程序集,同一文件多次加载会生成多个Assembly对象,适合加载独立程序集
注意:.NET Core 3.0+推荐使用AssemblyLoadContext(ALC)加载程序集,支持卸载(解决传统反射无法卸载程序集的问题)。 -
反射性能优化
反射的性能比直接调用稍差(因为需要运行时解析类型和方法),但可以通过以下方式优化:
缓存Type和MethodInfo:将常用的Type、MethodInfo、PropertyInfo缓存到字典中,避免重复查找;
使用接口或泛型:通过接口转换后调用方法(如实例中的IPlugin),比直接反射调用快2-5倍;
使用表达式树:动态生成Lambda表达式调用方法,性能接近直接调用;
避免频繁创建实例:如果插件实例可复用,缓存实例而不是每次创建。 -
反射的应用场景
插件系统:如VS Code插件、游戏Mod系统;
序列化/反序列化:如Newtonsoft.Json、System.Text.Json通过反射读取对象属性;
ORM框架:如Entity Framework Core通过反射映射数据库表和实体类;
依赖注入(DI):如Autofac、Microsoft.Extensions.DependencyInjection通过反射创建服务实例;
测试框架:如xUnit、NUnit通过反射发现测试方法。 -
反射的权限问题
反射需要ReflectionPermission权限(.NET Framework中),在部分受限制的环境(如ASP.NET Medium Trust)中可能无法使用。.NET Core后权限模型简化,但仍需注意:
反射可以访问私有成员(通过BindingFlags.NonPublic),但会破坏封装性,需谨慎使用;
对于internal成员,需要在程序集上添加[assembly: InternalsVisibleTo("MainApp")]特性才能通过反射访问。
扩展思考
如何卸载程序集?
传统反射中,Assembly加载后默认无法卸载(因为AppDomain缓存);
.NET Core 3.0+使用AssemblyLoadContext(ALC):创建自定义ALC加载程序集,使用完毕后销毁ALC即可卸载程序集;
示例代码(ALC方式):
csharp
var alc = new AssemblyLoadContext("PluginALC", isCollectible: true);
Assembly pluginAssembly = alc.LoadFromAssemblyPath(pluginPath);
// 使用完毕后卸载
alc.Unload();
如何处理带参数的构造函数?
使用Activator.CreateInstance(type, param1, param2):传入构造函数的参数;
示例:object instance = Activator.CreateInstance(type, "参数1", 123);
如何访问私有成员?
使用BindingFlags指定访问权限:
csharp
// 获取私有方法
MethodInfo privateMethod = type.GetMethod("PrivateMethod", BindingFlags.Instance | BindingFlags.NonPublic);
// 调用私有方法
privateMethod.Invoke(instance, null);
总结
反射是.NET中动态扩展的核心技术,动态加载程序集是反射最典型的应用场景。通过反射,我们可以实现插件系统、热更新等灵活的功能,但也需要注意:
优点:灵活性高,无需提前引用程序集,支持动态扩展;
缺点:性能稍差(可通过缓存优化),类型安全需要手动保证(如接口转换);
最佳实践:优先使用接口+反射的方式(如实例中的IPlugin),既保证灵活性,又兼顾性能和安全。
掌握反射,你就能写出更具扩展性的程序,应对复杂的业务需求(如插件化架构、动态配置等)。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49410.html










