.NET高级特性-Emit

.NET高级特性-Emit基础

  在这个大数据/云计算/人工智能研发普及的时代,Python的崛起以及Javascript的前后端的侵略,程序员与企业似乎越来越青睐动态语言所带来的便捷性与高效性,即使静态语言在性能,错误检查等方面的优于静态语言。对于.NETer来说,.NET做为一门静态语言,我们不仅要打好.NET的基本功,如基本类型/语法/底层原理/错误检查等知识,也要深入理解.NET的一些高级特性,来为你的工作减轻负担和提高代码质量。

  ok,咱们今天开始聊一聊.NET中的Emit。

一、什么是Emit?

  Emit含义为发出、产生的含义,这是.NET中的一组类库,命名空间为System.Reflection.Emit,几乎所有的.NET版本(Framework/Mono/NetCore)都支持Emit,可以实现用C#代码生成代码的类库

二、Emit的本质

  我们知道.NET可以由各种语言进行编写,比如VB,C++等,当然绝大部分程序员进行.NET开发都是使用C#语言进行的,这些语言都会被各自的语言解释器解释为IL语言并执行,而Emit类库的作用就是用这些语言来编写生成IL语言,并交给CLR(公共语言运行时)进行执行。

  我们先来看看IL语言长什么样子:

  (1) 首先我们创建一个Hello,World程序

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
复制代码

  (2) 将程序编译成dll文件,我们可以看到在开发目录下生成了bin文件夹

  

  (3) 向下寻找,我们可以看到dll文件已经生成,笔者使用netcore3进行开发,故路径为bin/Debug/netcoreapp3.0

  

  (4) 这时候,我们就要祭出我们的il查看神器了,ildasm工具

  

  如何找到这个工具?打开开始菜单,找到Visual Studio文件夹,打开Developer Command Prompt,在打开的命令行中键入ildasm回车即可,笔者使用vs2019进行演示,其它vs版本操作方法均一致

  

 

 

 

 

 

 

   (5) 在dasm菜单栏选择文件->打开,选择刚刚生成的dll文件

  

 

 

   (6) 即可查看生成il代码

  

 

  有了ildasm的辅助,我们就能够更好的了解IL语言以及如何编写IL语言,此外,Visual Studio中还有许多插件支持查看il代码,比如JetBrains出品的Resharper插件等,如果觉得笔者方式较为麻烦可以使用以上插件查看il代码

三、理解IL代码

  在上一章节中,我们理解了Emit的本质其实就是用C#来编写IL代码,既然要编写IL代码,那么我们首先要理解IL代码是如何进行工作的,IL代码是如何完成C#当中的顺序/选择/循环结构的,是如何实现类的定义/字段的定义/属性的定义/方法的定义的。

  IL代码是一种近似于指令式的代码语言,与汇编语言比较相近,所以习惯于写高级语言的.NETer来说比较难以理解

  让我们来看看Hello,World程序的IL代码:

IL_0000:  nop
IL_0001:  ldstr      "Hello World!"
IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
IL_000b:  nop
IL_000c:  ret

  我们可以把IL代码看成栈的运行

  第一条指令,nop表示不做任何事情,表示代码不做任何事情

  第二条指令,ldstr表示将字符串放入栈中,字符串的值为“Hello,World!”

  第三条指令,call表示调用方法,参数为调用方法的方法信息,并把返回的结构压入栈中,使用的参数为之前已经入栈的“Hello World!”,以此类推,如果方法有n个参数,那么他就会调取栈中n个数据,并返回一个结果放回栈中

  第四条指令,nop表示不做任何事情

  第五条指令,ret表示将栈中顶部的数据返回,如果方法定义为void,则无返回值

  关于Hello,world程序IL的理解就说到这里,更多的指令含义读者可以参考微软官方文档,笔者之后也会继续对Emit进行讲解和Emit的应用

四、用Emit类库编写IL代码

  既然IL代码咱们理解的差不多了,咱们就开始尝试用C#来写IL代码了,有了IL代码的参考,咱们也可以依葫芦画瓢的把代码写出来了

  (1) 引入Emit命名空间

using System.Reflection.Emit;

  (2) 首先我们定义一个Main方法,入参无,返回类型void

//定义方法名,返回类型,输入类型
var method = new DynamicMethod("Main", null, Type.EmptyTypes);

  (3) 生成IL代码

