在上篇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 = ""; 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 = "";
} 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, "");
//取出栈顶元素赋值给字段
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

.NET高级特性-Emit(2.1)字段的更多相关文章

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

    关于Emit的博客已经进入第四篇,在读本篇博文之前,我希望读者能先仔细回顾博主之前所编写的关于Emit的博文,从该篇博文开始,我们就可以真正的使用Emit,并把知识转化为实战,我也会把之前的博文链接放 ...

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

    在上一篇博文发了一天左右的时间,就收到了博客园许多读者的评论和推荐,非常感谢,我也会及时回复读者的评论.之后我也将继续撰写博文,梳理相关.NET的知识,希望.NET的圈子能越来越大,开发者能了解/深入 ...

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

    在这个大数据/云计算/人工智能研发普及的时代,Python的崛起以及Javascript的前后端的侵略,程序员与企业似乎越来越青睐动态语言所带来的便捷性与高效性,即使静态语言在性能,错误检查等方面的优 ...

  4. 你应该知道的Vue高级特性

    本文使用的Vue版本:2.6.10 Vue为我们提供了很多高级特性,学习和掌握它们有助于提高你的代码水平. 一.watch进阶 从我们刚开始学习Vue的时候,对于侦听属性,都是简单地如下面一般使用: ...

  5. mysql笔记04 MySQL高级特性

    MySQL高级特性 1. 分区表:分区表是一种粗粒度的.简易的索引策略,适用于大数据量的过滤场景.最适合的场景是,在没有合适的索引时,对几个分区进行全表扫描,或者是只有一个分区和索引是热点,而且这个分 ...

  6. Redis基础用法、高级特性与性能调优以及缓存穿透等分析

     一.Redis介绍 Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库.缓存服务或消息服务使用.Redis支持多种数据结构,包括字符串.哈希表.链表.集合.有序集合.位图.Hype ...

  7. Redis基础、高级特性与性能调优

    本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍.之后概览Redis提供的高级能力,并在部署.维护.性能调优等多个方面进行更深入的介绍和指导. ...

  8. Redis 基础、高级特性与性能调优

    本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍.之后概览Redis提供的高级能力,并在部署.维护.性能调优等多个方面进行更深入的介绍和指导. ...

  9. 自学Linux Shell19.2-gawk程序高级特性

    点击返回 自学Linux命令行与Shell脚本之路 19.2-gawk程序高级特性 linux世界中最广泛使用的两个命令行编辑器: sed gawk 1. gawk使用变量 编程语言共有的特性是使用变 ...

随机推荐

  1. MyBatis(4)-- 动态SQL

    如果使用JDBC或者类似于Hibernate的其他框架,很多时候要根据需要去拼装SQL,这是一个麻烦的事情.因为某些查询需要许多条件.通常使用其他框架需要大量的Java代码进行判断,可读性比较差,而M ...

  2. 设计模式(十二)Decorator模式

    Decorator模式就是不断地为对象添加装饰的设计模式.以蛋糕为例,程序中的对象就相当于蛋糕,然后像不断地装饰蛋糕一样地不断地对其增加功能,它就变成了使用目的更加明确的对象. 首先看示例程序的类图. ...

  3. 数据结构(四十四)交换排序(1.冒泡排序(O(n²))2.快速排序(O(nlogn))))

    一.交换排序的定义 利用交换数据元素的位置进行排序的方法称为交换排序.常用的交换排序方法有冒泡排序和快速排序算法.快速排序算法是一种分区交换排序算法. 二.冒泡排序 1.冒泡排序的定义 冒泡排序(Bu ...

  4. [springboot 开发单体web shop] 1. 前言介绍和环境搭建

    前言介绍和环境搭建 简述 springboot 本身是为了做服务化用的,我们为什么要反其道使用它来开发一份单体web应用呢? 在我们现实的开发工作中,还有大量的业务系统使用的是单体应用,特别是对于中小 ...

  5. 路由器配置深入浅出—路由器接口PPP协议封装及PAP和CHAP验证配置

    知识域: 是针对点对点专线连接的接口的二层封装协议配置 PPP的PAP和CHAP验证,cpt支持,不一定要在gns3上做实验. 路由器出厂默认是hdlc封装,修改为ppp封装后,可以采用pap验证或者 ...

  6. 深入理解大数据架构之——Lambda架构

    目录 传统系统的问题 Lambda架构简介 Lambda架构关键特性 数据系统的本质 Lambda的三层架构 Lambda架构组件选型 总结 原文链接:https://jiang-hao.com/ar ...

  7. Ubuntu 18.04 下安装pip3及pygame模块

    1.Ubuntu下pip3的安装.升级.卸载 安装pip3 sudo apt-get install python3-pip 升级pip3 sudo pip3 install --upgrade pi ...

  8. 网络安全-主动信息收集篇第二章SNMP扫描

    SNMP扫描: snmp在中大型企业中可以用来做网络管理和网络监控的使用,当开启了snmp简单网络管理后,那么客户机就可以通过这个协议向该设备发送snmp协议内容可以轻松查询到目标主机的相关信息. 以 ...

  9. 和manacher有关的乱写

    当初学kmp hash的时候被教导manacher非常的鸡肋 今天因为一篇神奇的题解我忍不住颓废了两节课把它学了 思路,代码都比较好懂 虽然它不如各种自动机霸气,唯一的功能貌似就是$O(n)$求出所有 ...

  10. NOIP模拟测试25

    这次考试后面心态爆炸了...发现刚了2h的T2是假的之后就扔掉了,草率地打了个骗分 T1只会搜索和m=0 最先做的T3,主要是发现部分分很多,当时第一眼看上去有87分(眼瞎了). 后来想了想,感觉一条 ...