C#基礎知識梳理系列五:委托與事件(上)


摘 要

委托與事件,這是一個老生常談的話題,很多人在講,很多人在用,但似乎它是一個永遠也說不完道不盡的東西。那么,到底什么是委托?什么是事件?委托鏈又是怎么回事?為什么使用事件時常常用到+=/-=?委托又是如何支持協變和逆變的呢?你喜歡使用Action和Func<T,TResult>嗎?由於內容比較多,這一章將分上、下兩部分慢慢為你講解。

第一節 委托

回調函數是Windows編程語言中一種常見而有用的編程實踐,在C/C++中,它指的是函數調用的指針,通過這個指針可以方便地對函數進行調用,當然這個指針也是可以被傳遞給別的函數使用。在.NET Framework中,回調是通過委托來實現的,當然在這里,比起非托管的C/C++,.NET中的委托提供了更豐富的功能,比如同步和異常調用、委托鏈等等。

委托其實是一種類型,是一種定義方法簽名的類型,它支持以new的方式來實例化。委托是使用關鍵字delegate進行定義的,它其實是對方法的包裝和聚集。既然它也是一種類型,所以能定義類的地方都可以定義委托,如下是一個委托的聲明:

public delegate void ShowMessage(string msg);

任何與委托簽名匹配的方法都可以分配給委托,這就要求該方法的返回值類型與參數列表必須與委托的簽名相匹配,方法可以是靜態的,也可以是對象級的,通過委托可以對分配給委托的方法進行調用。我們來看一下編譯器來干了什么事:

通過上圖我們可以看出,編譯器讓這個委托類型繼承了System.MulticastDelegate類,System.MuticastDelegate類又繼承了Delegate類,如下:

public abstract class MulticastDelegate : Delegate
{
    //...
}

同時還生成了三個方法,其中BeginInvoke()和EndInvoke()兩個方法是供異步調用,Invoke()方法是供同步調用。其實通過IL更能明確的看出委托最終經過編譯器生成的是一個類:

在Delegate類中有兩個非常重要的字段:

internal object _target; 當委托包裝的是一個靜態方法時,該字段為null;當委托包裝的是一個對象方法時,該字段引用的是該對象。該字段可以通過屬性Target獲取。

internal IntPtr _methodPtr; 保存着一個方法的IntPtr值,屬性Method 獲取一個標識了該回調方法的對象(MethodInfo)的引用,在類的內部,這個MethodInfo對象是通過方法GetMethodImpl()運算生成來的。

我們分別定義一個實例級方法ShowString()和一個靜態方法StaticShowString(),代碼:

void ShowString(String str)
        {
            Console.WriteLine("ShowString:" + str);
        }
    public class Code_05_01
    {
        public static void StaticShowString(string str)
        {
            Console.WriteLine("StaticShowString:" + str);
        }
    }

現在我們來看一下運行時的這兩個屬性,如下圖:

上圖中顯示了Target指向的是對象,下圖中由於綁定了一個靜態方法,所以Target是null。

MulticastDelegate類有兩個私有字段:

private IntPtr _invocationCount; 保存了委托鏈中方法個數。

private object _invocationList; 保存的即是委托鏈(方法集合)

MulticastDelegate類重寫了Delegate的一個虛方法public virtual Delegate[] GetInvocationList(),來獲取委托的調用列表。

對委托的調用也有兩種方式,可以同步調用也可以異步調用,同步調用有兩種方法:直接調用和使用委托對象的Invoke方法,如下:

void TestCall()
        {
            ShowMessage callShow = new ShowMessage(ShowString);
            callShow("abc");
        }
        void TestInvoke()
        {
            ShowMessage invokeShow = new ShowMessage(ShowString);
            invokeShow.Invoke("abc");
        }

這兩種調用有什么區別呢?我們來看一下它們的IL。

TestCall.IL:

