C# 动态程序集生成方法 C#如何使用Reflection.Emit在运行时创建类型

来源:这里教程网 时间:2026-02-21 17:40:29 作者:

AssemblyBuilder
ModuleBuilder
构建可执行程序集

直接调用

AssemblyBuilder.DefineDynamicAssembly
是唯一能生成可运行类型的方式,不能跳过这一步。.NET 5+ 中必须使用
AssemblyBuilderAccess.RunAndCollect
(推荐)或
Run
,否则生成的类型无法被 JIT 编译执行。

常见错误是传入

AssemblyBuilderAccess.Save
后尝试实例化类型——此时类型只写入磁盘,未加载进当前上下文,
Activator.CreateInstance
会抛出
FileNotFoundException
InvalidOperationException

必须用
AssemblyLoadContext.Current.LoadFromStream()
手动加载
Save
模式生成的程序集,再反射创建实例
RunAndCollect
支持 GC 回收,但要求所有动态类型不被长期引用,否则影响卸载
模块名(如
"MyModule.dll"
)只是标识符,不决定文件名;仅在
Save
模式下才实际写入磁盘

定义类时用
TypeBuilder
而不是
Class
关键字

TypeBuilder
是构建动态类型的唯一入口,它不继承任何已有类(除非显式调用
SetParent
),默认基类是
object
。字段、属性、方法都必须通过对应 Builder 添加,不能用 C# 语法糖。

典型陷阱:试图用

typeof(MyBaseClass).GetMethods()
获取虚方法再重写——不行。必须用
TypeBuilder.DefineMethod
显式定义,并调用
MethodBuilder.DefineParameter
MethodBuilder.GetILGenerator()
填充逻辑。

字段必须先用
TypeBuilder.DefineField
注册,才能在 IL 中用
OpCodes.Ldfld
访问
自动属性无法自动生成 backing field,需手动定义字段 + 属性 getter/setter 方法 泛型类型参数需通过
TypeBuilder.DefineGenericParameters
声明,且后续所有引用必须用
GenericTypeParameterBuilder

ILGenerator
写 IL 指令而不是 C# 代码

ILGenerator.Emit
系列方法是向方法体注入指令的唯一方式,没有“编译器帮你补全”这回事。比如返回一个整数,不能写
return 42;
,而要:
il.Emit(OpCodes.Ldc_I4, 42); il.Emit(OpCodes.Ret);

最容易出错的是栈平衡:每条指令对求值栈有明确要求。例如

OpCodes.Call
需要栈顶已有匹配参数个数的对象/值,否则运行时报
InvalidProgramException
,且堆栈无有效调试信息。

调用实例方法前必须先
Emit(OpCodes.Ldarg_0)
(this 引用)
字符串字面量用
Ldstr
,而非
Ldc_I4
加转换
访问字段用
Ldfld
/
Stfld
,访问属性必须调用其
get_*
/
set_*
方法
局部变量需先
DeclareLocal
,再用
Ldloc
/
Stloc
操作

动态类型在跨 Assembly 场景下的可见性问题

动态类型默认是

internal
级别,即使定义在
public
模块中,也无法被其他程序集访问。若需从外部调用(如插件系统),必须显式设为
public

typeBuilder.SetCustomAttribute(...); // 不够  
typeBuilder.SetAttributes(TypeAttributes.Public | TypeAttributes.Class);

更隐蔽的问题是:如果动态类型实现了某个接口,而该接口定义在另一个程序集中,必须确保该程序集已加载,且在

TypeBuilder.AddInterfaceImplementation
时传入的是已加载的
Type
对象,不能用字符串名称。

接口类型必须来自
Assembly.GetType("IFoo")
或类似方式获取,否则
AddInterfaceImplementation
ArgumentException
若接口含泛型参数,动态类型实现时需用
MakeGenericType
构造闭合类型
动态类型无法被序列化(如 JSON.NET 默认拒绝),除非手动添加
[Serializable]
或实现
ISerializable

真正难的不是写对 IL,而是让动态类型和现有代码边界清晰、生命周期可控。比如热重载场景下,旧类型实例还在运行,新类型已生成,两者共存时的委托绑定、事件订阅、GC 回收时机,这些细节比 Emit 本身更消耗调试时间。

相关推荐

热文推荐