Emit,更高級(jí)的反射用法(C#)

只要寫過幾年C#诺苹,你就不可能沒用過反射咕晋。

如果要通過反射調(diào)用函數(shù),最廣泛的用法是使用Invoke收奔。

比Invoke高效一些的做法是使用表達(dá)式預(yù)先編譯一個(gè)委托掌呜,之后通過委托進(jìn)行調(diào)用。

我此前做過一個(gè)粗略的測(cè)試來對(duì)比函數(shù)的直接調(diào)用坪哄、表達(dá)式委托质蕉、Invoke調(diào)用三者之間的性能差異。發(fā)現(xiàn)表達(dá)式方式比直接調(diào)用慢70倍翩肌,Invoke比直接調(diào)用慢2000倍模暗。

當(dāng)然,這只是在我的機(jī)器上跑出來的數(shù)據(jù)念祭,不同的機(jī)器和不同的Framework版本可能會(huì)有不同的結(jié)果兑宇。

但是無論如何,可以看到Invoke方式是有很大的性能損失的粱坤。

為了挽救這種性能損失隶糕,可以考慮使用Emit技術(shù)祝钢。

Emit可以使用MSIL指令動(dòng)態(tài)編寫程序邏輯,然后將指令編譯成程序集若厚。

之后執(zhí)行起來就跟原生調(diào)用沒什么區(qū)別了拦英,自然也就不會(huì)有太大的性能損失,這樣既保證了靈活性测秸,又保證了程序性能疤估。

來一個(gè)最簡(jiǎn)單的示例,感受一下Emit的用法:


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

namespace EmitDemo
{
    public interface IConsole
    {
        void Say();
    }

    class Example1
    {
        public static void Run()
        {
            // 在看下面的代碼之前霎冯,先明白這個(gè)依賴關(guān)系铃拇,即:
            // 方法->類型->模塊->程序集


            //定義程序集的名稱
            AssemblyName aName = new AssemblyName("DynamicAssemblyExample");

            // 創(chuàng)建一個(gè)程序集構(gòu)建器
            // Framework應(yīng)該這樣:AppDomain.CurrentDomain.DefineDynamicAssembly
            AssemblyBuilder ab =
                AssemblyBuilder.DefineDynamicAssembly(
                    aName,
                    AssemblyBuilderAccess.Run);


            // 使用程序集構(gòu)建器創(chuàng)建一個(gè)模塊構(gòu)建器
            ModuleBuilder mb = ab.DefineDynamicModule(aName.Name + ".dll");

            // 使用模塊構(gòu)建器創(chuàng)建一個(gè)類型構(gòu)建器
            TypeBuilder tb = mb.DefineType("DynamicConsole");

            // 使類型實(shí)現(xiàn)IConsole接口
            tb.AddInterfaceImplementation(typeof(IConsole));

            var attrs = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.HideBySig | MethodAttributes.Final;

            // 使用類型構(gòu)建器創(chuàng)建一個(gè)方法構(gòu)建器
            MethodBuilder methodBuilder = tb.DefineMethod("Say", attrs, typeof(void), Type.EmptyTypes);

            // 通過方法構(gòu)建器獲取一個(gè)MSIL生成器
            var IL = methodBuilder.GetILGenerator();

            // 開始編寫方法的執(zhí)行邏輯

            // 將一個(gè)字符串壓入棧頂
            IL.Emit(OpCodes.Ldstr, "I'm here.");

            // 調(diào)用Console.Writeline函數(shù)
            IL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));

            // 退出函數(shù)
            IL.Emit(OpCodes.Ret);

            //方法結(jié)束


            // 從類型構(gòu)建器中創(chuàng)建出類型
            var dynamicType = tb.CreateType();


            // 通過反射創(chuàng)建出動(dòng)態(tài)類型的實(shí)例
            var console = Activator.CreateInstance(dynamicType) as IConsole;

            console.Say();

            Console.ReadLine();
        }
    }
}

這段代碼在.net core下可正常運(yùn)行, 在Framework下有部分代碼需要調(diào)整沈撞,我已在注釋中給出備注慷荔。

代碼略長(zhǎng),不過概括起來說缠俺,這段代碼主要做了這些事情:

動(dòng)態(tài)的創(chuàng)建了一個(gè)程序集显晶,然后在程序集中定義了模塊和類。然后讓這個(gè)類實(shí)現(xiàn)了IConsole接口壹士,之后實(shí)現(xiàn)了Say方法磷雇。Say方法調(diào)用了靜態(tài)方法“Console.WriteLine”,最終輸出了一段字符躏救。