.method private hidebysig instance void  TestCall() cil managed
{
  // 代碼大小       27 (0x1b)
  .maxstack  3
  .locals init ([0] class ConsoleApp.Example05.ShowMessage callShow)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldftn      instance void ConsoleApp.Example05.Code_05::ShowString(string)
  IL_0008:  newobj     instance void ConsoleApp.Example05.ShowMessage::.ctor(object,
                                                                             native int)
  IL_000d:  stloc.0
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "abc"
  IL_0014:  callvirt   instance void ConsoleApp.Example05.ShowMessage::Invoke(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Code_05::TestCall

TestInvoke.IL:

.method private hidebysig instance void  TestInvoke() cil managed
{
  // 代碼大小       27 (0x1b)
  .maxstack  3
  .locals init ([0] class ConsoleApp.Example05.ShowMessage invokeShow)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldftn      instance void ConsoleApp.Example05.Code_05::ShowString(string)
  IL_0008:  newobj     instance void ConsoleApp.Example05.ShowMessage::.ctor(object,
                                                                             native int)
  IL_000d:  stloc.0
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "abc"
  IL_0014:  callvirt   instance void ConsoleApp.Example05.ShowMessage::Invoke(string)
  IL_0019:  nop
  IL_001a:  ret
} // end of method Code_05::TestInvoke

其實兩者的內部調用實現基本一樣,都是對委托對象的方法Invoke進行調用。

委托鏈 也叫多路廣播委托,是在委托內部由委托對象構成的一個委托對象集合,可以通過委托來調用委托鏈內的所有委托包裝的方法。Delegate有兩個靜態方法,Combine()用於創建委托鏈和委托鏈添加新的委托,我們假設有一個委托委托A,來模擬向委托鏈追加委托的過程:

(1) 委托A對象在實例化的時候已經包裝了一個方法,

(2) 當調用Delegate.Combine()方法向委托A追加新委托B對象時,在內部會重新創建一個委托對象C,並且用新追加的委托(方法)初始化_target和_methodPtr字段;

(3) 將委托C的_invocationList初始化為一個委托對象數組,並將委托A放到這個數組的第1項(索引為0)位置,然后將新的委托B對象放到數據的第2項位置(這里會根據委托的個數依次遞增,新增加的那個委托對象總是在這個數組的最后位置),最后返回這個新創建的委托對象C。

當再次向新委托C追加委托成員時,會重復(1)-(3)的步驟,每次最終都會返回一個新創建的委托,並且字段_target和_methodPtr總是根據新增加的委托對象來實例化,不過此時的這兩個字段好像用處已經不大了,它只是保存了是最后進來的一個委托對象的部分數據。很顯然,如果一個委托只包裝了一個方法,並沒有因追加新的委托而創建委托鏈,那么在這種情況下,這兩個字段_target和_methodPtr是非常有意義的。

與方法Combine()對應的有一個方法public static Delegate Remove(Delegate source, Delegate value);很顯然它是從委托鏈中移除一個委托對象。為了方便書寫,C#為委托類型的實例重載了兩個操作符+=和-=分別對應於方法Combine和方法Remove。通過下圖我們可以看一下追加委托的過程。繼續對上面的代碼進行改造,增加一個對象級方法:

void ShowString2(String str)
        {
            Console.WriteLine("ShowString2:" + str);
        }

然后實例化一個委托ShowMessage show3 = new ShowMessage(ShowString);,如下圖:

可以看到此時show3的Method指向的方法是ShowString(System.String),並且字段_invocationCount的值為0,_invocationList是null。

接下來我們向show3追加一個委托對象(事實上是向委托鏈追加),show3 += new ShowMessage(ShowString2);,也可以使用簡寫:show3 += ShowString2;這樣不用手寫代碼來創建委托對象,但在編譯的過程中,編譯器還是會識別出這是一個創建委托對象的過程並向IL中寫入創建委托對象的代碼。如下圖:

此時,我們已經看到Method指向的是新的方法ShowString2(System.String),_invocationCount的值為2,_invocationList已經是一個擁有兩個元素的集合。

對委托對象有委托鏈且不為空的時候,又是如何調用委托鏈內的各個回調函數的呢?通過上面對Invoke的討論,我們知道當調用一個委托對象的回調函數時,在內部CLR實際上是調用了Invoke方法,而在調用invoke方法時,該委托會發現字段_invocationList不為null,接着就會遍歷該數組中的所有委托對象依次對委托方法進行調用。

協變性與逆變性

委托的協變性 是指委托方法能返回從對應委托的返回類型派生的一個類型。

委托的逆變性 是指方法獲取的參數類型可以是委托的參數類型的基類。

這里的描述的有點繞口,我們來年如下代碼:

public class Code_05_02
    {
        public string Name { get; set; }
    }
    public class Code_05_03 : Code_05_02
    {
        public int Age { get; set; }
    }
//定義一個委托MyDel
public delegate Code_05_02 MyDel(Code_05_03 para);
//定義一個與委托MyDel 相匹配的方法
        private Code_05_03 GetData(Code_05_02 para)
        {
            return new Code_05_03();
        }
//以下的實例化及調用是可行的。
            MyDel del = new MyDel(GetData);
            del(new Code_05_03());

Code_05_03類繼承於Code_05_02類,委托MyDel的返回類型是Code_05_02,方法GetData的返回類型是Code_05_03,這體現的協變性;方法GetData的參數類型是Code_05_02,委托的參數類型是Code_05_03,這體現了逆變性。

需要說明一點的是:協變性和逆變性不能用於值類型(包括void)。

在我們開發的過程中,可能經常要使用委托,但委托的定義都大同小異,很幸運的是.NET Framework為我們預定義了很多的常用泛型委托。

無返回值的Action系列:

public delegate void Action<in T>(T obj)
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)
//共有16個,另外還有一個無參的非泛型委托:
public delegate void Action()

使用非常簡單,代碼示例:

void TestAction()
        {
            Action<string> act = new Action<string>(ShowString);
            act("Action");
        }

有返回值的Func<T,TResult>系列:

public delegate TResult Func<in T, out TResult>(T arg)
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2)
//共有16個,另外還有一個無參的泛型委托:
public delegate TResult Func<out TResult>()

一般的時候,我們使用這些委托已經足夠了。詳細內容可以查詢MSDN: Action<T>系列 Func<T,TResult>系列

C#的lambda表達式為委托的簡化使用顯示出了很不錯的編程體驗。可以參考MSDN的相關章節:Lambda 表達式(C# 編程指南)

這一章的上半部分,我們主要講解了與委托相關的內容,后一篇的下半部分將主要講解什么是事件及委托如何與事件共事。

 

小 結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM