.NET委託








什麼是委託? -> 初識委託

   在很多應用程序中(C,C++),需要對象使用某種回調機制,能夠與創建它 的實體進行通信,在.NET平臺下,通過委託來提供了一種回調函數機制,在.NET平臺下,委託確保回調函數是類型安全的(這也正是.NET FreamWork與非託管代碼的區別)。本質上來講,委託是一個類型安全的對象,它指向程序中另一個以後會被調用的方法(或多個方法),就是將方法作爲 參數來傳遞.

C#中定義委託類型

   在C#中創建一個委託類型時,需要使用關鍵字 delegate 關鍵字,類型名可以自定義,但是 委託要指定一個回調方法的簽名.

1 //聲明一個委託,該委託可以指向任何傳入兩個Int類型並且方法的返回值爲Int.2 public delegate int Binary(int x,int y);3 4 //聲明一個委託,該委託可以指向任何傳入一個String類型並且方法返回值爲Void5 public delegate void DelegateBackCall(string str);

使用委託發送對象狀態通知 -> 用委託回調靜態方法

 先來看一段代碼:

複製代碼

 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類中定義了私有的靜態方 法Counter,用來統計X到Y之間整數的個數,同時呢,Counter方法還獲取一個Foo,Foo是對一個DelegateBackCall委託對 象的引用, 在方法體中 我們首先遍歷一下,如果Foo不爲null,就調用Foo變量所指定的回調函數.那麼傳入的這個回調函數的是正在處理的那個數據項的值.

 然後 在Program的Main函數中,第一次調用Counter時,第三個參數傳遞的是Null,所在在Counter函數中是不會執行回調函數的.

 接着第二次調用Counter函數時,給第三個參數傳遞一個新構造的 DelegateBackCall委託對象(其實在此委託對象是方法的一個包裝器, 使方法能通過包裝器來間接的進行回調),然後靜態方法StaticDelegateToConsole被傳給DelegateBackCll委託類型的構 造器(StaticDelegateToConsole就是要包裝的方法),當Counter執行時,會在遍歷每個數據項之後調用靜態方法 StaticDelegateToConsole,最後輸出結果:

1 Result:2 Item : 1 3 Item : 24 Item : 3 5 Item : 4

使用委託發送對象狀態通知 -> 用委託調用實例方法

首先還是來看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創建的實例 方法,這將委託包裝對InstanceDelegateToMessage方法的一個引用,該方法是一個實例方法.同調用靜態的一樣,當Counter調 用Foo回調的時候會調用InstanceDelegateToMessage實例方法,新構造的對象將作爲隱式的this參數傳給這個實例方法.

最後的輸出結果爲:

1 Message : 12 Message : 23 Message : 34 Message : 4

通過上面兩個例子我們知道委託可以包裝對實例方法和靜態方法的調用,如果是實例方法,那麼委託需要知道方法操作的是哪個對象實例(包裝實例是很有用的,對象內部的代碼可以訪問對象的實例成員,這意味着對象可以維護一些狀態,並在回調方法執行期間利用這些狀態信心).

委託的協變和逆變

   將一個方法綁定到委託時,C#和CLR都允許引用類型的協變和逆變

   協變:方法的返回類型是從委託的返回類型派生的一個類型

   逆變:方法獲取的參數類型是委託參數類型的基類.

例如:

1 delegate object CallBack(FileStream file);2 3 string SomeMethod(Stream stream);

在上面代碼中,SomeMethod的返回類型(string)派生自委託的返回類型(object),這樣協變是可以的.

SomeMethod的參數類型(Stream)是委託的參數類型(FileStream)的基類,這樣逆變是可以的.

那麼如果將String SomeMethod(Stream stream) 改爲 Int SomeMethod(Stream stream);

那麼C#編輯器會報錯.

說明:協變性和逆變性只能用於引用類型,不能用於值類型和Void , 因爲值類型的存儲結構是變化的,而引用類型的存儲結構始終是一個指針.

委託和接口的逆變和協變 -> 泛型類型參數

委託的每個泛型類型參數都可標記爲協變量或者逆變量,這樣我們即可將泛型委託類型的一個變量轉型爲同一個委託類型的另一個變量,或者的泛型參數類型不同

不變量:表示泛型類型參數不能更改。 

逆變量:表示泛型類型參數可以從一個基類更改爲該類的派生類。在C#中,用in關鍵字標記逆變量形式的泛型類型參數,逆變量泛型參數只能出現在輸入位置. 

協變量:表示泛型類型參數可以從一個派生類更改爲它的基類,在C#中,用out標記協變量形式的泛型類型參數.協變量只能出現在輸出位置.例如:方法返回值類型.

public Delegate  TResult Func<in T,out TResult>(T arg);

在上面這行代碼中,泛型類型參數 T 用in關鍵字標記,使它成爲了一個逆變量,而TResult用out 關鍵字標記,這使他成爲了一個協變量.

在使用要獲取泛型參數和返回值的委託時,儘量使用逆變性和協變性指定in和out關鍵字.在使用具有泛型類型參數的接口也可將它的類型參數標記爲逆變量和協變量,如下代碼:

複製代碼

 1 public interface  IEnumerator<out T> : IEnumerator 2 { 3     Boolean MoveNext(); 4     T Current{get;} 5 } 6 // count方法接受任意類型的參數 7 public int count(IEnumerable<object> coll){} 8  9 //調用count 傳遞一個IEnumerable<string>10 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(當 委託對象包裝一個靜態方法時,這個字段爲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字段中的值,指向回調方法要操作的對象,即如果是一個靜態的方法那麼Target爲 null,Method屬性有一個內部轉換機制,可以將私有字段_methodPtr中的值轉爲爲一個MethodInfo對象並返回它.即我們所傳遞的 方法名.

在我們調用委託對象的變量時,實際上編譯器所生成的代碼時調用的該委託對象的 Invoke方法,比如,在Counter函數中,Foo(val) 那麼實際上編譯器爲我們解析爲 --> Foo.Invoke(val); Invoke是以一種同步的方式調用委託對象維護每一個方法.意思就是說:調用者必須等待調用完成才能繼續執行.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);

其實上面這就是委託鏈(也叫多播委託),

那麼我們來反編譯一下:

 

我們在反編譯之後發現, +=操作的內部是通過 Delegate類的靜態方法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字段與第二個參數中的字段匹配的委託, 如果找到匹配的委託,並且在刪除之後數組中只剩餘一個數據項,就返回那個數據項,如果找到,並且數組中還剩餘多個數據項,就新建一個委託對象 其中創建並初始化的_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 支持泛型,所以我們可以定義泛型委託,來減少委託聲明的個數,這樣可以減少系統中類型數目,同時也可以簡化編碼.

在.NET freamework中爲我們提供了17個Action委託,它們從無參數一直到最多16個參數,對於開發來說應該是足夠用的了.

除此之外 .NET FreameWork 還提供了17個Function函數,它們允許回調方法返回一個值.

使用獲取泛型實參和返回值的委託時,可利用逆變和協變,

委託的簡潔寫法 -> 1:Lambda表達式、匿名函數的方式來代替定義回調函數。

實際上這些的寫法可歸納爲C#的語法糖,它爲我們程序員提供了一種更簡單,更可觀方式.

例如:

不需要定義回調函數,直接使用Lambda表達式的形式,創建匿名函數來執行方法體.

Private Static Void CallBackNewDelegateObject()
{
      ThreadPool.QueueUserWorkItem( o => Console.WriteLine(o) ,5);  
}

傳給QueueUserWorkItem方法的第一個實參是一個Lambda表達 式,Lambda表達式可在編譯器預計會看到一個委託的地方使用,編譯器看到這個Lambda表達式之後會在類中自動定義一個新的私有方法,這個方法稱爲 匿名方法,所謂匿名方法,並不是沒有名字,它的名字是由編譯器自動創建的.在此說明一點,匿名函數是被標記爲Static,並且是private 這是因爲代碼沒有訪問任何實例成員,不過類中可以引用任何靜態字段或靜態方法.從而課件匿名函數的性能是比實例方法效率高的,因爲它不需要額外的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 方法必須構造這個類的一個實例,用方法定義的局部變量的值來初始化這個實例中的字段,然後構造綁定到實例方法的委託對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章