示例程序最核心的代碼是第48到59行唯笙,這幾句代碼使用MSIL指令編寫了一段函數(shù)邏輯,理解這些指令是掌握Emit的關(guān)鍵盒使。

通過上面這個(gè)示例可以習(xí)得動(dòng)態(tài)創(chuàng)建程序集的基本步驟崩掘,即:

定義Assembly,在Assembly中定義Module少办,在Module中定義Type苞慢,在Type中定義方法,之后使用IL編寫方法的執(zhí)行邏輯凡泣。

下面再來看一個(gè)復(fù)雜一些的示例枉疼,更多的了解一些常用的IL指令的用法。


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

namespace EmitDemo
{
    class Example2
    {
        public static void Run()
        {
            AssemblyName aName = new AssemblyName("ChefDynamicAssembly");

            AssemblyBuilder ab =
                AssemblyBuilder.DefineDynamicAssembly(
                    aName,
                    AssemblyBuilderAccess.Run);

            ModuleBuilder mb = ab.DefineDynamicModule(aName.Name + ".dll");

            TypeBuilder tb = mb.DefineType("Commander");

            var attrs = MethodAttributes.Public;

            // 使用類型構(gòu)建器創(chuàng)建一個(gè)方法構(gòu)建器
            MethodBuilder methodBuilder = tb.DefineMethod("Do", attrs, typeof(string), Type.EmptyTypes);

            // 通過方法構(gòu)建器獲取一個(gè)MSIL生成器
            var IL = methodBuilder.GetILGenerator();

            // 開始編寫方法的執(zhí)行邏輯

            // var vegetables = new string[3];
            var vegetables = IL.DeclareLocal(typeof(string[]));
            IL.Emit(OpCodes.Ldc_I4, 3);
            IL.Emit(OpCodes.Newarr, typeof(string));
            IL.Emit(OpCodes.Stloc, vegetables);

            //vegetables[0] = "土豆"
            IL.Emit(OpCodes.Ldloc, vegetables);
            IL.Emit(OpCodes.Ldc_I4, 0);
            IL.Emit(OpCodes.Ldstr, "土豆");
            IL.Emit(OpCodes.Stelem, typeof(string));

            //vegetables[1] = "青椒"
            IL.Emit(OpCodes.Ldloc, vegetables);
            IL.Emit(OpCodes.Ldc_I4, 1);
            IL.Emit(OpCodes.Ldstr, "青椒");
            IL.Emit(OpCodes.Stelem, typeof(string));

            //vegetables[2] = "木耳"
            IL.Emit(OpCodes.Ldloc, vegetables);
            IL.Emit(OpCodes.Ldc_I4, 2);
            IL.Emit(OpCodes.Ldstr, "木耳");
            IL.Emit(OpCodes.Stelem, typeof(string));

            // IChef chef = new GoodChef();
            var chef = IL.DeclareLocal(typeof(IChef));
            IL.Emit(OpCodes.Newobj, typeof(GoodChef).GetConstructor(Type.EmptyTypes));
            IL.Emit(OpCodes.Stloc, chef);

            //var dish = chef.Cook(vegetables);
            var dish = IL.DeclareLocal(typeof(string));
            IL.Emit(OpCodes.Ldloc, chef);
            IL.Emit(OpCodes.Ldloc, vegetables);
            IL.Emit(OpCodes.Callvirt, typeof(IChef).GetMethod("Cook"));
            IL.Emit(OpCodes.Stloc, dish);

            // return dish;
            IL.Emit(OpCodes.Ldloc, dish);
            IL.Emit(OpCodes.Ret);

            //方法結(jié)束


            // 從類型構(gòu)建器中創(chuàng)建出類型
            var dynamicType = tb.CreateType();


            // 通過反射創(chuàng)建出動(dòng)態(tài)類型的實(shí)例
            var commander = Activator.CreateInstance(dynamicType);

            Console.WriteLine(dynamicType.GetMethod("Do").Invoke(commander, null).ToString());

            Console.ReadLine();
        }
    }

    public interface IChef
    {
        string Cook(string[] vegetables);
    }

    public class GoodChef : IChef
    {
        public string Cook(string[] vegetables)
        {
            return "good:" + string.Join("+", vegetables);
        }
    }
}

這個(gè)示例有如下輸出:

good:土豆+青椒+木耳