复制代码
//生成IL代码
var ilGenerator = method.GetILGenerator();
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ldstr,"Hello World!");
ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); //寻找Console的WriteLine方法
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);
复制代码

  (4) 创建委托并调用

//创建委托
var helloWorldMethod = method.CreateDelegate(typeof(Action)) as Action;
helloWorldMethod.Invoke();

  (5)运行,即输出Hello World!

五、小结

  Emit的本质是使用高级语言生成IL代码,进而进行调用的的一组类库,依赖Emit我们可以实现用代码生成代码的操作,即编程语言的自举,可以有效弥补静态语言的灵活性的缺失。

  Emit的性能非常好,除了第一次构建IL代码所需要时间外,之后只要将操作缓存在计算机内存中,速度与手写代码相差无几

  有许多著名.NET类库均依赖于Emit:

  (.NET JSON操作库)Json.NET/Newtonsoft.Json: github地址

  (轻量ORM)Dapper:gituhb地址

  (ObjectToObjectMapper)EmitMapper:github地址

  (AOP库)Castle.DynamicProxy:github地址

  学习Emit:

  .NET官方文档:https://docs.microsoft.com/zh-cn/dotnet

  .NET API浏览器:https://docs.microsoft.com/zh-cn/dotnet/api

  之后作者将继续讲解.NET Emit的相关内容和应用,感谢阅读

 

出处:https://www.cnblogs.com/billming/p/emit-study.html

=======================================================================================

.NET高级特性-Emit(2)类的定义

  在上一篇博文发了一天左右的时间,就收到了博客园许多读者的评论和推荐,非常感谢,我也会及时回复读者的评论。之后我也将继续撰写博文,梳理相关.NET的知识,希望.NET的圈子能越来越大,开发者能了解/深入.NET的本质,将工作做的简单又高效,拒绝重复劳动,拒绝CRUD。

  ok,咱们开始继续Emit的探索。在这之前,我先放一下我往期关于Emit的文章,方便读者阅读。

  《.NET高级特性-Emit(1)

一、基础知识

  既然C#作为一门面向对象的语言,所以首当其冲的我们需要让Emit为我们动态构建类。

  废话不多说,首先,我们先来回顾一下C#类的内部由什么东西组成:

  (1) 字段-C#类中保存数据的地方,由访问修饰符、类型和名称组成;

  (2) 属性-C#类中特有的东西,由访问修饰符、类型、名称和get/set访问器组成,属性的是用来控制类中字段数据的访问,以实现类的封装性;在Java当中写作getXXX()和setXXX(val),C#当中将其变成了属性这种语法糖;

  (3) 方法-C#类中对逻辑进行操作的基本单元,由访问修饰符、方法名、泛型参数、入参、出参构成;

  (4) 构造器-C#类中一种特殊的方法,该方法是专门用来创建对象的方法,由访问修饰符、与类名相同的方法名、入参构成。

  接着,我们再观察C#类本身又具备哪些东西:

  (1) 访问修饰符-实现对C#类的访问控制

  (2) 继承-C#类可以继承一个父类,并需要实现父类当中所有抽象的方法以及选择实现父类的虚方法,还有就是子类需要调用父类的构造器以实现对象的创建

  (3) 实现-C#类可以实现多个接口,并实现接口中的所有方法

  (4) 泛型-C#类可以包含泛型参数,此外,类还可以对泛型实现约束

  以上就是C#类所具备的一些元素,以下为样例:

复制代码
public abstract class Bar
{
    public abstract void PrintName();
}
public interface IFoo<T> { public T Name { get; set; } } //继承Bar基类,实现IFoo接口,泛型参数T
public class Foo<T> : Bar, IFoo<T>
  //泛型约束
  where T : struct {
//构造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //属性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
    Console.WriteLine(_name.ToString()); }
}
复制代码

  在探索完了C#类及其定义后,我们要来了解C#的项目结构组成。我们知道C#的一个csproj项目最终会对应生成一个dll文件或者exe文件,这一个文件我们称之为程序集Assembly;而在一个程序集中,我们内部包含和定义了许多命名空间,这些命令空间在C#当中被称为模块Module,而模块正是由一个一个的C#类Type组成。

 

 

 

   所以,当我们需要定义C#类时,就必须首先定义Assembly以及Module,如此才能进行下一步工作。

