什么是委托? -> 初識委托
在很多應用程序中(C,C++),需要對象使用某種回調機制,能夠與創(chuàng)建它 的實體進行通信,在.NET平臺下,通過委托來提供了一種回調函數機制,在.NET平臺下,委托確?;卣{函數是類型安全的(這也正是.NET FreamWork與非托管代碼的區(qū)別)。本質上來講,委托是一個類型安全的對象,它指向程序中另一個以后會被調用的方法(或多個方法),就是將方法作為 參數來傳遞.
C#中定義委托類型
在C#中創(chuàng)建一個委托類型時,需要使用關鍵字 delegate 關鍵字,類型名可以自定義,但是 委托要指定一個回調方法的簽名.
1 //聲明一個委托,該委托可以指向任何傳入兩個Int類型并且方法的返回值為Int.2 public delegate int Binary(int x,int y);3 4 //聲明一個委托,該委托可以指向任何傳入一個String類型并且方法返回值為Void5 public delegate void DelegateBackCall(string str);
使用委托發(fā)送對象狀態(tài)通知 -> 用委托回調靜態(tài)方法
先來看一段代碼:
void Main()
{
Program.Main();
}
public delegate void DelegateBackCall(int value);
class Program
{
public static void Main()
{
Counter(1,4,null);
Counter(1,4,new DelegateBackCall(StaticDelegateToConsole));
}
private static void Counter(int x,int y,DelegateBackCall foo) {
for(var i = x;i <= y;i++)
{
if(foo != null)
foo(i);21
}
}
private static void StaticDelegateToConsole(int num)
{
Console.WriteLine("Item : " + num);
}
}
復制代碼
首先 定義了一個名字為DelegateBackCall委托,該委托指定的方法要獲取Int類型參數,返回void,在Program類中定義了私有的靜態(tài)方 法Counter,用來統(tǒng)計X到Y之間整數的個數,同時呢,Counter方法還獲取一個Foo,Foo是對一個DelegateBackCall委托對 象的引用, 在方法體中 我們首先遍歷一下,如果Foo不為null,就調用Foo變量所指定的回調函數.那么傳入的這個回調函數的是正在處理的那個數據項的值.
然后 在Program的Main函數中,第一次調用Counter時,第三個參數傳遞的是Null,所在在Counter函數中是不會執(zhí)行回調函數的.
接著第二次調用Counter函數時,給第三個參數傳遞一個新構造的 DelegateBackCall委托對象(其實在此委托對象是方法的一個包裝器, 使方法能通過包裝器來間接的進行回調),然后靜態(tài)方法StaticDelegateToConsole被傳給DelegateBackCll委托類型的構 造器(StaticDelegateToConsole就是要包裝的方法),當Counter執(zhí)行時,會在遍歷每個數據項之后調用靜態(tài)方法 StaticDelegateToConsole,最后輸出結果:
1 Result:2 Item : 1 3 Item : 24 Item : 3 5 Item : 4
使用委托發(fā)送對象狀態(tài)通知 -> 用委托調用實例方法
首先還是來看Main()函數中,第三個調用Counter的地方,在此將代碼貼出來吧.
復制代碼
1 class Program 2 { 3 public static void Main() 4 { 5 Counter(1,4,null); 6 Counter(1,4,new DelegateBackCall(StaticDelegateToConsole)); 7 8 Counter(1,4,new DelegateBackCall(new Program().InstanceDelegateToMessage)); 9 }10 private void InstanceDelegateToMessage(int num)11 {12 Console.WriteLine("Message : " + num);13 }14 }
復制代碼
在第三次調用Counter函數時,第三個參數傳遞的是Program創(chuàng)建的實例 方法,這將委托包裝對InstanceDelegateToMessage方法的一個引用,該方法是一個實例方法.同調用靜態(tài)的一樣,當Counter調 用Foo回調的時候會調用InstanceDelegateToMessage實例方法,新構造的對象將作為隱式的this參數傳給這個實例方法.
最后的輸出結果為:
1 Message : 12 Message : 23 Message : 34 Message : 4
通過上面兩個例子我們知道委托可以包裝對實例方法和靜態(tài)方法的調用,如果是實例方法,那么委托需要知道方法操作的是哪個對象實例(包裝實例是很有用的,對象內部的代碼可以訪問對象的實例成員,這意味著對象可以維護一些狀態(tài),并在回調方法執(zhí)行期間利用這些狀態(tài)信心).
委托的協(xié)變和逆變
將一個方法綁定到委托時,C#和CLR都允許引用類型的協(xié)變和逆變
協(xié)變:方法的返回類型是從委托的返回類型派生的一個類型
逆變:方法獲取的參數類型是委托參數類型的基類.
例如:
1 delegate object CallBack(FileStream file);2 3 string SomeMethod(Stream stream);
在上面代碼中,SomeMethod的返回類型(string)派生自委托的返回類型(object),這樣協(xié)變是可以的.
SomeMethod的參數類型(Stream)是委托的參數類型(FileStream)的基類,這樣逆變是可以的.
那么如果將String SomeMethod(Stream stream) 改為 Int SomeMethod(Stream stream);
那么C#編輯器會報錯.
說明:協(xié)變性和逆變性只能用于引用類型,不能用于值類型和Void , 因為值類型的存儲結構是變化的,而引用類型的存儲結構始終是一個指針.
委托和接口的逆變和協(xié)變 -> 泛型類型參數
委托的每個泛型類型參數都可標記為協(xié)變量或者逆變量,這樣我們即可將泛型委托類型的一個變量轉型為同一個委托類型的另一個變量,或者的泛型參數類型不同。
不變量:表示泛型類型參數不能更改。
逆變量:表示泛型類型參數可以從一個基類更改為該類的派生類。在C#中,用in關鍵字標記逆變量形式的泛型類型參數,逆變量泛型參數只能出現在輸入位置.
協(xié)變量:表示泛型類型參數可以從一個派生類更改為它的基類,在C#中,用out標記協(xié)變量形式的泛型類型參數.協(xié)變量只能出現在輸出位置.例如:方法返回值類型.
public Delegate TResult Func
(T arg);
在上面這行代碼中,泛型類型參數 T 用in關鍵字標記,使它成為了一個逆變量,而TResult用out 關鍵字標記,這使他成為了一個協(xié)變量.
在使用要獲取泛型參數和返回值的委托時,盡量使用逆變性和協(xié)變性指定in和out關鍵字.在使用具有泛型類型參數的接口也可將它的類型參數標記為逆變量和協(xié)變量,如下代碼:
復制代碼
1 public interface IEnumerator: IEnumerator 2 { 3 Boolean MoveNext(); 4 T Current{get;} 5 } 6 // count方法接受任意類型的參數 7 public int count(IEnumerablecoll){} 8 9 //調用count 傳遞一個IEnumerable10 int i = count(new[]{"Albin"});
復制代碼
深入委托 -> 委托揭秘
首先來聲明一個委托:
1 internal delegate void DelegateBackCall(int val);
通過查看委托反編譯:
通過反 編譯之后看到在DelegateBackCall來中有四個方法:分別為 : 構造器、BeginInvoke、EndInvoke、Invoke 而且還能看到 DelegateBackCall 類是繼承 system.MulticaseDelegate(其實所有的委托都是派生自它.本質上來講:System.MulticaseDelegate是派生 自system.Delegate.而后者又派生自system.object),
在此我們還看到 在 第一個構造函數中,有兩個參數,一個是對象引用,一個是引用回調方法的一個整數,其實所有委托中都有一個構造器,而且構造器的參數正如剛才所說的(一個對象引用,一個引用回調方法的整數) 在構造器的內部,這兩個參數分別保存在_target(當 委托對象包裝一個靜態(tài)方法時,這個字段為null,當委托對象包裝一個實例方法時,這個字段引用的是回調方法要操作的對象.)和_method(一個內部 的整數值,CLR用它來標識要回調的方法) 這兩個私有字段呢給 ,此外構造器還將_invocationList(構造一個委托鏈時,它可以引用一個委托數組,通常為null)字段設為null
每個委托對象實際都是一個包裝器,其中包裝了一個方法和調用該方法時要操作的一個對象
如下代碼:
1 DelegateBackCall DelegateInstance = new DelegateBackCall(Program.InstanceToConsole);2 3 DelegateBackCall DelegateStatic = new DelegateBackCall(StaticToMessage);
在此 DelegateInstance 和 DelegateStatic 變量引用兩個獨立的,初始化好的 DelegatebackCall委托對象. 在委托類(Delegate)中定義了兩個只讀的公共實例屬性,Target和Method,當我們給定一個委托對象引用可以查詢這些屬 性.,Target返回的就是我們之前說的_Target字段中的值,指向回調方法要操作的對象,即如果是一個靜態(tài)的方法那么Target為 null,Method屬性有一個內部轉換機制,可以將私有字段_methodPtr中的值轉為為一個MethodInfo對象并返回它.即我們所傳遞的 方法名.
在我們調用委托對象的變量時,實際上編譯器所生成的代碼時調用的該委托對象的 Invoke方法,比如,在Counter函數中,Foo(val) 那么實際上編譯器為我們解析為 --> Foo.Invoke(val); Invoke是以一種同步的方式調用委托對象維護每一個方法.意思就是說:調用者必須等待調用完成才能繼續(xù)執(zhí)行.Invoke通過_Target和 _methodPtr在指定對象上調用包裝好的回調方法,Invoke方法的簽名與委托的簽名是一致的.
用委托回調很多方法 —> 委托鏈(多播委托)
委托鏈是由委托對象構成的一個集合.利用委托鏈,可以調用集合中的委托所代表的任何方法.換句話說就是一個委托對象可以維護一個可調用方法的列表而不是單獨一個方法,給一個委托對象添加多個方法時,不用直接分配,在此C#編譯器為我們提供了重載 +=,-= 操作符,
那我們還是拿上一代代碼來舉例,代碼如下:
1 DelegateBackCall DelegateInstance = new DelegateBackCall(Program.InstanceToConsole);2 3 DelegateBackCall DelegateStatic = new DelegateBackCall(StaticToMessage);
這段代碼我們可以這樣來改裝一下:
1 DelegateBackCall DelegateFb = null;2 3 DelegateFb += new DelegateBackCall(Program.InstanceToConsole);4 5 DelegateFb += new DelegateBackCall(StaticToMessage);
其實上面這就是委托鏈(也叫多播委托),
那么我們來反編譯一下:
我們在反編譯之后發(fā)現, +=操作的內部是通過 Delegate類的靜態(tài)方法Combine將委托添加到鏈中的.
在此Combine會構造一個新的委托對象,這個新的委托對象對它的私有字段 _target和_methodPtr進行初始化, 同時_invocationList字段被初始化為引用一個委托對象數組,數組的第一個元素被初始化為引用包裝了 Program.InstanceToConsole 方法的委托,數組的第二個元素被初始化為引用了包裝了StaticToMessage方法的委托,然后再內部會進行遍歷每一個委托進行輸出.
同理當我們想移除委托對象集合中一個委托時我們可以通過-= 操作符.如下
DelegateBackCall delegateFb = null;
delegateFb -= new DelegateBackCall(new Program().InstanceToConsole);
delegateFb -= new DelegateBackCall(StaticToMessage);
那么反編譯后同樣的道理:
它的內部是通過Remove函數來對委托進行移除的.在Remove方法被調用 時,它會遍歷所引用的那個委托對象內部維護的委托數組,Remove通過查找其_target和methodPtr字段與第二個參數中的字段匹配的委托, 如果找到匹配的委托,并且在刪除之后數組中只剩余一個數據項,就返回那個數據項,如果找到,并且數組中還剩余多個數據項,就新建一個委托對象 其中創(chuàng)建并初始化的_invocationList數組將引用原始數組中的所有數據項(被刪除的例外),如果刪除僅有的一個元素,那么Remove會返回 NULL.每次Remove只能刪除一個委托.不會刪除所有的.
在此說明一個方法:GetInvocationList,這個方法操作一個從 MulticastDelegate派生的對象,返回一個由Delegate引用構成的數組,其中每個引用都指向委托鏈中的一個委托.在其內 部,GetInvocationList構造并初始化一個數組,讓它的每個元素都引用鏈中的一個委托,然后返回對數組的一個引用,如果 _invocationList字段為null,返回數組只有一個元素,那么它就是委托實例的本身.
如下代碼:
DelegateBackCall delegateFb = null;
delegateFb += new DelegateBackCall(new Program().InstanceToConsole);
delegateFb += new DelegateBackCall(StaticToMessage);private static void GetComponentReport(DelegateBackcall delegateFo)
{ if(delegatefo != null )
{
Delegate[] arrayDelegates = delegatefo.GetInvocationList(); foreach(DelegateBackCall delegateback in arrayDelegates){ //....省略 } }
}
委托定義太多? —> 泛型委托
在.NET Freamework 支持泛型,所以我們可以定義泛型委托,來減少委托聲明的個數,這樣可以減少系統(tǒng)中類型數目,同時也可以簡化編碼.
在.NET freamework中為我們提供了17個Action委托,它們從無參數一直到最多16個參數,對于開發(fā)來說應該是足夠用的了.
除此之外 .NET FreameWork 還提供了17個Function函數,它們允許回調方法返回一個值.
使用獲取泛型實參和返回值的委托時,可利用逆變和協(xié)變,
委托的簡潔寫法 -> 1:Lambda表達式、匿名函數的方式來代替定義回調函數。
實際上這些的寫法可歸納為C#的語法糖,它為我們程序員提供了一種更簡單,更可觀方式.
例如:
不需要定義回調函數,直接使用Lambda表達式的形式,創(chuàng)建匿名函數來執(zhí)行方法體.
Private Static Void CallBackNewDelegateObject()
{
ThreadPool.QueueUserWorkItem( o => Console.WriteLine(o) ,5);
}
傳給QueueUserWorkItem方法的第一個實參是一個Lambda表達 式,Lambda表達式可在編譯器預計會看到一個委托的地方使用,編譯器看到這個Lambda表達式之后會在類中自動定義一個新的私有方法,這個方法稱為 匿名方法,所謂匿名方法,并不是沒有名字,它的名字是由編譯器自動創(chuàng)建的.在此說明一點,匿名函數是被標記為Static,并且是private 這是因為代碼沒有訪問任何實例成員,不過類中可以引用任何靜態(tài)字段或靜態(tài)方法.從而課件匿名函數的性能是比實例方法效率高的,因為它不需要額外的this 參數.
2:簡化語法 -> 局部變量不需要手動的包裝到類中即可傳給回調方法
public static void UsingLocalVariablesInTheCallBack(int num)
{
int[] i = new int[num]; 4
AutoResetEvent done = new AutoResetEvent(false);
for (int n = 0; n < i.Length; n++)
{
ThreadPool.QueueUserWorkItem(obj =>
{
int nums = (Int32)obj;
i[nums] = nums * nums;
if (Interlocked.Decrement(ref num) == 0)
{
done.Set();
}
}, n);
}
done.WaitOne();
for (int n = 0; n < i.Length; n++)
{
Console.WriteLine("Index {0},i {1}", n, i[n]);
}
}
在Lambda表達式的方法體中,如果這個方法體是一個單獨的函數,那么我們如何 的將變量的值傳到方法中呢?那么現在在我們的匿名函數方法體中的代碼就需要抽出到一個單獨的類中,然后類通過字段賦值每一個值,然 后 UsingLocalVariablesInTheCallBack 方法必須構造這個類的一個實例,用方法定義的局部變量的值來初始化這個實例中的字段,然后構造綁定到實例方法的委托對象。
本文出自 51CTO “Albin” 博客
更多知識請進入【濟寧果殼學院】