前面,本系列一共写了 九 篇关于反射和特性相关的文章,讲解了如何从程序集中通过反射将信息解析出来,以及实例化类型。

前面的九篇文章中,重点在于读数据,使用已经构建好的数据结构(元数据等),接下来,我们将学习 .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,这个类型用于构建程序集。类推,构建其它数据结构例如方法属性,则有 MethodBuilderPropertyBuilder

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 方法用于创建 classstructDefineEnum方法用于创建 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 构建代码的更多相关文章

  1. C#图解教程 第二十四章 反射和特性

    反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...

  2. .NET技术-1.0.使用反射、特性简化代码(验证Model类)

    使用反射.特性简化代码 参考项目:利用反射验证Model类/AssemblyVerification 假设现在有一个学生类(Student) /// <summary> /// 学生类 / ...

  3. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(十二) 代码重构使用反射工厂解耦(一)缓存切换

    前言 上一篇中,我们用了反射工厂来解除BLL和UI层耦合的问题.当然那是最简单的解决方法,再复杂一点的程序可能思路相同,但是在编程细节中需要考虑的就更多了,比如今天我在重构过程中遇到的问题.也是接下来 ...

  4. C#反射发出System.Reflection.Emit学习

    一.System.Reflection.Emit概述 Emit,可以称为发出或者产生.与Emit相关的类基本都存在于System.Reflection.Emit命名空间下.反射,我们可以取得形如程序集 ...

  5. C#反射与特性(一):反射基础

    目录 C#反射与特性(一):反射基础 1. 说明 1.1 关于反射.特性 2. 程序集操作 2.1 获取 程序集对象(Assembly) 2.2 Assembly 使用 2.3 获取程序集的方式 C# ...

  6. C#反射与特性(九):全网最全-解析反射

    目录 1,判断类型 1.1 类和委托 1.2 值类型 1.3 接口 1.4 数组 2, 类型成员 2.1 类 2.2 委托 2.3 接口 [微信平台,此文仅授权<NCC 开源社区>订阅号发 ...

  7. .NET基础拾遗(4)委托、事件、反射与特性

    Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...

  8. 十七、C# 反射、特性和动态编程

    反射.特性和动态编程   1.访问元数据 2.成员调用 3.泛型上的反射 4.自定义特性 5.特性构造器 6.具名参数 7.预定义特性 8.动态编程   特性(attribute)是在一个程序集中插入 ...

  9. 利用反射的特性将DataReader对象转化为List集合

    问题:将SqlDataReader对象转换为List<T>集合 思路: 1,利用反射的特性得到对应实体Model的公共属性 Type type = typeof(T); PropertyI ...

随机推荐

  1. 如何在没有core文件的情况下用dmesg+addr2line定位段错误

    前言 在现网环境下,程序奔溃后不一定会留下core文件,原因有很多,比如存储空间不足就是其中一个常见的原因.此时我们只能依据linux记录的错误日志来定位问题. 涉及linux命令 本文涉及以下几条命 ...

  2. Postgres基础操作

    显示数据库\l \l+ dw=# \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges ...

  3. sobel( ) 算子

    只是简单的使用方面的记录 sobel()算子是图像处理中用于边缘检测的 opencv-python 中的函数形式为 def Sobel(src, ddepth, dx, dy, dst=None, k ...

  4. SEPC:使用3D卷积从FPN中提取尺度不变特征,涨点神器 | CVPR 2020

    论文提出PConv为对特征金字塔进行3D卷积,配合特定的iBN进行正则化,能够有效地融合尺度间的内在关系,另外,论文提出SEPC,使用可变形卷积来适应实际特征间对应的不规律性,保持尺度均衡.PConv ...

  5. sku算法详解及Demo~接上篇

    前言 做过电商项目前端售卖的应该都遇见过不同规格产品库存的计算问题,业界名词叫做sku(stock Keeping Unit),库存量单元对应我们售卖的具体规格,比如一部手机具体型号规格,其中ipho ...

  6. 一文彻底搞懂BERT

    一.什么是BERT? 没错下图中的小黄人就是文本的主角Bert ,而红色的小红人你应该也听过,他就是ELMo.2018年发布的BERT 是一个 NLP 任务的里程碑式模型,它的发布势必会带来一个 NL ...

  7. Beta冲刺 —— 5.29

    这个作业属于哪个课程 软件工程 这个作业要求在哪里 Beta冲刺 这个作业的目标 Beta冲刺 作业正文 正文 github链接 项目地址 其他参考文献 无 一.会议内容 1.展示了每个人当天的成果. ...

  8. Window10:不能建立到远程计算机的连接,你可能需要更改此连接的网络设置。

    一,右键我的电脑点击管理. 二,在系统工具中找到设备管理,在设备管理中找到网络适配器. 三,在网络适配器中找到WAN Miniport(IP) 四,找到WAN Miniport(IP)右键放心卸载,作 ...

  9. WALT(Window Assisted Load Tracking)学习

    QCOM平台使用WALT(Window Assisted Load Tracking)作为CPU load tracking的方法:相对地,ARM使用的是PELT(Per-Entity Load Tr ...

  10. Java实现 LeetCode 479 最大回文数乘积

    479. 最大回文数乘积 你需要找到由两个 n 位数的乘积组成的最大回文数. 由于结果会很大,你只需返回最大回文数 mod 1337得到的结果. 示例: 输入: 2 输出: 987 解释: 99 x ...