二、IL概览

   由于Emit实质是通过IL来生成C#代码,故我们可以反向生成,先将写好的目标代码写成cs文件,通过编译器生成dll,再通过ildasm查看IL代码,即可依葫芦画瓢的编写出Emit代码。所以我们来查看以下上节Foo所生成的IL代码。

  

 

 

   从上图我们可以很清晰的看到.NET的层级结构,位于树顶层浅蓝色圆点表示一个程序集Assembly,第二层蓝色表示模块Module,在模块下的均为我们所定义的类,类中包含类的泛型参数、继承类信息、实现接口信息,类的内部包含构造器、方法、字段、属性以及它的get/set方法,由此,我们可以开始编写Emit代码了

三、Emit编写

  有了以上的对C#类的解读和IL的解读,我们知道了C#类本身所需要哪些元素,我们就开始根据这些元素来开始编写Emit代码了。这里的代码量会比较大,请读者慢慢阅读,也可以参照以上我写的类生成il代码进行比对。

  在Emit当中所有创建类型的帮助类均以Builder结尾,从下表中我们可以看的非常清楚

元素中文元素名称对应Emit构建器名称
程序集  Assembly AssemblyBuilder
模块  Module ModuleBuilder
 Type TypeBuilder
构造器  Constructor ConstructorBuilder
属性  Property PropertyBuilder
字段  Field FieldBuilder
方法  Method MethodBuilder

  由于创建类需要从Assembly开始创建,所以我们的入口是AssemblyBuilder

  (1) 首先,我们先引入命名空间,我们以上节Foo类为样例进行编写

using System.Reflection.Emit;

  (2) 获取基类和接口的类型

var barType = typeof(Bar);
var interfaceType = typeof(IFoo<>);

  (3) 定义Foo类型,我们可以看到在定义类之前我们需要创建Assembly和Module

//定义类
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

  (4) 定义泛型参数T,并添加约束

//定义泛型参数
var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
//设置泛型约束
genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

  (5) 继承和实现接口,注意当实现类的泛型参数需传递给接口时,需要将泛型接口添加泛型参数后再调用AddInterfaceImplementation方法

//继承基类
typeBuilder.SetParent(barType);
//实现接口
typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

  (6) 定义字段,因为字段在构造器值需要使用,故先创建

//定义字段
var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

  (7) 定义构造器,并编写内部逻辑

复制代码
//定义构造器
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
var ctorIL = ctorBuilder.GetILGenerator();
//Ldarg_0在实例方法中表示this,在静态方法中表示第一个参数
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
//为field赋值
ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
ctorIL.Emit(OpCodes.Ret);
复制代码

  (8) 定义Name属性

//定义属性
var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

  (9) 编写Name属性的get/set访问器

复制代码
//定义get方法
var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
var getIL = getMethodBuilder.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldfld, fieldBuilder);
getIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //实现对接口方法的重载
propertyBuilder.SetGetMethod(getMethodBuilder); //设置为属性的get方法
//定义set方法
var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
var setIL = setMethodBuilder.GetILGenerator();
setIL.Emit(OpCodes.Ldarg_0);
setIL.Emit(OpCodes.Ldarg_1);
setIL.Emit(OpCodes.Stfld, fieldBuilder);
setIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //实现对接口方法的重载
propertyBuilder.SetSetMethod(setMethodBuilder); //设置为属性的set方法
复制代码

   (10) 定义并实现PrintName方法

复制代码
//定义方法
var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
var printIL = printMethodBuilder.GetILGenerator();
printIL.Emit(OpCodes.Ldarg_0);
printIL.Emit(OpCodes.Ldflda, fieldBuilder);
printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
printIL.Emit(OpCodes.Ret);
//实现对基类方法的重载
typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));
复制代码

  (11) 创建类

var type = typeBuilder.CreateType(); //netstandard中请使用CreateTypeInfo().AsType()

  (12) 调用

var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
(obj as Bar).PrintName();
Console.WriteLine((obj as IFoo<DateTime>).Name);

四、应用

  上面的样例仅供学习只用,无法运用在实际项目当中,那么,Emit构建类在实际项目中我们可以有什么应用,提高我们的编码效率

  (1) 动态DTO-当我们需要将实体映射到某个DTO时,可以用动态DTO来代替你手写的DTO,选择你需要的字段回传给前端,或者前端把他想要的字段传给后端

  (2) DynamicLinq-我的第一篇博文有个读者提到了表达式树,而linq使用的正是表达式树,当表达式树+Emit时,我们就可以用像SQL或者GraphQL那样的查询语句实现动态查询

  (3) 对象合并-我们可以编写实现一个像js当中Object.assign()一样的方法,实现对两个实体的合并

  (4) AOP动态代理-AOP的核心就是代理模式,但是与其对应的是需要手写代理类,而Emit就可以帮你动态创建代理类,实现切面编程

  (5) ...

