




必须先调用AssemblyBuilder.DefineDynamicAssembly,.NET 5+需用RunAndCollect或Run访问模式;TypeBuilder是定义动态类型的唯一入口,需手动声明字段、方法并用ILGenerator.Emit写IL指令,栈平衡与类型可见性须严格控制。
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 访问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 操作动态类型默认是 internal 级别,即使定义在 public 模块中,也无法被其他程序集访问。若需从外部调用(如插件系统),必须显式设为 public:
typeBuilder.SetCustomAttribute(...); // 不够 typeBuilder.SetAttributes(TypeAttributes.Public | TypeAttributes.Class);
更隐蔽的问题是:如果动态类型实现了某个接口,而该接口定义在另一个程序集中,必须确保该程序集已加载,且在 TypeBuilder.AddInterfaceImplementation 时传入的是已加载的 Type 对象,不能用字符串名称。
Assembly.GetType("IFoo") 或类似方式获取,否则 AddInterfaceImplementation 报 ArgumentException
MakeGenericType 构造闭合类型[Serializable] 或实现 ISerializable
真正难的不是写对 IL,而是让动态类型和现有代码边界清晰、生命周期可控。比如热重载场景下,旧类型实例还在运行,新类型已生成,两者共存时的委托绑定、事件订阅、GC 回收时机,这些细节比 Emit 本身更消耗调试时间。