委託

前言html

  委託和事件是c#基礎中兩個重要的知識,平時工做中也會常常用到。接下來我會寫兩篇我對委託和事件的理解,歡迎拍磚。面試

  回調函數是一種很是有用的編程機制,許多語言都對它提供了支持。回調函數是一個經過函數指針調用的函數。一般,咱們會把回調函數做爲參數傳遞給另外一個函數,當某些事件發生或知足某些條件時,由調用者執行回調函數用於對該事件或條件進行響應。簡單來講,實現回調函數有以下步驟:編程

  1. 定義一個回調函數。c#

  2. 將回調函數指針註冊給調用者。數組

  3. 在某些事件或條件發生時,調用者經過函數指針調用回調函數對事件進行處理。緩存

  回調機制的應用很是多,例如控件事件、異步操做完成通知等等;.net 經過委託來實現回調函數機制。相比其餘平臺的回調機制,委託提供了更多的功能,例如它確保回調方法是類型安全的,支持順序調用多個方法,以及調用靜態方法和實例方法。安全

1、初識委託異步

  在開始接觸委託前,相信不少人都會感受它用起來怪怪的,有些彆扭。理解它的本質後,就知道許多時候實際上是編譯器在背後「搞鬼」;編譯器作了大量的工做,目的是爲了減小代碼的編寫以及讓代碼看起來更優雅。接下來就讓咱們逐步深刻理解委託。函數

  先看一段簡單的代碼:  工具

        //1.定義一個委託類型
        delegate void TestDelegate(int value);

        static void Main(string[] args)
        {   
            //2.傳遞null
            ExecuteDelegate(null, 10);
   
            //3.調用靜態方法
            TestDelegate test1 = new TestDelegate(StaticFunction);
            ExecuteDelegate(test1, 10);

            //4.調用實例方法
            Program program = new Program();
            TestDelegate test2 = new TestDelegate(program.InstanceFunction);
            ExecuteDelegate(test2, 10);

            //5.調用多個方法
            TestDelegate test3 = (TestDelegate)Delegate.Combine(test1, test2);
            ExecuteDelegate(test3, 10);
        }

        //靜態方法
        static void StaticFunction(int value)
        {
            Console.WriteLine("Call StaticFunction: " + value.ToString());
        }

        //實例方法
        void InstanceFunction(int value)
        {
            Console.WriteLine("Call InstanceFunction: " + value.ToString());
        }

        //執行委託
        static void ExecuteDelegate(TestDelegate tg, int value)
        {
            if (tg != null)
            {
                tg(value);
            }
        }

  第1步,用delegate關鍵字定義了一個委託類型,名稱爲TestDelegate。它的簽名爲:1. 返回值爲void 2. 有一個int類型的參數。回調函數的簽名必須與之同樣,不然編譯會報錯。

  第2步,調用執行委託的方法並傳遞了null,實際上什麼也沒作。這裏說明了委託能夠做爲參數,能夠爲null,彷佛與引用類型類似。

  第3步,用 new 建立了一個TestDelegate的變量test1, 並將靜態方法做爲參數,它符合委託的簽名。經過new 來建立,咱們基本能夠推測TestDelegate是一個引用類型。

  第4步,與3相似,只不過它傳遞的參數是一個實例方法,因此須要先建立方法的對象Program。

  第5步,調用了Delegate.Combine()方法,經過名稱能夠指定它用於將多個委託組合起來,調用test3時,會按照它的參數順序執行全部方法。這種方式有時候很是有用,由於咱們極可能在某個事件發生時,要執行多個操做。  

  經過上面的代碼,咱們基本能夠知道委託是用來包裝回調函數的,對回調函數的調用實際上是經過委託來實現的,這也是很符合【委託】的稱呼。那麼委託究竟是一種什麼樣的類型?爲何它能夠將函數名稱做爲參數?爲何能夠像tg(value)這樣來執行?Delegate.Combine內部的實現機制又是怎樣的?接下來讓咱們一一解答。

2、委託揭祕

  上面提到,c#編譯器爲了簡化代碼的編寫,在背後作了不少處理。委託的確是一種用來包裝函數的引用類型,當咱們用delegate定義上面的委託時,編譯器會爲咱們生成一個class TestDelegate的類,這個類就是用來包裝回調函數的。經過ILDasm.exe查看上面的IL代碼能夠很清晰看到這個過程:

  能夠看到,編譯器爲咱們生成了一個 TestDelegate  的class 類型,而且它還繼承了MulticastDelegate。實際上,全部的委託都會繼承MulticastDelegate,而MulticastDelegate又繼承了Delegate。Delegate有2個重要的非公共字段:

1. _target: object類型,當委託包裝的是實例方法時,這個字段引用的是實例方法的對象;若是是靜態方法,這個字段就是null。

2. _methodPtr: IntPtr類型,一個整數值,用於標識回調方法。

因此對於實例方法,委託就是經過實例對象去調用所包裝的方法的。Delegate還公開了兩個屬性,Target和Method分別表示實例對象(靜態方法爲null)和包裝函數的元信息。

  能夠看到通過編譯器編譯後生成的這個類有4個函數,.ctor(構造函數),BeginInvoke, EndInvoke, Invoke。BeginInvoke/EndInvoke 是Invoke的異步版本,因此咱們主要關注.ctor和Invoke函數。

  .ctor構造函數有兩個參數,一個object類型,一個int類型。但當咱們new一個委託對象時,傳遞倒是一個方法的名稱。實際上,編譯器知道咱們要構造的是委託對象,因此會分析源代碼知道要調用的是哪一個對象和方法;對象引用就是做爲第一個參數(若是靜態就爲null),而從元數據獲取用於標識函數的特殊值就做爲第二個參數,從而調用構造函數。這兩個參數分別保存在 _target 和 _methodPth字段中。

  Invoke 函數顧名思義就是用來調用函數的,當咱們執行tg(value)時,編譯器發現tg引用的是一個委託對象,因此生成的代碼就是調用委託對象的Invoke方法,該方法的簽名與咱們簽名定義的簽名是一致的。生成的IL代碼如: callvirt  instance void TestDelegate2.Program/TestDelegate::Invoke(int32)。

  至此,咱們知道定義委託就是定義類,這個類用來包裝回調函數。經過該類的Invoke方法執行回調函數。

3、委託鏈

  前面說到全部的委託類型都會繼承MulticastDelegate。MulticastDelegate表示多路廣播委託,其調用列表能夠擁有多個委託,咱們稱之爲委託鏈。簡單的說,它擁有一個委託列表,咱們能夠順序調用裏面全部方法。經過源碼可知,MulticastDelegate有一個_invocationList字段,用於引用一個委託對象數組;咱們能夠經過Delegate.Combine將多個委託添加到這個數組當中,既然有Combine就會有Remove,對應用來從委託鏈中移除指定的委託。接下來咱們來看這個具體的過程。以下代碼:

            TestDelegate test1 = new TestDelegate(StaticFunction); //1
            TestDelegate test2 = new TestDelegate(StaticFunction); //2
            TestDelegate test3 = new TestDelegate(new Program().InstanceFunction); //3
            TestDelegate result = (TestDelegate)Delegate.Combine(test1, test2); //4
            result = (TestDelegate)Delegate.Combine(result, test3); //5
            Delegate.Remove(result, test1); //6

  當執行1~3行時,會建立3個TestDelegate對象,以下所示:

  

  執行第4行時,會經過Delegate.Combine建立一個具備委託鏈的TestDelegate對象,該對象的_target和_methodPtr已經不是咱們想關注的了,_invocationList引用了一個數組對象,數組有test1,test2兩個元素。以下:

  

  執行第5行代碼時,一樣會從新建立一個具備委託鏈的TestDelegate對象,此時_invocationList具備3個元素。須要注意的是,因爲Delegate.Combine(或者Remove)每一次都會從新建立委託對象,因此第4行的result引用的對象再也不被引用,此時它能夠被回收了。如:

  執行Remove時,與Combine相似,都會從新建立委託對象,此時從數組移除test1委託對象,這裏就不在重複。

  經過上面的分析,咱們知道調用方法實際就是調用委託對象的Invoke方法,若是_invocationList引用了一個數組,那麼它會遍歷這個數組,並執行全部註冊的方法;不然執行_methodPtr方法。Invoke僞代碼看起來也許像下面這樣:

        public void Invoke(Int32 value)
        {
            Delegate[] delegateSet = _invocationList as Delegate[];
            if (delegateSet != null)
            {
                foreach (var d in delegateSet)
                {
                    d(value);
                }
            }
            else
            {
                _methodPtr.Invoke(value);
            }
        }

  _invocationList畢竟是內部字段,默認狀況下會按順序調用,但有時候咱們想控制這個過程,例如按某些條件執行或者記錄異常等。MulticastDelegate有一個GetInvocationList()方法,用於獲取Delegate[]數組,有了該數組,咱們就能夠控制具體的執行過程了。

