C# 委托原理刨析、事件原理刨析,外加兩者對比

来源:https://www.cnblogs.com/Bob-luo/archive/2023/02/17/17129790.html
-Advertisement-
Play Games

C# 委托原理刨析,和事件原理刨析,外加兩者對比,應該是目前全網講的最細的帖子了吧。從委托介紹=》基本使用=》框架應用=》原理分析=》事件原理分析=》兩者對比 ...


什麼是委托

委托是一種引用類型,表示對具有特定參數列表和返回類型的方法的引用。 在實例化委托時,你可以將其實例與任何具有相容參數返回類型的方法進行綁定。 你可以通過委托實例調用方法。

簡單的理解,委托是方法的抽象類,它定義了方法的類型,可以實例化。和普通的類一樣,可以申明變數進行賦值,可以當作參數傳遞,可以定義成屬性。

委托具有以下屬性:

  • 委托類似於 C++ 函數指針,但委托完全面向對象,不像 C++ 指針會記住函數,委托會同時封裝對象實例和方法。
  • 委托允許將方法作為參數進行傳遞。
  • 委托可用於定義回調方法。
  • 委托可以鏈接在一起;具備單播、多播功能。
  • 方法不必與委托類型完全匹配。 有關詳細信息,請參閱使用委托中的變體
  • 使用 Lambda 表達式可以更簡練地編寫內聯代碼塊。 Lambda 表達式(在某些上下文中)可編譯為委托類型。

1.委托基礎介紹

1.1 delegate委托的聲明

使用 delegate 關鍵字,定義具體的委托類型,Delegate至少0個參數,至多32個參數,可以無返回值,也可以指定返回值類型。

查看代碼
namespace ConsoleApp.DelegateTest
{
    //例:表示無參數,無返回。
    public delegate void MethodtDelegate();
    //例:表示有兩個參數,並返回int型。
    public delegate int MethodtDelegate(int x, int y);
}

方法綁定,進行調用

查看代碼
static void Main(string[] args)
        {
            MethodtDelegate methodt = Test;
            //例1:直接調用
            methodt(1,2);
            //例2:假設作為參數傳遞,進行調用。比如回調函數場景
            InvokeTest(methodt);
        }
        public static int Test(int a, int b)
        {
            return a + b;
        }
        public static void InvokeTest(MethodtDelegate methodt)
        {
            //以下兩種方式都可以調用
            var sum = methodt(1, 2);
            var sum = methodt.Invoke(1, 2);
        }

1.2 ActionFunc 背景

抽象的 Delegate 類提供用於鬆散耦合和調用的基礎結構,但是這樣看來,引發一個問題,無論何時需要不同的方法參數,這都會創建新的委托類型。 一段時間後此操作可能變得繁瑣。 每個新功能都需要新的委托類型,幸運的是,沒有必要這樣做,框架已經幫我們定義ActionFunc 類,我們可以直接申明進行使用

1.3 Action<T> 類

Action是無返回值的泛型委托。Action 委托的變體可包含多達 16 個參數,如 Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>。 重要的是這些定義對每個委托參數使用不同的泛型參數,這樣可以具有最大的靈活性。框架源碼,如圖:

使用就很方便了,我們只需要直接申明委托類型進行使用,例:

查看代碼

//例:表示有傳入參數int,string,bool無返回值的委托
Action<int,string,bool> 

1.4 Func<T> 類

Func 委托的變體可包含多達 16 個輸入參數,如 Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>。 按照約定,返回結果的類型始終是所有 Func 聲明中最後一個參數的類型,利用out類型參數實現。

Func是有返回值的泛型委托,func至少0個參數,至多16個參數,根據返回值泛型返回。必須有返回值,不可void。框架源碼,如下:

使用就很方便了,我們只需要直接申明委托類型進行使用,例:

查看代碼

//表示無參,返回值為int的委托,
Func<int> 
//表示傳入參數為object, string 返回值為int的委托
Func<object,string,int> 

2. 委托實戰案例

我這裡就做一個多播案例,幫助大家理解,其實.NET core 日誌框架和其他第三方日誌框架,差不多就是這種套路

