.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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM