【詳細】【轉】C#中理解委托和事件

来源:https://www.cnblogs.com/gdsblog/archive/2018/04/11/8797398.html
-Advertisement-
Play Games

文章是很基礎,但很實用,看了這篇文章,讓我一下回到了2016年剛剛學委托的時候,故轉之! 1.委托 委托類似於C++中的函數指針(一個指向記憶體位置的指針)。委托是C#中類型安全的,可以訂閱一個或多個具有相同簽名方法的函數指針。簡單理解,委托是一種可以把函數當做參數傳遞的類型。很多情況下,某個函數需要 ...


文章是很基礎,但很實用,看了這篇文章,讓我一下回到了2016年剛剛學委托的時候,故轉之!

1.委托

委托類似於C++中的函數指針(一個指向記憶體位置的指針)。委托是C#中類型安全的,可以訂閱一個或多個具有相同簽名方法的函數指針。簡單理解,委托是一種可以把函數當做參數傳遞的類型。很多情況下,某個函數需要動態地去調用某一類函數,這時候我們就在參數列表放一個委托當做函數的占位符。在某些場景下,使用委托來調用方法能達到減少代碼量,實現某種功能的用途。

1.1.自定義委托

聲明和執行一個自定義委托,大致可以通過如下步驟完成:

  1. 利用關鍵字delegate聲明一個委托類型,它必須具有和你想要傳遞的方法具有相同的參數和返回值類型;
  2. 創建委托對象,並且將你想要傳遞的方法作為參數傳遞給委托對象; 
  3. 通過上面創建的委托對象來實現該委托綁定方法的調用。 

下麵一段代碼,完成了一次應用委托的演示:

複製代碼
    //step01:使用delegate關鍵字聲明委托
    public delegate int CalcSumDelegate(int a, int b);

class Program { static void Main(string[] args) { //step03:實例化這個委托,並引用方法 CalcSumDelegate del = new CalcSumDelegate(CalcSum); //step04:調用委托 int result = del(5, 5); Console.WriteLine("5+5=" + result); }
//step02:聲明一個方法和委托類型對應 public static int CalcSum(int a, int b) { return a + b; } }
複製代碼

通過上面4個步驟,完成了委托從聲明到調用的過程。接著,咱也學著大神用ILSpy反編譯上面的代碼生成的程式集。截圖如下:

  1. 自定義委托繼承關係是:System.MulticastDelegate —> System.Delegate —>System.Object。
  2. 委托類型自動生成3個方法:BeginInvoke、EndInvoke、Invoke。查資料得知,委托正是通過這3個方法在內部實現調用的。Invoke 方法,允許委托同步調用。上面調用委托的代碼del(5, 5)執行時,編譯器會自動調用 del.Invoke(5,5);BeginInvoke 方法,允許委托的非同步調用。假如上述委托以非同步的方式執行,則要顯示調用dal.BeginInvoke(5,5)。