2.1 定義Logger類

這個類我們的定義好委托和調用委托的方法。

查看代碼

    public static class Logger
    {
        public static Action<string> WriteMessage;

        public static void LogMessage(string msg)
        {
            WriteMessage(msg);
        }
    }

2.2 定義文件記錄器

一個寫入文件的,文件記錄器

查看代碼

    public class FileLogger
    {
        public FileLogger()
        {
            Logger.WriteMessage += LogMessage;
        }

        public void DetachLog() => Logger.WriteMessage -= LogMessage;
        // make sure this can't throw.
        private void LogMessage(string msg)
        {
            try
            {
                Console.WriteLine($"FileLogger\t{msg}");
            }
            catch (Exception)
            {
                // Hmm. We caught an exception while
                // logging. We can't really log the
                // problem (since it's the log that's failing).
                // So, while normally, catching an exception
                // and doing nothing isn't wise, it's really the
                // only reasonable option here.
            }
        }
    }

2.3 定義資料庫記錄器

一個寫入不同資料庫的,資料庫記錄器

查看代碼

   public class DBLogger
    {
        private readonly string name;
        public DBLogger(string name)
        {
            this.name = name;
            Logger.WriteMessage += LogMessage;
        }

        public void DetachLog() => Logger.WriteMessage -= LogMessage;
        // make sure this can't throw.
        private void LogMessage(string msg)
        {
            try
            {
                Console.WriteLine($"DBLogger{name}\t{msg}");
            }
            catch (Exception)
            {
                // Hmm. We caught an exception while
                // logging. We can't really log the
                // problem (since it's the log that's failing).
                // So, while normally, catching an exception
                // and doing nothing isn't wise, it's really the
                // only reasonable option here.
            }
        }
    }

以上兩個代碼邏輯,博主就不介紹了,就用一個控制台輸出,代表業務代碼了

2.4 測試

測試一下,廣播和委托刪除效果

查看代碼
static void Main(string[] args)
        {
            //添加一個文件記錄器和兩個資料庫記錄器
            new FileLogger();
            new DBLogger("DB1");
            var a = new DBLogger("DB2");
            //調用委托
            Logger.LogMessage("add失敗");
    
            //刪除此資料庫記錄器
            a.DetachLog();
            Console.WriteLine("======DetachLogDB2========");
            //調用委托
            Logger.LogMessage("add失敗");
        }

運行效果:

在實際項目中,大家就自行發揮

3. 委托變數捕獲

3.1效果演示

說到委托,博主也把這個重要的知識點講解一下,這個知識點很多人可能不知道或者踩過坑,但掌握了這個知識點其實可以實現一些比較花哨功能。

這裡博主就用一個案例進行體現變數捕獲,這裡代碼博主就用 lambda 表達式 進行簡寫,不太熟悉的可以通過鏈接跳轉進行學習。

邏輯就是,簡單的累計一下數量,通過最終的值體現。這裡博主分別申明兩個整數型變數,通過兩個委托分別累計,然後看各自的值。兩個委托區別就是傳值方式的不同。

查看代碼
        static void Main(string[] args)
        {
            int count1 = 0;//委托1的參數
            int count2 = 0;//委托2的參數
            //實例化委托1
            Action<int> action1 = (p) =>
            {
                p++;
                Console.WriteLine("action1:" + p);
            };
            //實例化委托2
            Action action2 = () =>
            {
                count2++;
                Console.WriteLine("action2:" + count2);
            };
            //迴圈5此
            for (int i = 0; i < 5; i++)
            {
                action1(count1);//調用委托1
                action2();//調用委托2
                Console.WriteLine("---------------------------分割線");
            }
            Console.WriteLine("count1 最終值:" + count1);
            Console.WriteLine("count2 最終值:" + count2);
        }

測試效果:

大家發現沒?邏輯代碼一下,只是參數傳遞方式不一樣,結果截然不同:

委托1的方式:不改變變數的值,方法之間是不共用這個參數的。這種很容易理解,就和我們調用普通方法一樣,變數是值類型,是拷貝了一個副本傳給了方法進行使用