除開一些基礎(chǔ)設(shè)施的定義鞋拟,以第30至72行為主要關(guān)注點(diǎn)骂维。

代碼其實(shí)沒有什么復(fù)雜的邏輯,就是定義了一個(gè)數(shù)組贺纲,創(chuàng)建一個(gè)實(shí)現(xiàn)了IChef的接口的實(shí)例航闺,將數(shù)組傳遞給實(shí)例的Cook方法,獲得輸出后返回。

不過潦刃,邏輯雖然簡(jiǎn)單侮措,但是卻展示了多種常用的IL指令的使用方法。例如乖杠,創(chuàng)建數(shù)組分扎,給數(shù)組賦值,創(chuàng)建對(duì)象胧洒,調(diào)用實(shí)例方法等等……

每一段IL指令對(duì)應(yīng)的C#語法都已在注釋中進(jìn)行了說明畏吓,建議對(duì)照起來看。

對(duì)于這些指令的具體用法卫漫,建議參考MSDN菲饼。要是MSDN看的云里霧里,可以看一下文章末尾介紹的MSDN查閱指南列赎。

另外宏悦,我再介紹兩點(diǎn)編程技巧:

第一、如果不知道某些語句怎么寫包吝,可以先把你想要寫的邏輯用C#表達(dá)出來之后編譯成DLL饼煞,之后使用微軟提供的ILDAsm(VS自帶)工具打開這個(gè)DLL查看具體的IL指令,然后對(duì)照著翻譯成Emit IL漏策。

第二派哲、如果要驗(yàn)證Emit生成的DLL是否是自己想要表的的邏輯臼氨,可以使用ILSpy反編譯工具將生成的DLL反編譯出來看一看具體的邏輯掺喻。

實(shí)際應(yīng)用中,除了使用Emit來提升反射的執(zhí)行效率外储矩,還可以用它來實(shí)現(xiàn)動(dòng)態(tài)代理或者應(yīng)用到AOP編程技術(shù)中感耙,可謂不得不掌握的C#高級(jí)編程技術(shù)。


MSDN查閱指南之OpCodes

MSDN找到System.Reflection.Emit命名空間持隧,找到OpCodes類即硼,定位到Fields部分。

這里可以全局總覽所有的IL指令屡拨,可以大致瀏覽一下每個(gè)指令的作用只酥。

然后選擇一個(gè)指令進(jìn)入子頁面查看對(duì)該指令的詳細(xì)介紹。

接下來以Call指令為例進(jìn)行介紹呀狼。

這里大致描述了這個(gè)指令的作用
Remarks是關(guān)注的重點(diǎn)裂允,這里描述了參數(shù)和棧的操作,想要正確的使用指令全靠這里了哥艇。
隨著Remarks往下翻绝编,可以看到具體的調(diào)用范式。

好了,正文就啰嗦到這里吧十饥。


在開始認(rèn)真寫文章之前總覺得沒什么可寫的窟勃,但是一旦開始之后總覺得有太多的東西可以寫。

所以說逗堵,思路是需要整理的秉氧。

謝謝觀看,點(diǎn)個(gè)贊唄蜒秤?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谬运,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子垦藏,更是在濱河造成了極大的恐慌梆暖,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂骏,死亡現(xiàn)場(chǎng)離奇詭異轰驳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)弟灼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門级解,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人田绑,你說我怎么就攤上這事勤哗。” “怎么了掩驱?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵芒划,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我欧穴,道長(zhǎng)民逼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任涮帘,我火速辦了婚禮拼苍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘调缨。我一直安慰自己疮鲫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布弦叶。 她就那樣靜靜地躺著俊犯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪湾蔓。 梳的紋絲不亂的頭發(fā)上瘫析,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼贬循。 笑死咸包,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杖虾。 我是一名探鬼主播烂瘫,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奇适!你這毒婦竟也來了坟比?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤嚷往,失蹤者是張志新(化名)和其女友劉穎葛账,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皮仁,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡籍琳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贷祈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趋急。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖势誊,靈堂內(nèi)的尸體忽然破棺而出呜达,到底是詐尸還是另有隱情,我是刑警寧澤粟耻,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布查近,位于F島的核電站,受9級(jí)特大地震影響勋颖,放射性物質(zhì)發(fā)生泄漏嗦嗡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一饭玲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叁执,春花似錦茄厘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吆录,卻和暖如春窑滞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工哀卫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巨坊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓此改,卻偏偏與公主長(zhǎng)得像趾撵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子共啃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344