 註意:BeginInvoke 和 EndInvoke 是.Net中使用非同步方式調用同步方法的兩個重要方法,具體用法詳見微軟官方示例

1.2.多播委托

一個委托可以引用多個方法,包含多個方法的委托就叫多播委托。下麵通過一個示例來瞭解什麼是多播委托:

複製代碼
    //step01:聲明委托類型
    public delegate void PrintDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            //step03:實例化委托,並綁定第1個方法
            PrintDelegate del = Func1;
            //綁定第2個方法
            del += Func2;
            //綁定第3個方法
            del += Func3;
            //step04:調用委托
            del();
//控制台輸出結果: //調用第1個方法! //調用第2個方法! //調用第3個方法! } //step02:聲明和委托對應簽名的3個方法 public static void Func1() { Console.WriteLine("調用第1個方法!"); } public static void Func2() { Console.WriteLine("調用第2個方法!"); } public static void Func3() { Console.WriteLine("調用第3個方法!"); } }
複製代碼

可以看出,多播委托的聲明過程是和自定義委托一樣的,可以理解為,多播委托就是自定義委托在實例化時通過 “+=” 符號多綁定了兩個方法。

Q:為什麼能給委托綁定多個方法呢?

自定義委托的基類就是多播委托MulticastDelegate ,這就要看看微軟是如何對System.MulticastDelegate定義的:

MulticastDelegate擁有一個帶有鏈接的委托列表,該列表稱為調用列表,它包含一個或多個元素。在調用多路廣播委托時,將按照調用列表中的委托出現的順序來同步調用這些委托。如果在該列表的執行過程中發生錯誤,則會引發異常。(--摘自MSDN)

Q:為什麼使用“+=”號就能實現綁定呢?

先來看上述程式集反編譯後的調用委托的代碼:

 

“+=”的本質是調用了Delegate.Combine方法,該方法將兩個委托連接在一起,並返回合併後的委托對象。

Q:多播委托能引用多個具有返回值的方法嗎?

答案是,當然能。委托的方法可以是無返回值的,也可以是有返回值的。不過,對於有返回值的方法需要我們從委托列表上手動調用。否則,就只能得到委托調用的最後一個方法的結果。下麵通過兩段代碼驗證下:

複製代碼
    public delegate string GetStrDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            GetStrDelegate del = Func1;
            del += Func2;
            del += Func3;
            string result = del();
            Console.WriteLine(result);
            
            //控制台輸出結果:
        //You called me from Func3
        }
        public static string Func1()
        {
            return "You called me from Func1!";
        }
        public static string Func2()
        {
            return "You called me from Func2!";
        }
        public static string Func3()
        {
            return "You called me from Func3!";
        }
    }
複製代碼 直接執行

正確做法:利用GetInvocationList獲得委托列表上所有方法,迴圈依次執行委托,並處理委托返回值。 

複製代碼
    public delegate string GetStrDelegate();
    public class Program
    {
        public static void Main(string[] args)
        {
            GetStrDelegate del = Func1;
            del += Func2;
            del += Func3;
            //獲取委托鏈上所有方法
            Delegate[] delList = del.GetInvocationList();
            //遍歷,分別處理每個方法的返回值
            foreach (GetStrDelegate item in delList)
            {
                //執行當前委托
                string result = item();
                Console.WriteLine(result);
                //控制台輸出結果:
                //You called me from Func1
                //You called me from Func2
                //You called me from Func3
            }
            Console.ReadKey();
        }
        public static string Func1()
        {
            return "You called me from Func1";
        }
        public static string Func2()
        {
            return "You called me from Func2";
        }
        public static string Func3()
        {
            return "You called me from Func3";
        }
    }
複製代碼 遍歷執行

1.3.匿名方法

匿名方法是C#2.0版本引入的一個新特性,用來簡化委托的聲明。假如委托引用的方法只使用一次,那麼就沒有必要聲明這個方法,這時用匿名方法表示即可。

複製代碼
    //step01:定義委托類型
    public delegate string ProStrDelegate(string str);
    public class Program
    {
        public static void Main(string[] args)
        {
            //step02:將匿名方法指定給委托對象
            ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
            string result = del("KaSlFkaDhkjHe");
            Console.WriteLine(result);
            Console.ReadKey();
            //輸出:KASLFKAFHKJHE
        }
    }
複製代碼

匿名方法只是C#提供的一個語法糖,方便開發人員使用。在性能上與命名方法幾乎無異。

匿名方法通常在下麵情況下使用:

  1. 委托需要指定一個臨時方法,該方法使用次數極少;
  2. 這個方法的代碼很短,甚至可能比方法聲明都短的情況下使用。

1.4.Lambda表達式

Lambda表達式是C#3.0版本引入的一個新特性,它提供了完成和匿名方法相同目標的更加簡潔的格式。下麵示例用Lambda表達式簡化上述匿名方法的例子:

