C# 反射与特性(十):EMIT 构建代码
前面,本系列一共写了 九 篇关于反射和特性相关的文章,讲解了如何从程序集中通过反射将信息解析出来,以及实例化类型。
前面的九篇文章中,重点在于读数据,使用已经构建好的数据结构(元数据等),接下来,我们将学习 .NET Core 中,关于动态构建代码的知识。
其中表达式树已经在另一个系列写了,所以本系列主要是讲述 反射,Emit ,AOP 等内容。
如果现在总结一下,反射,与哪些数据结构相关?
我们可以从 AttributeTargets 枚举中窥见:
public enum AttributeTargets
{
All=16383,
Assembly=1,
Module=2,
Class=4,
Struct=8,
Enum=16,
Constructor=32,
Method=64,
Property=128,
Field=256,
Event=512,
Interface=1024,
Parameter=2048,
Delegate=4096,
ReturnValue=8192
}
分别是程序集、模块、类、结构体、枚举、构造函数、方法、属性、字段、事件、接口、参数、委托、返回值。
以往的文章中,已经对这些进行了很详细的讲解,我们可以中反射中获得各种各样的信息。当然,我们也可以通过动态代码,生成以上数据结构。
动态代码的其中一种方式是表达式树,我们还可以使用 Emit 技术、Roslyn 技术来编写;相关的框架有 Natasha、CS-Script 等。
构建代码
首先我们引入一个命名空间:
using System.Reflection.Emit;
Emit 命名空间中里面有很多用于构建动态代码的类型,例如 AssemblyBuilder
,这个类型用于构建程序集。类推,构建其它数据结构例如方法属性,则有 MethodBuilder
、PropertyBuilder
。
1,程序集(Assembly)
AssemblyBuilder 类型定义并表示动态程序集,它是一个密封类,其定义如下:
public sealed class AssemblyBuilder : Assembly
AssemblyBuilderAccess 定义动态程序集的访问模式,在 .NET Core 中,只有两个枚举:
枚举 | 值 | 说明 |
---|---|---|
Run | 1 | 可以执行但无法保存该动态程序集。 |
RunAndCollect | 9 | 当动态程序集不再可供访问时,将自动卸载该程序集,并回收其内存。 |
.NET Framework 中,有 RunAndSave 、Save 等枚举,可用于保存构建的程序集,但是在 .NET Core 中,是没有这些枚举的,也就是说,Emit 构建的程序集只能在内存中,是无法保存成 .dll 文件的。
另外,程序集的构建方式(API)也做了变更,如果你百度看到文章 AppDomain.CurrentDomain.DefineDynamicAssembly
,那么你可以关闭创建了,说明里面的很多代码根本无法在 .NET Core 下跑。
好了,不再赘述,我们来看看创建一个程序集的代码:
AssemblyName assemblyName = new AssemblyName("MyTest");
AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
构建程序集,分为两部分:
- AssemblyName 完整描述程序集的唯一标识。
- AssemblyBuilder 构建程序集
一个完整的程序集,有很多信息的,版本、作者、构建时间、Token 等,这些可以使用
AssemblyName 来设置。
一般一个程序集需要包含以下内容:
- 简单名称。
- 版本号。
- 加密密钥对。
- 支持的区域性。
你可以参考以下示例:
AssemblyName assemblyName = new AssemblyName("MyTest");
assemblyName.Name = "MyTest"; // 构造函数中已经设置,此处可以忽略
// Version 表示程序集、操作系统或公共语言运行时的版本号.
// 构造函数比较多,可以选用 主版本号、次版本号、内部版本号和修订号
// 请参考 https://docs.microsoft.com/zh-cn/dotnet/api/system.version?view=netcore-3.1
assemblyName.Version = new Version("1.0.0");
assemblyName.CultureName = CultureInfo.CurrentCulture.Name; // = "zh-CN"
assemblyName.SetPublicKeyToken(new Guid().ToByteArray());
最终程序集的 AssemblyName 显示名称是以下格式的字符串:
Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision> <, StrongName> <,PublicKeyToken> '\0'
例如:
ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d015c7d5a0b012
另外,创建程序集构建器使用 AssemblyBuilder.DefineDynamicAssembly()
而不是 new AssemblyBuilder()
。
2,模块(Module)
程序集和模块之间的区别可以参考
https://stackoverflow.com/questions/9271805/net-module-vs-assembly
https://stackoverflow.com/questions/645728/what-is-a-module-in-net
模块是程序集内代码的逻辑集合,每个模块可以使用不同的语言编写,大多数情况下,一个程序集包含一个模块。程序集包括了代码、版本信息、元数据等。
MSDN指出:“模块是没有 Assembly 清单的 Microsoft 中间语言(MSIL)文件。”。
这些就不再扯淡了。
创建完程序集后,我们继续来创建模块。
AssemblyName assemblyName = new AssemblyName("MyTest");
AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule("MyTest"); //
3,类型(Type)
目前步骤:
Assembly -> Module -> Type 或 Enum
ModuleBuilder 中有个 DefineType
方法用于创建 class
和 struct
;DefineEnum
方法用于创建 enum
。
这里我们分别说明。
创建类或结构体:
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyClass",TypeAttributes.Public);
定义的时候,注意名称是完整的路径名称,即命名空间+类型名称。
我们可以先通过反射,获取已经构建的代码信息:
Console.WriteLine($"程序集信息:{type.Assembly.FullName}");
Console.WriteLine($"命名空间:{type.Namespace} , 类型:{type.Name}");
结果:
程序集信息:MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
命名空间:MyTest , 类型:MyClass
接下来将创建一个枚举类型,并且生成枚举。
我们要创建一个这样的枚举:
namespace MyTest
{
public enum MyEnum
{
Top = 1,
Bottom = 2,
Left = 4,
Right = 8,
All = 16
}
}
使用 Emit 的创建过程如下:
EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEnum", TypeAttributes.Public, typeof(int));
TypeAttributes 有很多枚举,这里只需要知道声明这个枚举类型为 公开的(Public);typeof(int)
是设置枚举数值基础类型。
然后 EnumBuilder 使用 DefineLiteral
方法来创建枚举。
方法 | 说明 |
---|---|
DefineLiteral(String, Object) | 在枚举类型中使用指定的常量值定义命名的静态字段。 |
代码如下:
enumBuilder.DefineLiteral("Top", 0);
enumBuilder.DefineLiteral("Bottom", 1);
enumBuilder.DefineLiteral("Left", 2);
enumBuilder.DefineLiteral("Right", 4);
enumBuilder.DefineLiteral("All", 8);
我们可以使用反射将创建的枚举打印出来:
public static void WriteEnum(TypeInfo info)
{
var myEnum = Activator.CreateInstance(info);
Console.WriteLine($"{(info.IsPublic ? "public" : "private")} {(info.IsEnum ? "enum" : "class")} {info.Name}");
Console.WriteLine("{");
var names = Enum.GetNames(info);
int[] values = (int[])Enum.GetValues(info);
int i = 0;
foreach (var item in names)
{
Console.WriteLine($" {item} = {values[i]}");
i++;
}
Console.WriteLine("}");
}
Main 方法中调用:
WriteEnum(enumBuilder.CreateTypeInfo());
接下来,类型创建成员,就复杂得多了。
4,DynamicMethod 定义方法与添加 IL
下面我们来为 类型创建一个方法,并通过 Emit 向程序集中动态添加 IL。这里并不是使用 MethodBuider,而是使用 DynamicMethod。
在开始之前,请自行安装反编译工具 dnSpy 或者其它工具,因为这里涉及到 IL 代码。
这里我们先忽略前面编写的代码,清空 Main 方法。
我们创建一个类型:
public class MyClass{}
这个类型什么都没有。
然后使用 Emit 动态创建一个 方法,并且附加到 MyClass 类型中:
// 动态创建一个方法并且附加到 MyClass 类型中
DynamicMethod dyn = new DynamicMethod("Foo",null,null,typeof(MyClass));
ILGenerator iLGenerator = dyn.GetILGenerator();
iLGenerator.EmitWriteLine("HelloWorld");
iLGenerator.Emit(OpCodes.Ret);
dyn.Invoke(null,null);
运行后会打印字符串。
DynamicMethod 类型用于构建方法,定义并表示可以编译、执行和丢弃的一种动态方法。 丢弃的方法可用于垃圾回收。。
ILGenerator 是 IL 代码生成器。
EmitWriteLine 作用是打印字符串,
OpCodes.Ret 标记 结束方法的执行,
Invoke 将方法转为委托执行。
上面的示例比较简单,请认真记一下。
下面,我们要使用 Emit 生成一个这样的方法:
public int Add(int a,int b)
{
return a + b;
}
看起来很简单的代码,要用 IL 来写,就变得复杂了。
ILGenerator 正是使用 C# 代码的形式去写 IL,但是所有过程都必须按照 IL 的步骤去写。
其中最重要的,便是 OpCodes 枚举了,OpCodes 有几十个枚举,代表了 IL 的所有操作功能。
请参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1
如果你点击上面的链接查看 OpCodes 的枚举,你可以看到,很多 功能码,这么多功能码是记不住的。我们现在刚开始学习 Emit,这样就会难上加难。
所以,我们要先下载能够查看 IL 代码的工具,方便我们探索和调整写法。
我们看看此方法生成的 IL 代码:
.method public hidebysig instance int32
Add(
int32 a,
int32 b
) cil managed
{
.maxstack 2
.locals init (
[0] int32 V_0
)
// [14 9 - 14 10]
IL_0000: nop
// [15 13 - 15 26]
IL_0001: ldarg.1 // a
IL_0002: ldarg.2 // b
IL_0003: add
IL_0004: stloc.0 // V_0
IL_0005: br.s IL_0007
// [16 9 - 16 10]
IL_0007: ldloc.0 // V_0
IL_0008: ret
} // end of method MyClass::Add
看不懂完全没关系,因为笔者也看不懂。
目前我们已经获得了上面两大部分的信息,接下来我们使用 DynamicMethod
来动态编写方法。
定义 Add 方法并获取 IL 生成工具:
DynamicMethod dynamicMethod = new DynamicMethod("Add",typeof(int),new Type[] { typeof(int),typeof(int)});
ILGenerator ilCode = dynamicMethod.GetILGenerator();
DynamicMethod 用于定义一个方法;ILGenerator是 IL 生成器。当然也可以将此方法附加到一个类型中,完整代码示例如下:
// typeof(Program),表示将此动态编写的方法附加到 MyClass 中
DynamicMethod dynamicMethod = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) },typeof(MyClass));
ILGenerator ilCode = dynamicMethod.GetILGenerator();
ilCode.Emit(OpCodes.Ldarg_0); // a,将索引为 0 的自变量加载到计算堆栈上。
ilCode.Emit(OpCodes.Ldarg_1); // b,将索引为 1 的自变量加载到计算堆栈上。
ilCode.Emit(OpCodes.Add); // 将两个值相加并将结果推送到计算堆栈上。
// 下面指令不需要,默认就是弹出计算堆栈的结果
//ilCode.Emit(OpCodes.Stloc_0); // 将索引 0 处的局部变量加载到计算堆栈上。
//ilCode.Emit(OpCodes.Br_S); // 无条件地将控制转移到目标指令(短格式)。
//ilCode.Emit(OpCodes.Ldloc_0); // 将索引 0 处的局部变量加载到计算堆栈上。
ilCode.Emit(OpCodes.Ret); // 即 return,从当前方法返回,并将返回值(如果存在)从被调用方的计算堆栈推送到调用方的计算堆栈上。
// 方法1
Func<int, int, int> test = (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
Console.WriteLine(test(1, 2));
// 方法2
int sum = (int)dynamicMethod.Invoke(null, BindingFlags.Public, null, new object[] { 1, 2 }, CultureInfo.CurrentCulture);
Console.WriteLine(sum);
实际以上代码与我们反编译出来的 IL 编写有所差异,具体俺也不知道为啥,在群里问了调试了,注释掉那么几行代码,才通过的。
C# 反射与特性(十):EMIT 构建代码的更多相关文章
- C#图解教程 第二十四章 反射和特性
反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...
- .NET技术-1.0.使用反射、特性简化代码(验证Model类)
使用反射.特性简化代码 参考项目:利用反射验证Model类/AssemblyVerification 假设现在有一个学生类(Student) /// <summary> /// 学生类 / ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(十二) 代码重构使用反射工厂解耦(一)缓存切换
前言 上一篇中,我们用了反射工厂来解除BLL和UI层耦合的问题.当然那是最简单的解决方法,再复杂一点的程序可能思路相同,但是在编程细节中需要考虑的就更多了,比如今天我在重构过程中遇到的问题.也是接下来 ...
- C#反射发出System.Reflection.Emit学习
一.System.Reflection.Emit概述 Emit,可以称为发出或者产生.与Emit相关的类基本都存在于System.Reflection.Emit命名空间下.反射,我们可以取得形如程序集 ...
- C#反射与特性(一):反射基础
目录 C#反射与特性(一):反射基础 1. 说明 1.1 关于反射.特性 2. 程序集操作 2.1 获取 程序集对象(Assembly) 2.2 Assembly 使用 2.3 获取程序集的方式 C# ...
- C#反射与特性(九):全网最全-解析反射
目录 1,判断类型 1.1 类和委托 1.2 值类型 1.3 接口 1.4 数组 2, 类型成员 2.1 类 2.2 委托 2.3 接口 [微信平台,此文仅授权<NCC 开源社区>订阅号发 ...
- .NET基础拾遗(4)委托、事件、反射与特性
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- 十七、C# 反射、特性和动态编程
反射.特性和动态编程 1.访问元数据 2.成员调用 3.泛型上的反射 4.自定义特性 5.特性构造器 6.具名参数 7.预定义特性 8.动态编程 特性(attribute)是在一个程序集中插入 ...
- 利用反射的特性将DataReader对象转化为List集合
问题:将SqlDataReader对象转换为List<T>集合 思路: 1,利用反射的特性得到对应实体Model的公共属性 Type type = typeof(T); PropertyI ...
随机推荐
- 03 返回静态文件的高级web框架
03 返回静态文件的高级web框架 服务器server端python程序(高级版): import socket server=socket.socket() server.bind(("1 ...
- xampp apache 安全性问题
要禁止 Apache 显示目录结构列表,只需将 Option 中的 Indexes 去掉即可.<Directory "D:/Apa/blabla"> Options I ...
- jdk1.8的一些特性
一.jdk1.8的特性: Lambda表达式 函数式接口 方法引用 接口的默认方法和静态方法 Optional Streams 并行数组 新时间日期API 二.Lambda表达式: Lambda 表达 ...
- 解决使用IDEA启动Tomcat成功但localhost:8080无法访问的问题
解决使用IDEA启动Tomcat成功但localhost:8080无法访问的问题
- 解决linux下启动tomcat找不到jdk
在tomcat目录下 vim catalina.sh 头部加入 JAVA_HOME='/root/use/local/java/jdk/';export JAVA_HOME;
- Java 在PPT中创建SmartArt图形、读取SmartArt图形中的文本
一.概述及环境准备 SmartArt 图形通过将文字.图形从多种不同布局.组合来表现内容和观点的逻辑关系,能够快速.有效地传达设计者的意图和信息.这种图文表达的视觉表示形式常用于PPT,Word,Ex ...
- 技术大佬:我去,这个容易被忽略的小程序Image图片属性,竟然这么屌!
前段时间在做“高清壁纸推荐”小程序优化的时候,发现一个很实用的图片属性——能够实现最大化压缩图片大小.且图片质量最小的损失,在此之前一直没有注意.今天跟大家分享一下这个属性的用法,主要是让大家能够,意 ...
- 伪静态%{REQUEST_FILENAME} !-f 和!-d用法
%{REQUEST_FILENAME} !-f 和!-d只对下一条RewriteRule起作用.再往下的RewriteRule不管用. -f 表示为文件 -d 表示为目录 ! 表示非,取反的意思 R ...
- Cypress系列(9)- Cypress 编写和组织测试用例篇 之 钩子函数Hook
如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html Hook 就是常说的钩子函数,在 pyt ...
- Linux (五) VIM编辑器
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 1.简介 Linux系统环境下的一款非常重要的文本编辑工具,我们在Linux环境下几乎所有的文本文件 ...