委托2的方式:改變變數的值,方法之間是共用這個參數的。這種就像引用類型參數一樣,是不是很神奇,難道是利用了ref關鍵字實現的?

3.2原理刨析

其實沒有大家想學的那麼神秘,委托之所以使用方式和類無異,是因為它本身就是一個類,只是這個過程的定義由編譯器幫我們做了,我們只需要使用C#的語法糖。接下來博主就帶大家揭開委托的神秘面紗。

我也給大家畫一個簡單的編譯=》執行的過程

3.2.1 委托真實面貌

博主就簡單寫了一個委托,然後通過IL DASM工具查看IL代碼

查看代碼

    internal class Program
    {
        static void Main(string[] args)
        {
            int b = 888888888;
            Func<int> action = () =>
            {
                return b++;
            };
            var a = action.Invoke();
        }
    }

3.2.2模擬委托調用過程
查看代碼

    internal class Program
    {
        public class DisplayClass
        {
            public int b;
            public int Invoke()
            {
                return b++;
            }
        }
        public class _Func<T>
        {
            private readonly DisplayClass displayClass;
            public _Func(DisplayClass display)
            {
                displayClass = display;
            }
            public T Invoke()
            {
                object b = displayClass.Invoke();
                return (T)b;
            }
        }
        static void Main(string[] args)
        {
            var display = new DisplayClass();
            display.b = 888888888;
            var actionTest = new _Func<int>(display);
            var a = actionTest.Invoke();
        }
    }

 

大家發現沒,最終的IL代碼一模一樣。也就說,委托就是編譯器幫我們把func編譯成一個帶invoke函數的func類和生成一個裝捕獲的變數和函數體的類,然後通過構造函數將對象引用和函數指針(獲取指針就是大家所說的把非托管指針壓入當前)傳給func類的實例化。然後最終調用的時候,委托類的invoke函數會去調用真正的函數。就這樣完成了對函數的抽象。

3.2.3 委托變數生命周期

現在大家是不是對委托有了一定的理解了,而委托涉及到的捕獲變數和參數變數,生命周期就說得通了,也知道為啥委托改變了變數,能通知到原本的變數,因為對變數就行了類的裝箱,打包成了一個一個引用類型,那方法外部當然知道變數的值被改變了,因為大家都是拿著引用對象的地址呀。下麵做個生命周期小總結:

  • p變數是普通變數,當方法被銷毀時,它就會被銷毀。
  • count2變數是捕獲變數,當委托實例被銷毀時,它才會被銷毀。

4. 事件

其實講完委托,事件就很容易理解了, 博主就簡單講解一下,如果大家有需要,博主就再寫一篇詳細的講解。

事件:實際上,事件是建立在對委托的語言支持之上的一種設計而已。

4.1 事件定義語法

/定義一個委托
4     public delegate void delegateRun();
5     //定義一個事件
6     public event delegateRun eventRun;

簡單的說,事件可以看作是一個委托類型的變數

4.2委托和事件共性:

它們都提供了一個後期綁定方案:在該方案中,組件通過調用僅在運行時識別的方法進行通信。 它們都支持單個和多個訂閱伺服器方法。 也就是單播和多播支持。 二者均支持用於添加和刪除處理程式的類似語法。 最後,引發事件和調用委托使用完全相同的方法調用語法。 它們甚至都支持與 ?. 運算符一起使用的相同的 Invoke() 方法語法。

4.3 事件原理刨析

public event EventHandler<NewMailEventArgs> NewMail;  

可以看到當我們定義一個NewEvent時,編譯器幫我們生成了:1. 一個private NewMail 欄位,類型為 EventHandler<NewMailEventArgs>。 2.一個 add_NewMail 方法,用於將委托添加到委托鏈(內部調用了Delegate.Combine方法)。3.一個 remove_NewMail 方法,用於將委托從委托鏈移除(內部調用了Delegate.Remove方法)。對事件的操作,就是是對NewMail欄位的操作。

4.4 如何選擇

