CSharp 委托(delegate)與事件(event)


CSharp 委托(delegate)與事件(event)

前話

面向群體

  • C#初學者
  • 對委托或者事件分辨不清的同學

目標

  • 了解C#語言中的委托與事件
  • 分辨C#語言中的委托與事件
  • 了解C#語言中的委托與事件部分應用場景
  • 了解事件的實現原理

委托(delegate)

委托是什么

A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type . You can invoke (or call) the method through the delegate instance. ——Delegates (C# Programming Guide)
翻譯過來的大致意思(本人英語水平不佳)如下:
委托是一種表示對具有特定參數列表和返回類型的方法的 引用類型 。實例化委托時,可以將其實例與具有 兼容簽名返回類型 的任何方法相關聯。您可以通過委托實例調用(或調用)方法。
從官網咱們可以看到委托的定義,明確了下面這幾點:

  1. 委托是一種引用類型,這意味着,委托是和類(class)、枚舉類型(enum)、結構體(struct)同等級別的存在,其作用都是定義了一種類型。
  2. 委托實例可以和具有相同簽名的 兼容簽名返回類型任何方法 相關聯。
    這句話可能對於各位來說,包含的信息會多一些,我舉一段代碼進行說明:
/// 在此我們定義了一個名叫MyDelegate的委托類型
/// 其返回值為int類型,而參數列表中有兩個分別名為a和b的int類型的參數
delegate int MyDelegate(int a,int b);

class Test{
    /// 在Test類中包含了一個名為mDelelgate的MyDelegate類型實例
    MyDelegate mDelegate;
    public Test(MyDelegate myDelegate){
        /// 在Test類中的構造方法中賦予mDelegate值
        mDelegate = myDelegate;
    }
    
    /// 通過Test類中的Invoke方法里像調用方法一樣使用mDelegate
    public int Invoke(int a,int b){
        return mDelegate(a,b);
    }
    
    public static int Add(int a,int b){
        return a+b;
    }
    
    /// 為了說明某個要點,構建了一個名為InnerClass的內部類
    /// 該類只有一個方法
    public class InnerClass{
        public int Plus(int a,int b){
            return a*b;
        }
    }
    
    public static void Main(){
        /// 使用靜態方法Add作為參數構造一個Test實例
        Test test1 = new Test(new MyDelegate(Add));
        /// 使用InnerClass類的實例方法Plus作為參數構造一個Test實例
        Test test2 = new Test(new InnerClass().Plus);
        /// 分別輸出兩個實例Invoke方法的結構
        Console.WriteLine(test1.Invoke(1,2));
        Console.WriteLine(test2.Invoke(1,2));
        Console.ReadKey();
    }
}

閱讀了這段代碼后,咱們再回過頭來看那第二個要點:

  • 委托實例可以和具有相同簽名的 兼容簽名返回類型任何方法 相關聯。
    首先是關鍵詞 兼容簽名 這里的簽名主要指的是方法的參數列表的參數個數、參數類型要兼容,這里的兼容目前理解為一致即可。(若要深入理解的話,可以了解C#的協變性與逆變性)
    再是關鍵詞 返回類型 其實與簽名一致,也需要是兼容的返回類型。
    最后是關鍵詞 任何方法 ,這里的任何指的意思就是說包括靜態方法和實例方法,在上述代碼中我們舉了Add靜態方法和Plus實例方法為例說明了這一要點。
    也就是說,當方法滿足了這三個條件后,我們便可以將這個方法來當成一個委托實例來使用(事實上是進行了類型轉換),或者使用這個方法來生成一個委托實例。
    雖然說,委托的定義和一些規矩咱們已經清楚了,但是大家肯定還會有疑問,比如下面:
  • 我到底為什么要定義一個委托,直接調用一個方法不好嗎?
    接下來我會對這個疑問來進行說明。

委托的用處

  • 現在給你一個任務,實現冒泡排序讓一個int數組從小到大排序。
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (arr[j] - arr[j + 1]>0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 再給你一個任務,實現冒泡排序讓一個int數組從大到小排序。
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (arr[j] - arr[j + 1]<0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 再再再給你一個任務,實現冒泡排序讓一個int數組按絕對值從小到大排序,你是不是已經有一點厭煩了?
for (int i = 0; i < arr.Length - 1; i++){
    for (int j = 0; j < arr.Length - 1 - i; j++) {
        if (Math.Abs(arr[j]) - Math.Abs(arr[j + 1])>0){
            var temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
            }
        }
    }
}
  • 如果接着讓你實現不同要求的冒泡排序,你是不是會有點想打我?
    不過為了我的生命安全,暫且就實現到這里。
    你可以發現,其實這三段代碼大同小異,三個要求的改變之處無非就是if后面的條件語句發生了改變。
    並且,如果往后還要提其他要求的話,事實上你也只需要改變一下條件語句,而其他的代碼都是可以復用的。
    於是我們希望有一種方式能夠達到這一要求,此時委托便是解決這個問題的好辦法。
    首先我們定義一個方法的規則,傳入的參數為兩個int值a、b。
    1. 若a在邏輯上大於b,方法返回一個大於0的數字。
    2. 若a在邏輯上小於b,方法則返回一個小於0的數字。
    3. 若a在邏輯上等於b,則返回零。
    然后請閱讀以下代碼:
delegate int CMP(int a, int b);

class Test
{
    /// 冒泡排序主體
    static void Sort(int[] arr, CMP cmp)
    {
        for (int i = 0; i < arr.Length - 1; i++)
        {
            for (int j = 0; j < arr.Length - 1 - i; j++)
            {
                if (cmp(arr[j], arr[j + 1]) > 0)
                {
                    var temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
    /// 比較函數1,若a>b則返回大於0的數字
    static int CMP1(int a,int b)
    {
        return a - b;
    }

    /// 比較函數2,若a<b則返回大於0的數字
    static int CMP2(int a, int b)
    {
        return b-a;
    }
    
    /// 比較函數2,若a絕對值>b絕對值則返回大於0的數字
    static int CMP3(int a, int b)
    {
        return Math.Abs(a) - Math.Abs(b);
    }
    
    /// 輔助函數,遍歷arr並輸出顯示
    static void PrintArr(int[] arr) {
        foreach(var v in arr)
        {
            Console.Write($"{v} ");
        }
        Console.WriteLine();
    }
    
    public static void Main() {
        var arr = new[] { 1, -2, 9, 8, 10 };
        Sort(arr, CMP1);
        PrintArr(arr);
        
        Sort(arr, CMP2);
        PrintArr(arr);
        
        Sort(arr, CMP3);
        PrintArr(arr);
        
        Console.ReadKey();
    }
}

我們發現,定義了一種委托以后,我們在需求發生改變的時候,只需要新寫一個比較方法,然后再調用Sort方法的時候將其當成委托實例傳入即可。
而如果我們直接調用一個方法的話,我們就要寫多個不同的排序方法,從而有大量的重復邏輯。
這樣,我們節省了很多的重復代碼,而讓自己的代碼得到了復用。
事實上,我們也可以從面向對象的角度來理解:

  • 委托實例即是將一個方法當作一個對象來看待,我們可以通過傳遞委托實例,來動態的改變某個地方的邏輯,而使得其他的代碼復用。
    有些同學可能會發現,這和面向對象中的多態會有些類似。
    事實上,上述代碼完全可以使用多態來實現,但是多態需要繼承,也就是說,如果使用多態來實現上述內容的話,那么很明顯我們需要再寫三個類,很明顯這是一個比較麻煩的事情,甚至按照上個例子來說,這三個類中只需要實現各自的CMP方法,而不存在其他的內容。這樣很明顯是一種很不方便的方式。
    而使用委托的話,就不需要再弄幾個新的類,而只需要寫幾個新的方法即可,甚至還有更簡單的方式,也就是接下來我們會提到的lambda表達式。

lambda表達式

上文提到,如果要使用委托的話,往往都會使用一個方法來轉換成一個委托實例。然而,如果每弄一個別的委托實例都需要新建一個方法的話,存在以下問題:

  1. 起名字困難,比如上述的CMP1,CMP2,CMP3,很難以起一個形象的好名字。
  2. 要轉換成委托實例的方法不一定會被作為委托實例以外別處調用,一個類會因此多一個無用的方法。
    為了解決這些問題,簡化編程,有一種語法叫做lambda表達式。
/// 使用lambda表達式的話,就可以使用下面這段代碼來代替上述的CMP1方法構造CMP委托的實例
CMP cmp = (a,b)=>a-b;

關於lambda表達式的粗略了解,可以閱讀 Lambda 表達式(C# 編程指南) 的表達式lambda與語句lambda部分進行了解。

事件(event)

Delegate.Combine & Delegate.Remove

在了解事件前我們先來了解一下這兩個Delegate類的靜態方法。
我通過下面的偽代碼來進行講解:

delegate void MyDelegate();
var delegate1 = ()=>{ Console.WriteLine("delegate1!"); };
var delegate2 = ()=>{ Console.WriteLine("delegate2!"); };
/// 使用Combine方法將兩個delegate實例結合返回一個新的實例
delegate1 = Delegate.Combine(delegate1,delegate2);
/// 調用delegate3會分別調用delegate1和delegate2
delegate1();
/// 使用Remove方法把delegate3中對delegate2的調用移除
delegate1 = Delegate.Remove(delegate1,delegate2);
delegate1();

/// 另外,C#中重載了委托類型的 +、-、+=、-=操作符
/// +會調用Delegate.Combine方法
/// -會調用Delegate.Remove方法
/// +=、-=依次類推

/// 所以上述代碼也可以這樣寫
var delegate1 = ()=>{ Console.WriteLine("delegate1!"); };
var delegate2 = ()=>{ Console.WriteLine("delegate2!"); };
delegate1 += delegate2;
delegate1();
delegate1 -= delegate2;
delegate1();

事件是什么

Events are a special kind of multicast delegate that can only be invoked from within the class or struct where they are declared (the publisher class). ——event (C# reference)
翻譯過來的意思即是:
事件是一種特殊的多播委托僅可以從聲明事件的類或結構(發布服務器類)中對其進行調用
在上述描述中有兩處比較指的關注的地方:

  • 多播委托:多播委托暫時可以這么理解,即之前使用Delegate.Combine方法,將兩個委托合成一個委托,合成的這個委托即是多播委托。多播的意思也就是說,一處調用,就會造成多處影響。如想了解更多可以閱讀:C# 委托(二)—— 多播委托與事件
  • 僅可以從聲明事件的類或結構(發布服務器類)中對其進行調用:這句話會稍難解釋一些,我們先來看一下事件是怎么用的:
delegate void MyDelegate();
class Test{
    /// 使用event關鍵詞修飾一個委托實例,我們稱這個為一個事件
    public static event MyDelegate Delegate;
    
    public static void Invoke(){
        Delegate?.Invoke();
    }
}

class Solution{
    public static void Main(){
        MyDelegate delegate1 = ()=>Console.WirteLine(1);
        MyDelegate delegate2 = ()=>Console.WirteLine(2);
        Test.Delegate += delegate1;
        Test.Delegate += delegate2;
        
        Test.Invoke();
        Console.ReadKey();
    }
}

大家可以看到,在Test類里有一個名為Delegate的MyDelegate類型的事件,在Solution類中的Main方法內對其進行了+=操作(稱之為注冊),在后面通過Test類中的Invoke方法觸發了這個事件(稱之為通知)。
然鵝,我們發現,這個用法和之前的多播委托來說大同小異。我們再回過頭來看那句話:

  • 事件是一種特殊的多播委托僅可以從聲明事件的類或結構(發布服務器類)中對其進行調用
    首先,事件是一種特殊的委托,特殊在哪呢?他只可以再聲明事件的類或者結構中對其調用。我們發現,在上面例子中,我特意用了兩個類,使用Test類去調用Invoke方法去觸發這個事件。
    各位可以試一試,雖然Test類中的Delegate事件是用public修飾的,但是在Solution類中卻不能直接觸發這個事件。
    而如果使用public的多播委托的話,我們同樣可以使用+=、-=進行注冊或者取消注冊,但是很明顯,在能夠使用+=、-=注冊或者取消注冊的同時,我們也開放了調用這個多播委托的權限。
    因此我們可以總結:
  • 使用event修飾一個委托形成事件是給委托加一個調用的限制。
    可是,可能很多同學會有這樣的疑問,如果直接使用多播委托,能否達到相同的效果呢?事實上這是完全可以的,下面我們來描述下委托的實質。

事件的實質

首先我們使用多播委托來實現一下事件:

class Test{
    private MyDelegate mDelegate = null;
    private Invoke(){ mDelegate?.Invoke(); }
    public Add(MyDelegate d){ mDelegate += d; }
    public Remove(MyDelegate d){ mDelegate -= d; }
    #region Other Members
    ///
    #endregion
}

利用上述代碼,我們便可以使用Add方法來注冊事件而使用Remove方法來取消注冊事件。
並且,我們將mDelegate的調用權限給了這個類而不公開。
事實上,事件的默認實現就是類似上述的代碼實現,當你使用event關鍵詞定義一個事件時,會為他創建一個私有的委托字段,還有兩個與該事件的訪問權限一致的方法(即類似上述的Add與Remove方法)。
而之所以剛剛強調是事件的默認實現,是因為我們其實可以改變事件的add方法和remove方法的行為,這個形式其實與C#中的屬性會很相似,下面通過一段代碼來演示一下如何使用自定義的事件。

delegate void MyDelegate();
class Test{
    private MyDelegate mDelegate=null;
    public event MyDelegate Event{
        add{ mDelegate += value; }
        remove{ mDelegate -= value; }
    }
}

是不是非常類似於屬性呢?當然了,其實他們兩個的設計思維我覺得很類似,屬性的實質其實也類似,其本質都是簡化了很多操作的語法糖。
另外,這里還給出一個自定義事件的例子來幫助大家的理解。
默認的事件實現有一個特性,如果一個委托實例被多次注冊,事實上在事件發送通知的時候,這個委托會被執行多次。
那么,怎樣來讓每個委托實例只被執行一次呢?
請閱讀下面的代碼:

delegate void MyDelegate();
class Test
{
    /// 使用一個HashSet容器來保存委托實例,以在容器中,每個委托實例只存在對他的一個引用
    static HashSet<MyDelegate> delegates = new HashSet<MyDelegate>();
    public static event MyDelegate Delegate
    {
        add { delegates.Add(value); }
        remove { delegates.Remove(value); }
    }
    public static void Invoke()
    {
        foreach(var dele in delegates)
        {
            dele();
        }
    }
}
class Test2
{
    public static void Main()
    {
        MyDelegate delegate1 = () => Console.WriteLine(1);
        Test.Delegate += delegate1;
        Test.Delegate += delegate1;
        Test.Delegate += delegate1;

        Test.Invoke();
        Console.ReadKey();
    }
}

后話

總結

委托:

  • 委托是一種引用類型,其目的是將方法作為對象來對待。
  • 委托可以很方便的去替換方法中的部分邏輯。
  • 通過lambda表達式可以很方便的創建委托。

事件:

  • 事件是類似於屬性的存在,其作用是開放委托實例的+=、-=權限但是保留類的調用權限。
  • 可以重寫事件的add、remove方法來自定義事件的行為。

其他資料

Func與Action

Func與Action是兩種官方提供的泛型委托類型,按我的理解來說的話,主要目是簡化常去創建新的委托類型的操作。
應用較為簡單,各位可以參考這篇博客進行簡要了解:C# 之 Action 和 Func 的用法

觀察者模式

觀察者模式是一種可以廣泛應用事件機制的設計模式,如果想對這種設計模式進行了解的話建議各位去觀看這個視頻:23個設計模式——觀察者模式
或者閱讀《Head First 設計模式》/《設計模式》的相關章節。


免責聲明!

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



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