五、小结

  对于Emit,确实初学者会对其感到复杂和难以学习,但是只要搞懂其中的原理,其实最终就是C#和.NET语言的本质所在,在学习Emit的同时,也是在锻炼你的基本功是否扎实,你是否对这门语言精通,是否有各种简化代码的应用。

  保持学习,勇于实践;Write Less,Do More;作者之后还会继续.NET高级特性系列,感谢阅读!

 

出处:https://www.cnblogs.com/billming/p/emit-study-class.html

=======================================================================================

.NET高级特性-Emit(2.1)字段

  在上篇blog写完的几天后,有读者反映写的过于复杂,导致无法有效的进行实践;博主在考虑到园子里程序员水平高低不一致的情况,所以打算放慢脚步,对类的一些内容进行详细的讲解,顺带的会写一些笔者所遇到过的Emit的坑以及如何使用Emit来为我们的工作减负,毕竟,知识用到实践当中才有其因有的价值。博主在文末也会将样例上传github,方便大家实践。

  首先,照例我先把我之前写的博文链接上来,方便大家阅读

  《.NET高级特性-Emit(1)

  《.NET高级特性-Emit(2)类的定义

一、什么是字段

   有很多读者会说,我在项目当中基本上没怎么用到字段啊,基本上都是用C#的属性居多,两者不是都能存储数据吗,你看我只要写以下代码就可以完成使用或存储对象的信息。

复制代码
public class User
{
    public string Id { get; set; }

    public string UserName { get; set; }

    public string PasswordHash { get; private set; }

    public void SetPassword(string password)
    {
        PasswordHash = password;
    }
}
复制代码

  你看,我上面的实体一个字段都没用到,全部都是属性,字段有什么作用啊。

  其实,这就是典型的因为C#的语法糖带来的误解,C#中存储数据的地方只可能是字段,这在所有面向对象的语言当中都是一致的,C++也好,Java也罢,都是相同的那是什么导致了C#当中会有这种误解存在呢;没错,就是属性这种C#特有的东西存在,以及在C#5.0之后出现的自动属性让程序员对字段与属性产生了误解,在C#5.0之前,也就是没有自动属性之前,以上实体定义是这样编写的:

复制代码
public class User2
{
    private string _id;
    public string Id { get => _id; set => _id = value; }

    private string _userName;
    public string UserName { get => _userName; set => _userName = value; }

    private string _passwordHash;
    public string PasswordHash { get => _passwordHash; private set => _passwordHash = value; }

    public void SetPassword(string password)
    {
        PasswordHash = password;
    }
}
复制代码

  当我写了以上代码的时候,Visual Studio也提示我,希望我使用自动属性对字段进行隐藏:

 

   当我点击黄色感叹号时,它就出现对应的修改方案

 

   点击使用自动属性时,就变成了只有属性,没有字段的形式了

 

   所以,C#类当中可以保存数据的有且只可能有字段,.NET开发者不要因为C#丰富的语法糖而产生误解,要看透这些语法糖中的C#本质,此外你也可以使用Emit查看刚才User的IL代码,自动属性最终还是会生成一个私有字段和一个该字段对应的属性

二、字段的定义

   讲完了什么是字段,以及一些容易掉入的C#概念误区,我没开始来使用Emit创建字段定义,由于字段只可能是类的一部分,故所以需要使用TypeBuilder来创建字段,对Emit不熟悉的读者可以查看博主的前两篇文章,里面概述了Emit所使用的一些类的定义。

  好,咱们开始写代码,首先,我们先给出我们要最终生成的结果:

复制代码
    public class UserField
    {
        public static readonly string TokenPrefix = "Bearer";
        public UserField()
        {
            id = Guid.NewGuid().ToString("N");
        }

        public readonly string id;

        public string userName;

        private string passwordHash = "123456";

        public string GetPasswodHash()
        {
            return passwordHash;
        }

        public void SetPassword(string password)
        {
            passwordHash = password;
        }
    }
