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 Action 和 Func 背景
抽象的 Delegate 類提供用於鬆散耦合和調用的基礎結構,但是這樣看來,引發一個問題,無論何時需要不同的方法參數,這都會創建新的委托類型。 一段時間後此操作可能變得繁瑣。 每個新功能都需要新的委托類型,幸運的是,沒有必要這樣做,框架已經幫我們定義Action 和 Func 類,我們可以直接申明進行使用
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)委托:屬於一個定義,是和類、介面類似的,通常放在外部。
所以事件這種架構設計思想還是很值得大家去學習的。
所以說,如果你的代碼在不調用任何訂閱伺服器的情況下可完成其所有工作,使用基於事件的設計會更好點。
大家在項目中,怎麼進行選擇,就看實際需求了。
彩蛋
看到這裡的朋友,肯定對委托和事件還是有了一定的瞭解了,畢竟博主很用心的在寫,儘量講細一點。如果大家覺得博主講解的比較全面,且透徹。大家可以點點贊,給予鼓勵。也可以關註博主後續的更新,每一篇都會盡心講解