玩轉動態編譯 - 高級篇:二,IL設置靜態屬性,字段和類型轉換


  • 靜態屬性賦值

先來看 Reflector反射出的IL源碼(感謝Moen的提示),這次用 Release模式編譯,去掉那些無用的輔助指令

public void AAA(string s)
{
    MyClass.Name = s;
}
.method public hidebysig instance void AAA(string s) cil managed
{
    .maxstack 8
    //L_0000: ldarg.1 //這個是真正反射出的內容,但是理論上 這里應該是ldarg.0
    //下面一行是我特意修改的,上面的現象我無法解釋,請知道的朋友也可以告知一二
    L_0000: ldarg.0//參數0,也就是string s這個參數推送的堆棧上 
    L_0001: stsfld string blqw.IL.Demo.MyClass::Name//使用當前堆棧上最近的一個參數,執行靜態字段賦值指令,
    L_0006: ret //方法結束
}

  • 小貼士:

每個操作系統都會從堆棧中獲取指定數量的參數,比如上一篇中的靜態字段/屬性取值操作,這個操作不需要用到任何參數,比如執行一個方法,這個方法簽名有幾個參數,就需要提供幾個參數,再比如執行一次比較,需要提供2個參數等等,每個操作需要的參數都是事先就指定好的

再來看C#中的代碼:

public static Action<string> ILTest()
{
    var dm = new DynamicMethod("", null, new[] { typeof(string) }, typeof(MyClass));
    var il = dm.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Stsfld, typeof(MyClass).GetField("Name"));
    il.Emit(OpCodes.Ret);
    return (Action<string>)dm.CreateDelegate(typeof(Action<string>));
}

 

 進行一些測試

static void Main(string[] args)
{
    var act = ILTest();
    MyClass.Name = "111";
    Console.WriteLine(MyClass.Name);
    act("aaaaa");
    Console.WriteLine(MyClass.Name);
}
測試方法
111
aaaaa
請按任意鍵繼續. . .
測試結果

 

靜態屬性賦值

public static Action<string> ILTest()
{
    var dm = new DynamicMethod("", null, new[] { typeof(string) }, typeof(MyClass));
    var il = dm.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Call, typeof(MyClass).GetProperty("Name").GetSetMethod());
    il.Emit(OpCodes.Ret);
    return (Action<string>)dm.CreateDelegate(typeof(Action<string>));
}
  • 類型轉換

之前看的栗子都是方法參數類型和屬性字段類型相同的情況下的賦值,那么如果類型需要轉換呢?

比如這樣: 將MyClass的Name屬性的類型改為int

class MyClass
{
    public static int Name { get; set; }
}

 

然后把要生成的方法改為傳入Object類型的參數進行復制,但是在調用的時候依然傳入int

static void Main(string[] args)
{
    var act = ILTest();
    act(222);
}

public static Action<object> ILTest()
{
    var dm = new DynamicMethod("", null, new[] { typeof(object) }, typeof(MyClass));
    var il = dm.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Call, typeof(MyClass).GetProperty("Name").GetSetMethod());
    il.Emit(OpCodes.Ret);
    return (Action<object>)dm.CreateDelegate(typeof(Action<object>));
}

 

 

注意加了下划線的幾個地方

再來看運行結果

static void Main(string[] args)
{
    var act = ILTest();
    MyClass.Name = 111;
    Console.WriteLine(MyClass.Name);
    act(222);
    Console.WriteLine(MyClass.Name);
}
測試代碼
111
50310368
請按任意鍵繼續. . .

 

雖然程序沒有拋出異常,但是結果確錯了...

其實這個時候IL相當於生成了一個這樣的方法

public void AAA(object i)
{
    MyClass.Name = i; //編譯器在這里就報錯了
}

 

如果這個方法你是在VS中寫的,那么在編譯的時候編譯器就告訴你這樣寫是錯誤,並且中斷你的程序編譯

編譯器希望你改成這樣

public void AAA(object i)
{
    MyClass.Name = (int)i;//增加類型轉換 
}

 