复制代码

  我们首先忽略掉类的构造器与方法,我们当前只关注字段的定义,我们可以看到,字段可以由四部分组成:

    (1)字段的修饰符-访问修饰符定义了字段的一些特性,如public/private/protected表示访问级别;readonly表示了字段是否可以被外部写入;static表示该字段的归属,是属于对象还是属于类。

    (2)字段的类型-字段的类型定义了该字段是由什么数据类型,由此计算机才可以确定该字段在计算机中所使用的内存空间,进而知晓一个对象需要分配多少内存空间才能将数据装入

    (3)字段的名称-字段的名称用来表述该字段在该对象/类中所表达的含义,让程序员能理解该字段所存储的数据在现实世界的表述

    (4)字段的默认值-字段在类初始化后一定会拥有一个默认值,除了在构造器中或者字段后给予的默认值之外,其它未赋值的字段均使用default填充该字段,当然,不同的字段类型default给予的值也会不一样,对于引用类型会给予null值,对于结构体类型会使用默认构造器,对于基本值类型,会赋予0值,对于枚举,也会赋予0值;这个博主会在之后讲解Emit变量与常量当中会讲解到

  

 

   好,开始撸代码,第一步当然是要引入我们的主角-Emit类库,而且由于一些枚举特性存放在反射类库中,我们也要将其引入

using System.Reflection.Emit;
using System.Reflection;

  第二步,创建类,若对创建类的过程不清楚可以阅读我的博文《.NET高级特性-Emit(2)类的定义》,里面详细介绍了类的定义及项目的结构组成

            var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
            var moduleBuilder = asmBuilder.DefineDynamicModule("Edwin.Blog.Emit");
            var typeBuilder = moduleBuilder.DefineType("UserField", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.BeforeFieldInit);

  第三步,首先创建静态字段TokenPrefix

            //第一个变量表示字段名称,第二个变量表示字段的类型,第三个变量表示字段的特性(修饰符)为public readonly static
            var tokenPrefixBuilder = typeBuilder.DefineField("TokenPrefix", typeof(string), FieldAttributes.Public | FieldAttributes.InitOnly | FieldAttributes.Static);

  第四步,同第三步,创建其余非静态字段

            var idBuilder = typeBuilder.DefineField("id", typeof(string), FieldAttributes.Public | FieldAttributes.InitOnly);
            var userNameBuilder = typeBuilder.DefineField("userName", typeof(string), FieldAttributes.Public);
            var passwordHashBuilder = typeBuilder.DefineField("passwordHash", typeof(string), FieldAttributes.Private);

  这样我们的字段就定义好了。

  ok,相信很多读者都有疑问,我这怎么没写默认值啊,你看字段TokenPrefix都有字段携带着啊,你怎么就把它丢掉了呢?别急,其实在字段后面写默认值也是C#语言的语法糖,我会在下一节进行讲述。

三、字段的操作

  上一节的代码当中只有字段的定义而少了字段的默认值和对字段的对于的方法,那么我们就来开始解决以上问题吧。

  首先,在字段后面写默认值的方法是C#的语法糖,其实其真正的写法是将默认值在构造器中进行赋值,静态字段在静态构造器中赋值,对象字段在构造器中赋值,那么在IL中,UserField类生成的源代码应该是这样的

复制代码
    public class UserField
    {
        public static readonly string TokenPrefix;
        static UserField()
        {
            TokenPrefix = "Bearer";
        }
        public UserField()
        {
            id = Guid.NewGuid().ToString("N");
            passwordHash = "123456";
        }

        public readonly string id;

        public string userName;

        private string passwordHash;

        public string GetPasswodHash()
        {
            return passwordHash;
        }

        public void SetPassword(string password)
        {
            passwordHash = password;
        }
    }
复制代码

  也就是说,C#只允许在构造器中对字段可以进行赋初值,所以在Emit中,我们也只能通过构造器来对字段进行默认值赋值,那么问题来了,如何对字段进行操作,字段又有哪些操作呢?这一节博主就来聊一聊字段的操作。

  其实,在Emit当中,对字段的操作只有两种:

  (1)入栈(取值)-将字段的值取出放入到栈顶,入栈的Emit操作码都是以Ld作为开头,而字段在Emit操作码均以fld(field)出现,所以字段入栈的Emit操作码为OpCodes.Ldfld以及OpCodes.Ldsfld,前者表示入栈对象字段,后者表示入栈静态字段;

  (2)保存-将栈顶的值保存到字段,由于保存的Emit操作码以St(Store)作为开头,所以字段有两个保存操作码OpCodes.Stfld和OpCodes.Stsfld,各自的含义请各位联想。

  如果需要更为详细的操作码信息,各位读者请阅读微软API浏览器了解详细信息:《MS DOTNET API浏览器

  好,说完了字段的操作类型,我们开始编写对字段的操作。

  • 首先我们从静态构造器开始,创建静态构造器并编写Emit代码:
            //创建静态构造器(第一个参数表示为私有静态,第三个参数表示入参数量和类型)
            var staticCtorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.SpecialName | MethodAttributes.HideBySig, CallingConventions.Standard, Type.EmptyTypes);
            var staticCtorIL = staticCtorBuilder.GetILGenerator();
  • 编写Emit代码
复制代码
            //将常量字符串"Bearer"放入栈顶
            staticCtorIL.Emit(OpCodes.Ldstr, "Bearer");
            //取出栈顶元素赋值给字段TokenPrefix
            staticCtorIL.Emit(OpCodes.Stsfld, tokenPrefixBuilder);
            //返回
            staticCtorIL.Emit(OpCodes.Ret);
复制代码
  • 静态构造器编写完成,我们开始编写实例构造器,与上边静态构造器同理,唯一的区别是,对象字段都是对象的成员,所以需要找到this成员才能获得字段(即this.field)
复制代码
            var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, CallingConventions.Standard, Type.EmptyTypes);
            var ctorIL = ctorBuilder.GetILGenerator();
            //将this压入栈中(与上面静态构造器的区别)
            ctorIL.Emit(OpCodes.Ldarg_0);
            //将常量字符串"123456"放入栈顶
            ctorIL.Emit(OpCodes.Ldstr, "123456");
            //取出栈顶元素赋值给字段
            ctorIL.Emit(OpCodes.Stfld, passwordHashBuilder);
            //返回
            ctorIL.Emit(OpCodes.Ret);
复制代码
  • 最后,我们编写一个GetPasswordHash方法,实现字段的取值并返回
复制代码
            var getPasswordHashMethodBuilder = typeBuilder.DefineMethod("GetPasswordHash", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(string), Type.EmptyTypes);
            var getPasswordHashIL = getPasswordHashMethodBuilder.GetILGenerator();
            //将this压入栈中
            getPasswordHashIL.Emit(OpCodes.Ldarg_0);
            //将字段值压入到栈中
            getPasswordHashIL.Emit(OpCodes.Ldfld, passwordHashBuilder);
            //返回
            getPasswordHashIL.Emit(OpCodes.Ret);
复制代码
  • 最后的最后,不要忘记创建类型哦
       typeBuilder.CreateTypeInfo().AsType();

  使用类型创建对象,并调用即可看到效果

            dynamic user = Activator.CreateInstance(type);
            Console.WriteLine(user.GetPasswordHash());

一、小结

  在编写C#时,一定要小心C#自带的语法糖产生错误认知,看穿语法糖的本质,你对这门语言的理解就更加深入,对你了解其它语言也有类似的帮助,毕竟即使编程语言在不断的涌现和发展,你也能把握其最本质的、不变的东西,就像算法与数据结构一样是软件的灵魂一样。

  下一篇,博主将详细介绍C#中最特殊的东西-属性,感谢阅读,以下为github样例地址

  https://github.com/MJEdwin/edwin-blog-sample/blob/master/Edwin.Blog.Sample/Field/UserEmit.cs

 

出处:https://www.cnblogs.com/billming/p/emit-study-field.html

=======================================================================================

.NET高级特性-Emit(2.2)属性

  关于Emit的博客已经进入第四篇,在读本篇博文之前,我希望读者能先仔细回顾博主之前所编写的关于Emit的博文,从该篇博文开始,我们就可以真正的使用Emit,并把知识转化为实战,我也会把之前的博文链接放在下方,以方便读者阅读,大家也可以将自己的疑问或者指正写在评论当中,博主会积极进行回复。

  ok,今天我们继续来探索C#-Emit中关于类的知识和应用,今天我们要来探索和挖掘关于C#属性的二三事,并且我们要开始使用Emit中关于类、字段和属性开启我们的第一个应用-动态创建匿名类 

