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

2023-02-17 15:01:05

什麼是委託

委託是一種參照型別,表示對具有特定參數列和返回型別的方法的參照。 在範例化委託時,你可以將其範例與任何具有相容引數返回型別的方法進行繫結。 你可以通過委託範例呼叫方法。

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

委託具有以下屬性:

  • 委託類似於 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)委託:屬於一個定義,是和類、介面類似的,通常放在外部。

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

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

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

彩蛋

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