複製代碼
    public delegate string ProStrDelegate(string str);
    public class Program
    {
        public static void Main(string[] args)
        {
            //匿名委托
            //ProStrDelegate del = delegate(string str) { return str.ToUpper(); };
            //簡化1
            //ProStrDelegate del1 = (string str) =>{ return str.ToUpper(); };
            //簡化2
            //ProStrDelegate del2 = (str) =>{ return str.ToUpper(); };
            //簡化3
            ProStrDelegate del3 = str => str.ToUpper();
            string result = del3("KaSlFkaDhkjHe");
            Console.WriteLine(result);
            Console.ReadKey();
            //輸出:KASLFKAFHKJHE
        }
    }
複製代碼
  • 簡化1:去掉delegate關鍵字,用"=>"符號表示參數列表和方法體之間的關係;
  • 簡化2:去掉方法的參數類型;假如只有一個參數,參數列表小括弧()也可省略;
  • 簡化3:如果方法體中的代碼塊只有一行,可以去掉 return,去掉方法體的大括弧{}。

1.5.內置委托

上述幾種委托的使用,都沒能離開定義委托類型這一步驟。微軟乾脆直接把定義委托這一步驟封裝好,形成三個泛型類:Action<T>、Func<T>和Predicate<T>,這樣就省去了定義的步驟,推薦使用。