雖然這樣寫,在傳入參數錯誤的情況下也會拋出異常,但是這是運行時的錯誤,就跟程序本身沒關系了

所以IL代碼也需要加一個轉換的操作

public static Action<object> ILTest()
{
    var dm = new DynamicMethod("", null, new[] { typeof(object) }, typeof(MyClass));
    var il = dm.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Unbox_Any, typeof(int));//加上這一句拆箱操作
    il.Emit(OpCodes.Call, typeof(MyClass).GetProperty("Name").GetSetMethod());
    il.Emit(OpCodes.Ret);
    return (Action<object>)dm.CreateDelegate(typeof(Action<object>));
}

 

再來看運行結果

 

好了,這回就對了

  • 重點:

別看我們在C#代碼中類型轉換操作都是一樣的(Type)Object,但是在IL中值類型和引用類型會被編譯成不同的指令,原因就是大家都知道的,值類型和引用類型的儲存方式和位置不同引起的

object轉值類型被稱為拆箱,對應指令是OpCodes.Unbox_AnyOpCodes.Unbox(我不知道區別,一般都是用OpCodes.Unbox_Any)

object轉引用類型就是強轉,對應指令是OpCodes.Castclass

上面2個指令都需要提供一個指令參數,如il.Emit(OpCodes.Castclass, typeof(int));表示拆箱后的類型,或強轉后的類型

值類型轉為object稱為裝箱,對應指令是OpCodes.Box,不需要提供指令參數

引用類型轉object,也是強轉,對應指令是OpCodes.Castclass

所以其實我們昨天的栗子中有一部分也是需要改正的,在獲取靜態屬性和字段值的時候,返回值的object,但是IL中沒有進行轉換

public static Func<object> ILTest()
{
    //編譯li動態方法
    //1.聲明動態方法對象DynamicMethod
    //  第一個參數是方法名,可以為空字符,不可以是null
    //  第二個參數是動態方法返回值類型
    //  第三個參數是Type[],表示方法參數,如果沒有參數可以是null
    //  第四個參數是聲明邏輯關聯類,可以讓動態方法訪問該類有權訪問所有字段,包括邏輯關聯類的私有字段
    var dm = new DynamicMethod("", typeof(object), null, typeof(MyClass));
    //2.聲明il編譯器
    var il = dm.GetILGenerator();
    //3.執行MyClass類的Name屬性的Get方法 這句對應剛才的L_0001
    il.Emit(OpCodes.Call, typeof(MyClass).GetProperty("Name").GetGetMethod());
    //4.方法結束,這句對應剛才的L_000a
    il.Emit(OpCodes.Ret);
    //5.由il編譯器創建指定類型的動態方法委托
    return (Func<object>)dm.CreateDelegate(typeof(Func<object>));
}
昨天的栗子

 

修改后就應該變成這樣:

public static Func<object> ILTest()
{
    var dm = new DynamicMethod("", typeof(object), null, typeof(MyClass));
    var il = dm.GetILGenerator();
    var MyClass_Name = typeof(MyClass).GetProperty("Name");
    il.Emit(OpCodes.Call, MyClass_Name.GetGetMethod());
    if (MyClass_Name.PropertyType.IsValueType)//判斷屬性類型是否是值類型
    {
        il.Emit(OpCodes.Box);//如果是值類型就裝箱
    }
    else
    {
        il.Emit(OpCodes.Castclass, typeof(object));//引用類型就強轉為object
    }
    il.Emit(OpCodes.Ret);
    return (Func<object>)dm.CreateDelegate(typeof(Func<object>));
}

 

  • 轉型的注意事項

在轉型雖然不是必須的,但最好是帶上,不然你不知道什么時候會出現一些莫名其妙的錯誤

比如下面這個栗子:

錯誤的栗子

當把強轉加上之后

雖然會導致程序異常,但至少比剛才那種情況好多了不是嗎

  • 下篇預告

實例屬性/字段的讀取與設置,及其實用價值


免責聲明!

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



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