主要區別就是:

    1.事件處理程式通過修改事件參數對象的屬性將信息傳回到事件源。 雖然這些慣用語可發揮作用,但它們不像從方法返回值那樣自然。

    2.包含事件的類以外的類只能添加和刪除事件偵聽器;只有包含事件的類才能調用事件。 事件通常是公共類成員。 相比之下,委托通常作為參數傳遞,並存儲為私有類成員(如果它們全部存儲)

    3.當事件源將在很長一段時間內引發事件時,基於事件的設計會更加自然。比如基於事件的 UI 控制項設計案例

總結:

(1)事件:事件時屬於類的成員,所以要放在類的內部。

(2)委托:屬於一個定義,是和類、介面類似的,通常放在外部。

所以事件這種架構設計思想還是很值得大家去學習的。

所以說,如果你的代碼在不調用任何訂閱伺服器的情況下可完成其所有工作,使用基於事件的設計會更好點。

大家在項目中,怎麼進行選擇,就看實際需求了。

彩蛋

看到這裡的朋友,肯定對委托和事件還是有了一定的瞭解了,畢竟博主很用心的在寫,儘量講細一點。如果大家覺得博主講解的比較全面,且透徹。大家可以點點贊,給予鼓勵。也可以關註博主後續的更新,每一篇都會盡心講解

 


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

-Advertisement-
Play Games
更多相關文章
  • 什麼是素數? 質數又稱素數。一個大於1的自然數,除了1和它自身外,不能被其他自然數整除的數叫做質數;否則稱為合數(規定1既不是質數也不是合數)。 實際案例 比如我們想找出1-1000的所有素數 思路1 可以先定義一個方法 primeNumber//參數列表用來接收所想找的素數範圍 public st ...
  • 本文介紹基於Python的隨機森林(Random Forest,RF)回歸代碼,以及模型超參數(包括決策樹個數與最大深度、最小分離樣本數、最小葉子節點樣本數、最大分離特征數等)自動優化的代碼~ ...
  • nano /etc/motd // _ooOoo_ // // o8888888o // // 88" . "88 // // (| ^_^ |) // // O\ = /O // // ____/` '\____ // // .' \\| |// `. // // / \\||| : |||// ...
  • 本文敘述的問題的根源在於對C#基礎知識掌握不牢固,從而在遇到難以理解的問題時浪費了大量時間; 在此也警示自己,該啃啃基礎書籍了! 話不多說,先上代碼: 先看問題 services.AddEasyCaching(option => { option.UseHybrid(config => { .... ...
  • ###語法: select * from 表名 where 條件1 start with 條件2 connect by 條件3; 條件2: 是根結點的限定語句,當然可以放寬限定條件,以取得多個根結點,實際就是多棵樹。 條件3: 是連接條件,其中用PRIOR表示上一條記錄,比如 CONNECT BY ...
  • 1.說說顯示實現介面和隱式實現介面的區別。 2.說說file訪問修飾的作用。 3.說說什麼是原始字元串。 4.C#10 中struct有什麼改進? 5.說說C#10中Lambda表達式的新特點。 6.說說對於泛型特性的理解。 7.說說在ASP.NET Core7中,依賴註入中的方法註入需要註意什麼? ...
  • 我相信很多人看了其他的貼子,都沒有成功部署,因為裡面有很多暗坑。接下來博主就一步一步給大家講明白,帶領大家部署 先基本的發佈 操作:右擊web項目的《發佈》按鈕。選文件 配置發佈屬性 部署模式,建議選框架依賴,且安裝對應的運行時框架,可共用系統級版本的 .NET Core,如果框架依賴部署不行,可以 ...
  • 在日常工作中,我們有時會需要修改字體的顏色來突出文本重點,讓讀者更容易抓住文章要點。在今天這篇文章中,我將為大家介紹如何以編程方式,在Word更改字體顏色。本文將分為兩部分分別介紹如何實現此操作。以下是我整理的步驟及方法,並附上C#/VB.NET代碼供大家參考。 更改段落字體顏色 更改特定文本字體顏 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...