一、什么是属性?

  属性-C#中让人既爱又恨的东西,爱的是C#当中因为有了属性,.NET开发者只需要一句话就可以完成对类的封装,根本不需要像其它语言写这么多东西,我们可以用java来比较一下

  在C#当中我们定义一个实体属性

public string Title { get; set; }

   在Java当中我们就需要这样定义

复制代码
private String title;

public String getTitle()
{
    return title;
}

public void setTitle(String value)
{
    title = value;
}
复制代码

  在C#当中简简单单的一句话在Java当中就需要写一个字段将两个方法,当然我不是在贬低Java,只是表明Java没有在语法上为开发者提供便利,当然这些年Java的语法也在逐渐完善,从Java8开始逐渐加入了推断类型var/匿名委托等等优秀的语法。

  扯的有点远了,当然C#中使用属性也有它的问题,首先是许多入门级的程序员把属性当成字段进行泛滥的使用,造成了C#类失去了封装性,没有了封装,有可能就会造成致命的漏洞,所以请刚入门的程序员请慎重使用属性,属性虽然好但是不要滥用,在你对属性不熟悉的时候,尤其要处理好它的set访问器,或者抛弃属性使用以下最原始的方法进行编写

复制代码
private string title;

public string GetTitle()
{
    return title;
}

public void SetTitle(string value)
{
    title = value;
}
复制代码

  ok,其实在上面与Java的比较当中我们其实已经知道了属性是什么了,属性是对类中一类特殊方法的语法糖,这一类方法的功能是负责对字段的读取和设置,称之为get/set访问器,get方法用于获取字段的值,而set方法是对传入的值对字段进行赋值,当然,如何赋值和取值,就取决于你方法怎么写了

  那么有的读者就会有疑问,既然属性只是对于get/set访问器的语法糖,那么对应的字段跑哪里去了呢,其实这里面还运用了一种叫做自动属性的语法糖,这是在C#5.0之后增加的一种语法糖,对它详细的讲解可以查看我的博文《.NET高级特性-Emit(2.1)字段》,文章中详细说明了C#如何将最终的字段省略的全过程。

二、IL中的属性

   简单讲完了属性是什么以及属性的本质,我们就要来简要说说IL中的属性,因为Emit当中最终编写的还是IL代码。在IL当中,属性或者自动属性它的原本面貌就会被还原,下面的样例看的就清清楚楚。

  首先,我们先定义一个Blog类,里面包含两个属性-Title和Content,表示标题和内容

复制代码
    public class Blog
    {
        public string Title { get; set; }

        public string Content { get; set; }
    }
复制代码

  接着,使用ildasm工具查看IL代码,ildasm工具博主有在《.NET高级特性-Emit(1)》中讲到如何使用,我们可以看到仅仅一句话定义Title属性的话,C#为我生成四个东西,分别是

  • Title字段
  • get_Title方法
  • set_Title方法
  • Title属性

 

 

   我们双击查看Title属性,可以看到它的get和set直接链接向get_Title方法和set_Title方法

 

 

 

   之后,我们来观察下get_TItle方法和set_Title方法,结合上一章《.NET高级特性-Emit(2.1)字段》对字段操作,我们很明显的看到,set_Title方法实现了对字段的赋值,而get_Title方法也正好对应了字段的取值

 

 

 

   这就是在IL中呈现的属性的真正样貌,有了IL的理解,我们就能开始我们的Emit之旅了。

三、属性的定义

   属性的定义其实很简单,属性真正的难点是在于如何编写get/set访问器,因为这才是属性的核心逻辑,而且对于自动属性来说我们需要定义字段/get访问器/set访问器和属性本身,所以博主打算用一个方法来实现自动属性的生成,已完成这一个过程的复用

  首先,我们来看一下方法定义,博主使用了扩展方法来为TypeBuilder扩展一个定义自动属性的方法,该方法只需要属性名称和类型,即可创建自动属性需要定义的字段/get访问器/set访问器和属性本身,工欲善其事必先利其器,有了这个方法我们就能快速的创建自动属性

public static PropertyBuilder DefineAutomaticProperty(this TypeBuilder typeBuilder, string propertyName, Type propertyType)
{
    //do something  
}

  (1)然后,我们定义属性的字段,由于是自动属性,所以字段的类型与属性类型相同,名称博主采用下划线+属性小写的方式定义

            var fieldBuilder = typeBuilder.DefineField("_" + propertyName.ToLower(), propertyType, FieldAttributes.Private);

  (2)之后,我们定义属性,这个时候的属性是没有任何get/set访问器的

            var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, Type.EmptyTypes);

  (3)在定义完属性之后,我们开始编写属性的get方法,get方法的内容是读取字段值并返回,如何编写可以参考我的文章《.NET高级特性-Emit(2.1)字段》中字段操作一节

复制代码
            //定义Get方法,返回属性类型,入参无
            var getMethodBuilder = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName, propertyType, Type.EmptyTypes);
            var getIL = getMethodBuilder.GetILGenerator();
            getIL.Emit(OpCodes.Ldarg_0);
            //将字段放入栈顶
            getIL.Emit(OpCodes.Ldfld, fieldBuilder);
            getIL.Emit(OpCodes.Ret);
复制代码

  (4)之后,我们同样定义属性的set方法,内容为读取第一个参数并保存到字段,emit含义同样可以参考上一步get方法的文章

复制代码
            //定义Set方法,返回void,入参一个,类型为属性类型
            var setMethodBuilder = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName, null, new Type[] { propertyType });
            var setIL = setMethodBuilder.GetILGenerator();
            setIL.Emit(OpCodes.Ldarg_0);
            //将第一个参数放入栈顶
            setIL.Emit(OpCodes.Ldarg_1);
            //将栈顶元素弹出并保存到字段
            setIL.Emit(OpCodes.Stfld, fieldBuilder);
            setIL.Emit(OpCodes.Ret);
复制代码

  (5)最后,我们将get和set方法设置为属性的get和set

            propertyBuilder.SetGetMethod(getMethodBuilder);
            propertyBuilder.SetSetMethod(setMethodBuilder);

  (6)返回属性,我们的定义自动属性方法就完成了,完整代码用户可以查看我的github:

            return propertyBuilder;

  在定义自动属性方法中,我们定义了字段/属性/get访问器与set访问器,这样,我们定义Blog类就非常的简单

  首先,我们只要先定义Blog类

            var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
            var moduleBuilder = asmBuilder.DefineDynamicModule("Edwin.Blog.Emit");
            var typeBuilder = moduleBuilder.DefineType("Blog", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.BeforeFieldInit);

  然后直接使用上面我们定义的扩展方法来定义自动属性

            typeBuilder.DefineAutomaticProperty("Title", typeof(string));
            typeBuilder.DefineAutomaticProperty("Content", typeof(string));

  最后创建类型,就完成了我们对Blog类的创建

            typeBuilder.CreateTypeInfo().AsType();

   最后创建并对属性赋值

            dynamic user = Activator.CreateInstance(type);
            user.Title = "Emit高级特性-属性";
            user.Content = "xxx";

  即可在调试窗口看到如下结果

  样例github地址:https://github.com/MJEdwin/edwin-blog-sample/blob/master/Edwin.Blog.Sample/Property/BlogEmit.cs

四、属性的应用-匿名类

  请读者思考,如果我定义一个方法,方法中传入类所需要的属性的名称和它对应的类型,我们是不是就可以根据上述创建Blog的方式来创建一个只包含属性和字段的类,这样的类不就是我们C#当中所说的匿名类了吗?

  想想,我们平常开发当中什么时候使用匿名类居多?博主告诉你,没错就是Mapper,C#当中匿名类存在的意义就是可以实现实体对象到匿名对象的映射,使用最广泛的就是在Linq当中,那么如果我们用Emit来创建匿名类,再在Linq中完成实体类到匿名类的映射,我们我就可以动态DynamicLinq了吗?

  由于篇幅原因以及其中包含了表达式树的原因,故博主就不将代码放在博文当中,有兴趣的读者可以查看我的github了解实现,博主将动态Select的流程图画在下方,知识丰富的小伙伴也可以自行实现。

五、小结

  本章讲解了属性是什么,Emit如何编写属性,以及属性最重要的一个应用-创建匿名类;不积跬步无以至千里,不积小流无以成江海,作为身处软件行业的我们来说,更需要这一份持之以恒的积累,只有不断的积累-思考-积累-思考,才能从量变完成质变,写出更加优秀的代码和软件。

  博主将继续更新.NET高级特性系列,感谢阅读!!!

 

出处:https://www.cnblogs.com/billming/p/emit-study-property.html

=======================================================================================

posted on 2022-01-30 10:30  jack_Meng  阅读(787)  评论(0编辑  收藏  举报

导航