1. 程式人生 > >.NET高階特性-Emit(2.1)欄位

.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