国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

委托

2019-11-14 13:50:45
字體:
來源:轉載
供稿:網友

前言

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

  回調函數是一種非常有用的編程機制,許多語言都對它提供了支持。回調函數是一個通過函數指針調用的函數。通常,我們會把回調函數作為參數傳遞給另一個函數,當某些事件發生或滿足某些條件時,由調用者執行回調函數用于對該事件或條件進行響應。簡單來說,實現回調函數有如下步驟:

  1. 定義一個回調函數。

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

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

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

一、初識委托

  在開始接觸委托前,相信很多人都會感覺它用起來怪怪的,有些別扭。理解它的本質后,就知道許多時候其實是編譯器在背后“搞鬼”;編譯器做了大量的工作,目的是為了減少代碼的編寫以及讓代碼看起來更優雅。接下來就讓我們逐步深入理解委托。

  先看一段簡單的代碼:  

        //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內部的實現機制又是怎樣的?接下來讓我們一一解答。

二、委托揭秘

  上面提到,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方法執行回調函數。

三、委托鏈

  前面說到所有的委托類型都會繼承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[]數組,有了該數組,我們就可以控制具體的執行過程了。

四、泛型委托

  我們可能會在多個地方用到委托,例如在另一個程序集,我們可能會定義一個 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代替。

  有了泛型委托,我們就不用到處定義委托類型了,除非不滿足需求,否則都應該優先使用內置的泛型委托。

五、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提供的更方便的實現方式,但在方法有重用或者實現起來比較復雜的地方,還是推薦重新定義一個方法。

五、委托與反射

  雖然委托類型直接繼承了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系列(還沒寫完,囧)后續也會提到。

  委托和事件經常會聯系在一起,一些面試官也特別喜歡問這個問題。它們之間究竟是一個什么樣的關系,下一篇就對事件展開討論。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 翼城县| 黄浦区| 同仁县| 邵东县| 泗洪县| 巨鹿县| 梓潼县| 海盐县| 渝中区| 平山县| 密山市| 汾阳市| 昌平区| 交城县| 都兰县| 镇雄县| 浦北县| 广德县| 台北县| 泰安市| 通州区| 襄汾县| 铁岭市| 涟源市| 丁青县| 龙江县| 鞍山市| 朝阳县| 敦煌市| 洮南市| 土默特右旗| 福建省| 蒙自县| 共和县| 祥云县| 荆州市| 堆龙德庆县| 永善县| 江安县| 政和县| 文昌市|