複製代碼
    public class Program
    {
        public static void Main(string[] args)
        {
            //Action
            Action<string> action = delegate(string str) { Console.WriteLine("你好!" + str); };
            action("GG");

//Func Func<int, int, int> func = delegate(int x, int y) { return x + y; }; Console.WriteLine("計算結果:" + func(5, 6));
//Predicate Predicate<bool> per = delegate(bool isTrue) { return isTrue == true; }; Console.WriteLine(per(true)); } }
複製代碼

它們的區別如下:

  1. Action<T>委托:允許封裝的方法有多個參數,不能有返回值;
  2. Func<T>委托:允許封裝的方法有多個參數,必須有返回值;
  3. Predicate<T>委托:允許封裝的方法有一個參數,返回值必須為bool類型。

2.事件

委托是一種類型,事件依賴於委托,故事件可以理解為是委托的一種特殊實例。它和普通的委托實例有什麼區別呢?委托可以在任意位置定義和調用,但是事件只能定義在類的內部,只允許在當前類中調用。所以說,事件是一種類型安全的委托。

2.1.定義事件

通過一個簡單的場景來演示下事件的使用:

複製代碼
    /// <summary>
    /// 音樂播放器
    /// </summary>
    public class MusicPlayer
    {
        //step01:定義 音樂播放結束 事件
        public event EventHandler<EventArgs> PlayOverEvent;
        public string Name { get; set; }
        public MusicPlayer(string name)
        {
            this.Name = name;
        }
        //step02:定義一個觸發事件的方法
        public void PlaySong()
        {
            //模擬播放
            Console.WriteLine("正在播放歌曲:" + this.Name);
            for (int i = 0; i < 20; i++)
            {
                Console.Write(".");
                Thread.Sleep(100);
            }
            //播放結束,則觸發PlayOverEvent事件
            if (PlayOverEvent != null)
            {
                PlayOverEvent(this, null);
            }
        }
    }
    public class Program
    {
        static void Main(string[] args)
        {
            //創建音樂播放器對象
            MusicPlayer player = new MusicPlayer("自由飛翔");
            //step03:註冊事件
            player.PlayOverEvent += player_PlayOverEvent;
            //播放歌曲,結束後觸發事件
            player.PlaySong();
            Console.ReadKey();
        }
        static void player_PlayOverEvent(object sender,EventArgs e)
        {
            MusicPlayer player = sender as MusicPlayer;
            Console.WriteLine("\r\n{0}播完了!", player.Name);
        }
    }
複製代碼

程式運行結果:

總結上面事件使用的幾個步驟:

  1. 用event關鍵字定義事件,事件必須要依賴一個委托類型;
  2. 在類內部定義觸發事件的方法;
  3. 在類外部註冊事件並引發事件。

public event EventHandler<EventArgs> PlayOverEvent
這句代碼在MusicPlayer類定義了一個事件成員PlayOverEvent,我們說事件依賴於委托、是委托的特殊實例,所以EventHandler<EventArgs>肯定是一個委托類型。下麵我們來驗證一下:

EventHandler是微軟封裝好的事件委托,該委托沒有返回值類型,兩個參數:sender事件源一般指的是事件所在類的實例;TEventArgs事件參數,如果有需要創建,要顯示繼承System.EventArgs。

2.2.事件的本質

  MusicPlayer player = new MusicPlayer("自由飛翔");
  //註冊事件
  player.PlayOverEvent += player_PlayOverEvent;
  player.PlaySong();

從上面代碼我們觀察到,事件要通過"+="符號來註冊。我們猜想,事件是不是像多播委托一樣通過Delegate.Combine方法可以綁定多個方法?還是通過反編譯工具查看下。

我們看到PlayOverEvent事件內部生成了兩個方法:add_ PlayOverEvent和remove_ PlayOverEvent。add方法內部調用Delegate.Combine把事件處理方法綁定到委托列表;remove方法內部調用Delegate.Remove從委托列表上移除指定方法。其實,事件本質上就是一個多播委托。

3.參考文章

[1] Edison Chou,http://www.cnblogs.com/edisonchou/p/4827578.html

[2] jackson0714,http://www.cnblogs.com/jackson0714/p/5111347.html

[3] Sam Xiao, http://www.cnblogs.com/xcj26/p/3536082.html

【轉:http://www.cnblogs.com/esofar/p/5493028.html

歡迎大家關註我都我的微信 公眾號,公眾號漲粉絲人數,就是你們對我的喜愛程度!

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 要為掃雷游戲佈置地雷,掃雷游戲的掃雷面板可以用二維int數組表示。如某位置為地雷,則該位置用數字-1表示, 如該位置不是地雷,則暫時用數字0表示。 編寫程式完成在該二維數組中隨機佈雷的操作,程式讀入3個參數:佈雷面板的行數(r),列數(c),佈置的地雷個數(n), 且要滿足0<n<r*c*0.75( ...
  • import org.apache.http.HttpEntity;import org.apache.http.client.methods.HttpGet;import org.apache.http.impl.client.CloseableHttpClient;import org.apac ...
  • 實現過程 A 創建三個服務 一主二從模式 B 實現一主二從關係 C 創建sentinel.conf文件 D 增加以下內容 1.sentinel monitor <master-name> <ip> <redis-port> <quorum> 告訴sentinel去監聽地址為ip:port的一個mas ...
  • 1.Slice(切片)代表變長的序列,序列中每個元素都有相同的類型,一個slice類型一般寫作[]T,其中T代表slice中元素的類型;slice的語法和數組很像,只是沒有固定長度而已,slice的底層確實引用一個數組對象 2.內置的len和cap函數分別返回slice的長度和容量 3.s[i:j] ...
  • Java 內部類分為: 1)成員內部類 2)靜態嵌套類 3)方法內部類 4)匿名內部類 內部類的共性 1、內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class文件,但是前面冠以外部類的類名和$符號 。 2、內部類不能用普通的方式訪問。內部類是外部類的一個成員,因此內部類可以自由地訪 ...
  • 方法 綁定方法和非綁定方法 綁定方法和非綁定方法在創建時沒有任何區別,同一方法,既可以為綁定方法,也可以為非綁定方法,一切不同都只在調用時的手法上有所區別。 綁定方法即該方法綁定類的一個實例上,必須將self作為第一個參數傳入,而這個過程是由Python自動完成。可以通過實例名.方法名(參數列表)來 ...
  • 出現這個問題的原因可能很多,但是最終原因都是部署的項目文件中沒有這個類包。 那麼出錯的點在哪呢?逐一排除! 1.首先在項目文件中沒有添加相應的jar包,可以在maven dependencis文件夾中看是否有。如果沒有,在pom文件添加依賴配置即可;如果有,還是出現問題轉第二步 2.在maven的本 ...
  • 概述 UWP Community Toolkit Extensions 中有一個為 View 提供的擴展 - View Extensions,本篇我們結合代碼詳細講解 View Extensions 的實現。 View Extensions 包括了 ApplicationViewExtensions ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...