4、泛型委託

  咱們可能會在多個地方用到委託,例如在另外一個程序集,咱們可能會定義一個 delegate void AnotherDelegate(int value); 這個委託的簽名和簽名的是同樣的。實際上.net內部就有許多這樣的例子,平時咱們也常常看到。例如:

            public delegate void WaitCallback(object state);
            public delegate void TimerCallback(object state);
            public delegate void ParameterizedThreadStart(object obj);

  上面只是這種簽名的形式,另一種形式也可能出現大量的重複,這將給代碼維護帶來很大的難度。泛型委託就是爲了解決這個問題的。

  .net 已經定義了三種類型的泛型委託,分別是 Predicate、Action、Func。在使用linq的方法語法中,咱們會常常遇到這些類型的參數。

  Action 從無參到16個參數共有17個重載,用於分裝有輸入值而沒有返回值的方法。如:delegate void Action<T>(T obj);

  Fun 從無參到16個參數共有17個重載,用於分裝有輸入值並且有返回值的方法。如:delegate TResule Func<T>(T obj);

  Predicate 只有一種形式:public delegate bool Predicate<T>(T obj)。用於封裝傳遞一個對象而後判斷是否知足某些條件的方法。Predicate也能夠用Func代替。

  有了泛型委託,咱們就不用處處定義委託類型了,除非不知足需求,不然都應該優先使用內置的泛型委託。

5、c#對委託的支持

5.1 +=/-= 操做符

  c#編譯器自動爲委託類型重載了 += 和 -= 操做符,簡化編碼。例如要添加一個委託對象到委託鏈中,咱們也能夠 test1 += test2; 編譯器能夠理解這種寫法,實際上這樣寫和調用test1 = Delegate.Combine(test1, test2) 生成的 IL 代碼是同樣的。

5.2 不須要構造委託對象

  在一個須要使用委託對象的地方,咱們沒必要每次都new 一個,只傳遞要包裝的函數便可。例如:test1 += StaticFunction; 或者 ExecuteDelegate(StaticFunction, 10);都是直接傳遞函數。編譯器能夠理解這種寫法,它會自動幫咱們new 一個委託對象做爲參數。

5.3 不須要定義回調方法

  有時候回調方法只有很簡單的幾行,爲了代碼更緊湊和方便閱讀,咱們不想要定義一個方法。這個時候可使用匿名方法,如:

ExecuteDelegate(delegate { Console.WriteLine("使用匿名方法"); }, 10);

  匿名方法也是用delegate關鍵字修飾的,形式爲 delegate(參數){方法體}。匿名方法是c#2.0提供的,c#3.0提供了更優雅的lambda表達式來代替匿名方法。如:

ExecuteDelegate(obj => Console.WriteLine("使用lambda表達式"), 10);

  實際上編譯器發現方法的形參是一個委託,而咱們傳遞了lambda表達式,編譯會嘗試隨機爲咱們生成一個外部不可見的特殊方法,本質上仍是在源碼中定義了一個新的方法,咱們能夠經過反編譯工具看到這個行爲。lambda提供的更方便的實現方式,但在方法有重用或者實現起來比較複雜的地方,仍是推薦從新定義一個方法。

5、委託與反射

  雖然委託類型直接繼承了MulticastDelegate,但Delegate提供了許多有用的方法,實際上這兩個都是抽象類,只要提供一個便可,多是.net設計的問題,搞了兩個出來。Delegate提供了CreateDelegate 和 DynamicInvoke兩個關於反射的方法。CreateDelegate提供了多種重載方式,具體能夠查看msdn;DynamicInvoke參數數一個可變的object數組,這就保證了咱們能夠在對參數未知的狀況下對方法進行調用。如:

            MethodInfo methodInfo = typeof(Program).GetMethod("StaticFunction", BindingFlags.Static | BindingFlags.NonPublic);
            Delegate funcDelegate = Delegate.CreateDelegate(typeof(Action<int>), methodInfo);
            funcDelegate.DynamicInvoke(10);

  這裏咱們只須要知道方法的名稱(靜態或實例)和委託的類型,徹底不用知道方法的參數個數、具體類型和返回值就能夠對方法進行調用。

  反射能夠帶來很大靈活性,但效率一直是個問題。有幾種方式能夠對其進行優化。基本就是:Delegate.DynamicInvoke、Expression(構建委託) 和 Emit。從上面能夠看到,DynamicInvoke的方式仍是須要知道委託的具體類型(Action<int>部分),而不能直接從方法的MethodInfo元信息直接構建委託。當在知道委託類型的狀況下,這種狀況下是最簡單的實現方式。

  使用委託+緩存來優化反射是我比較喜歡的方式,相比另外兩種作法,能夠兼顧效率和代碼的可讀性。具體的實現方式你們能夠在網上找,或者參考個人Ajax系列(還沒寫完,囧)後續也會提到。

  委託和事件常常會聯繫在一塊兒,一些面試官也特別喜歡問這個問題。它們之間到底是一個什麼樣的關係,下一篇就對事件展開討論。

相關文章
相關標籤/搜索