C# 從1到Core--委托與事件

来源:https://www.cnblogs.com/FlyLolo/archive/2020/06/30/12879190.html
-Advertisement-
Play Games

委托與事件在C#1.0的時候就有了,隨著C#版本的不斷更新,有些寫法和功能也在不斷改變。本文溫故一下這些改變,以及在NET Core中關於事件的一點改變。 一、C#1.0 從委托開始 1. 基本方式 什麼是委托,就不說概念了,用例子說話。 某HR說他需要招聘一個6年 .NET5 研發經驗的“高級”工 ...


  委托與事件在C#1.0的時候就有了,隨著C#版本的不斷更新,有些寫法和功能也在不斷改變。本文溫故一下這些改變,以及在NET Core中關於事件的一點改變。

一、C#1.0 從委托開始

1. 基本方式

  什麼是委托,就不說概念了,用例子說話。

  某HR說他需要招聘一個6年 .NET5 研發經驗的“高級”工程師,他想找人(委托)別人把這條招聘消息發出去。這樣的HR很多,所以大家定義了一個通用的發消息規則:

public delegate string SendDelegate(string message);

  這就像一個介面的方法,沒有實際的實現代碼,只是定義了這個方法有一個string的參數和返回值。所有想發招聘消息的HR只要遵守這樣的規則即可。

委托本質上是一個類,所以它可以被定義在其他類的內部或外部,根據實際引用關係考慮即可。本例單獨定義在外部。

為HR定義了一個名為HR的類:

public class HR
{
    public SendDelegate sendDelegate;
    public void SendMessage(string msg)
    {
        sendDelegate(msg);
    }
}

  HR有一個SendDelegate類型的成員,當它需要發送消息(SendMessage)的時候,只需要調用這個sendDelegate方法即可。而不需要實現這個方法,也不需要關心這個方法是怎麼實現的。

當知道這個HR需要發送消息的時候,獵頭張三接了這個幫忙招人的工作。獵頭的類為Sender,他有一個用於發送消息的方法Send,該方法恰好符合眾人定義的名為SendDelegate的發消息規則。這有點像實現了一個介面方法,但這裡不要求方法名一致,只是要求方法的簽名一致。

public class Sender
{
    public Sender(string name)
    {
        this.senderName = name;
    }

    private readonly string senderName;
    public string Send(string message)
    {
        string serialNumber = Guid.NewGuid().ToString();
        Console.WriteLine(senderName + " sending....");
        Thread.Sleep(2000);
        Console.WriteLine("Sender: " + senderName + " , Content: " + message + ", Serial Number: "  + serialNumber);
        return serialNumber;
    }
}

獵頭幫助HR招人的邏輯如下:

public void Test()
{
    //一個HR
    HR hr = new HR();

    //獵頭張三來監聽,聽到HR發什麼消息後立刻傳播出去
    Sender senderZS = new Sender("張三");
    hr.sendDelegate = senderZS.Send;

//HR遞交消息 hr.SendMessage("Hello World"); }

獵頭將自己的發消息方法“賦值”給了HR的SendDelegate方法,為什麼可以“賦值”? 因為二者都遵守SendDelegate規則。 就像A和B兩個變數都是int類型的時候,A可以賦值給B一樣。

這就是一個簡單的委托過程,HR將招人的工作委托給了獵頭,自己不用去做招人的工作。

但經常一個招聘工作經常會有多個獵頭接單,那就有了多播委托。

2. 多播委托

 看一下下麵的代碼:

public void Test()
{
    //一個HR
    HR hr = new HR();

    //獵頭張三來監聽,聽到HR發什麼消息後立刻傳播出去
    Sender senderZS = new Sender("張三");
    hr.sendDelegate = senderZS.Send;

    //快嘴李四也來了
    Sender senderLS = new Sender("李四");
    hr.sendDelegate += senderLS.Send;
//HR遞交消息 hr.SendMessage("Hello World"); }

與之前的代碼改變不大, 只是添加了李四的方法綁定,這樣HR發消息的時候,張三和李四都會發出招人的消息。

這裡要註意李四綁定方法的時候,用的是+=而不是=,就像拼接字元串一樣,是拼接而不是賦值,否則會覆蓋掉之前張三的方法綁定。

對於第一個綁定的張三,可以用=號也可以用+=(記得之前好像第一個必須用=,實驗了一下現在二者皆可)。

這同時也暴露了一些問題:

  • 如果後面的獵頭接單的時候不小心(故意)用了=號, 那麼最終前面的人的綁定都沒有了,那麼他將獨占這個HR客戶,HR發出的消息只有他能收到。
  • 可以偷偷的調用獵頭的hr.sendDelegate
public void Test()
{
    //一個HR
    HR hr = new HR();

    //大嘴張三來監聽,聽到HR發什麼消息後立刻傳播出去
    Sender senderZS = new Sender("張三");
    //hr.sendDelegate -= senderZS.Send; //即使未進行過+=  直接調用-=,也不會報錯
    hr.sendDelegate += senderZS.Send;

    //快嘴李四也來了
    Sender senderLS = new Sender("李四");
    hr.sendDelegate += senderLS.Send;

    //移除
    //hr.sendDelegate -= senderZS.Send;

    //風險:註意上面用的符號是+=和-=   如果使用=,則是賦值操作,
    //例如下麵的語句會覆蓋掉之前所有的綁定
    //hr.sendDelegate = senderWW.Send;

    //HR遞交消息
    hr.SendMessage("Hello World");

    //風險:可以偷偷的以HR的名義偷偷的發了一條消息    sendDelegate應該只能由HR調用   
    hr.sendDelegate("偷偷的發一條");

}

3. 通過方法避免風險

  很自然想到採用類似Get和Set的方式避免上面的問題。既然委托可以像變數一樣賦值,那麼也可以通過參數來傳值,將一個方法作為參數傳遞。

 

    public class HRWithAddRemove
    {
        private SendDelegate sendDelegate;

        public void AddDelegate(SendDelegate sendDelegate)
        {
            this.sendDelegate += sendDelegate; //如果需要限制最多綁定一個,此處可以用=號
        }

        public void RomoveDelegate(SendDelegate sendDelegate)
        {
            this.sendDelegate -= sendDelegate;
        }

        public void SendMessage(string msg)
        {
            sendDelegate(msg);
        }
    }

經過改造後的HR,SendDelegate方法被設置為了private,之後只能通過Add和Remove的方法進行方法綁定。

4.模擬多播委托機制

通過上面委托的表現來看,委托就像是保存了一個相同方法名的集合 List<SendDelegate> ,可以向集合中添加或移除方法,當調用這個委托的時候,會逐一調用該集合中的各個方法。

例如下麵的代碼( 註意這裡假設SendDelegate只對應一個方法 ):

public class HR1
{
    public void Delegate(SendDelegate sendDelegate)
    {
        sendDelegateList = new List<SendDelegate> { sendDelegate }; //對應=
    }

    public void AddDelegate(SendDelegate sendDelegate)
    {
        sendDelegateList.Add(sendDelegate); //對應+=
    }

    public void RomoveDelegate(SendDelegate sendDelegate)
    {
        sendDelegateList.Remove(sendDelegate);//對應-=
    }

    public List<SendDelegate> sendDelegateList;

    public void SendMessage(string msg)
    {
        foreach (var item in sendDelegateList)
        {
            item(msg);
        }
    }
}

二、C#1.0 引入事件

  1.簡單事件

  如果既想使用-=和+=的方便,又想避免相關功能開閉的風險怎麼辦呢?可以使用事件:

    public class HRWithEvent
    {
        public event SendDelegate sendDelegate;
        public void SendMessage(string msg)
        {
            sendDelegate(msg);
        }
    }

  只是將SendDelegate前面添加了一個event標識,雖然它被設置為public,但如下代碼卻會給出錯誤提示: 事件“HRWithEvent.sendDelegate”只能出現在 += 或 -= 的左邊(從類型“HRWithEvent”中使用時除外) 

 hr.sendDelegate = senderZS.Send;
 hr.sendDelegate("偷偷的發一條");

  2.事件的訪問器模式

   上文為委托定義了Add和Remove方法,而事件支持這樣的訪問器模式,例如如下代碼:

    public class CustomerWithEventAddRemove
    {
        private event SendDelegate sendDelegate;

        public event SendDelegate SendDelegate
        {
            add { sendDelegate += value; }
            remove { sendDelegate -= value; }
        }
        public void SendMessage(string msg)
        {
            sendDelegate(msg);
        }
    }

 

  可以像使用Get和Set方法一樣,對事件的綁定與移除進行條件約束。 

  3. 控制綁定事件的執行

  當多個委托被綁定到事件之後,如果想精確控制各個委托的運行怎麼辦,比如返回值(雖然經常為void)、異常處理等。

第一章第4節通過一個List<SendDelegate> 模擬了多播委托的綁定。 會想到如果真能迴圈調用一個個已綁定的委托,就可以精確的進行控制了。那麼這裡說一下這樣的方法:

 

    public class HRWithEvent
    {
        public event SendDelegate sendDelegate;
        public void SendMessage(string msg)
        {
            //sendDelegate(msg);  此處不再一次性調用所有
            if (sendDelegate != null)
            {
                Delegate[] delegates = sendDelegate.GetInvocationList(); //獲取所有已綁定的委托
                foreach (var item in delegates)
                {
                    ((SendDelegate)item).Invoke(msg); //逐一調用
                }
            }

        }
    }

  這裡通過Invoke方法逐一調用各個Delegate,從而實現對每一個Delegate的調用的控制。若需要非同步調用,則可以通過BeginInvoke方法實現(.NET Core之後不再支持此方法,後面會介紹。)

((SendDelegate)item).BeginInvoke(msg,null,null);

  4. 標準的事件寫法

  .NET 事件委托的標準簽名是:

void OnEventRaised(object sender, EventArgs args);

 

  返回類型為 void。 事件基於委托,而且是多播委托。 參數列表包含兩種參數:發件人和事件參數。 sender 的編譯時類型為 System.Object

  第二種參數通常是派生自 System.EventArgs 的類型.NET Core 中已不強制要求繼承自System.EventArgs,後面會說到)

  將上面的例子修改一下,改成標準寫法,大概是下麵代碼的樣子:

public class HRWithEventStandard
{
    public delegate void SendEventHandler(object sender, SendMsgArgs e);
    public event SendEventHandler Send;
    public void SendMessage(string msg)
    {
        var arg = new SendMsgArgs(msg);
        Send(this,arg); //arg.CancelRequested 為最後一個的值   因為覆蓋
    }
}

public class SendMsgArgs : EventArgs
{
    public readonly string Msg = string.Empty;
    public bool CancelRequested { get; set; }
    public SendMsgArgs(string msg)
    {
        this.Msg = msg;
    }
}

 

 

三、隨著C#版本改變

1. C#2.0 泛型委托

  C#2.0 的時候,隨著泛型出現,支持了泛型委托,例如,在委托的簽名中可以使用泛型,例如下麵代碼

public delegate string SendDelegate<T>(T message);

這樣的委托適用於不同的參數類型,例如如下代碼(註意使用的時候要對應具體的類型)

public delegate string SendDelegate<T>(T message);

public class HR1
{
    public SendDelegate<string> sendDelegate1;
    public SendDelegate<int> sendDelegate2;
    public SendDelegate<DateTime> sendDelegate3;
}

public static class Sender1
{
    public static string Send1(string msg)
    {
        return "";
    }

    public static string Send2(int msg)
    {
        return "";
    }
}
    
public class Test
{
    public void TestDemo()
    {
        HR1 hr1 = new HR1();
        hr1.sendDelegate1 = Sender1.Send1; // 註意使用的時候要對應具體的類型
        hr1.sendDelegate2 = new SendDelegate<int>(Sender1.Send2);
        hr1.sendDelegate3 = delegate (DateTime dateTime) { return dateTime.ToLongDateString(); };

    }
}

2. C#2.0 delegate運算符

delegate 運算符創建一個可以轉換為委托類型的匿名方法:

例如上例中這樣的代碼:

hr1.sendDelegate3 = delegate (DateTime dateTime) { return dateTime.ToLongDateString(); };

3. C#3.0 Lambda 表達式

從 C# 3 開始,lambda 表達式提供了一種更簡潔和富有表現力的方式來創建匿名函數。 使用 => 運算符構造 lambda 表達式,

例如“delegate運算符”的例子可以簡化為如下代碼:

hr1.sendDelegate3 = (dateTime) => { return dateTime.ToLongDateString(); };

 

4.C#3,NET Framework3.5,Action 、Func、Predicate

Action 、Func、Predicate本質上是框架為我們預定義的委托,在上面的例子中,我們使用委托的時候,首先要定義一個委托類型,然後在實際使用的地方使用,而使用委托只要求方法名相同,在泛型委托出現之後,“定義委托”這一操作就顯得越來越累贅,為此,系統為我們預定義了一系列的委托,我們只要使用即可。

例如Action的代碼如下:

    public delegate void Action();
    public delegate void Action<in T>(T obj);
    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
    public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
    public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15);
    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);

實際上定義了最多16個參數的無返回值的委托。

Func與此類似,是最多16個參數的有返回值的委托。Predicate則是固定一個參數以及bool類型返回值的委托。

public delegate bool Predicate<T>(T obj);

 5. .NET Core 非同步調用

第2.3節中,提示如下代碼在.NET Core中已不支持

((SendDelegate)item).BeginInvoke(msg,null,null);

 

會拋出異常:

System.PlatformNotSupportedException:“Operation is not supported on this platform.”

 

需要非同步調用的時候可以採用如下寫法:

Task task = Task.Run(() => ((SendDelegate)item).Invoke(msg));

 

對應的 EndInvoke() 則改為: task.Wait(); 

 

 5. .NET Core的 EventHandler<TEventArgs>

.NET Core 版本中,EventHandler<TEventArgs> 定義不再要求 TEventArgs 必須是派生自 System.EventArgs 的類, 使我們使用起來更為靈活。

例如我們可以有這樣的寫法:

EventHandler<string> SendNew

 

這在以前的版本中是不允許的。

 


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

-Advertisement-
Play Games
更多相關文章
  • 見了鬼! 工資竟然又跌了 #平均工資 2020年6月全國招收程式員313739人。2020年6月全國程式員平均工資14404元,工資中位數12500元,其中95%的人的工資介於5250元到35000元。 雖然收入又下降了,但是崗位比上個月多了起來,隨著經濟好轉,收入還是會漲回去的。大家要有信心。 # ...
  • 學了一段時間java,接觸了mvc設計模式,但是對設計模式概念比較模糊,通過百度瞭解了一下.這裡簡單總結一下. 設計模式(Design pattern),百度說是也叫軟體設計模式,是一套被反覆使用,多數人知曉的,經過分類編目的,代碼設計經驗的總結. 使用設計模式是為了可重用代碼,讓代碼更容易被他人理 ...
  • from typing import Listclass Solution: def minimumTotal1(self, triangle: List[List[int]]) -> int: return self.dfs(triangle,0,0,len(triangle),0) # 深搜的做 ...
  • 作為一個潛入IT圈五年之久、看過數萬份簡歷的HR,在這個問題上還是有點發言權的。HR在篩選簡歷時主要從公司需求出發,重點不一,不過還是有很多“通用”的套路,為了在30秒內判斷出這份簡歷是否值得跟進,我認為程式員寫簡歷的正確姿勢是這樣的: 一、基本格調 即打開簡歷之後的第一印象。就好比我們看見一個人, ...
  • Object A =new Object();java中new一個對象要經歷哪些過程首先java在new一個對象的時候,會先查看對象所屬的類有沒有被載入到記憶體,如果沒有的話就會先通過類的全限定名將對象所屬的.class文件載入到記憶體中。載入並初始化類完成後,再進行對象的創建工作。(全限定名有絕對路徑 ...
  • import shelve a = shelve.open('1') b = [1,2,3] a['b'] = b a.close()a['b'] Traceback (most recent call last):File "C:\Users\Administrator\AppData\Local ...
  • 最近在折騰一些控制相關的軟體設計,想起來狀態機這個東西,對解決一些控制系統狀態切換還是挺有用的。 狀態機(有限狀態自動機)網上有很多介紹。簡單理解就是定義一系列狀態,通過一系列的事件,可以使得狀態可以相互之間切換。 如果不使用狀態機的思想來編程,那麼針對過程的編程方法會使得程式拓展性變差,並且不容易 ...
  • 悲哀的就是,想吃技術飯,那就要走專家路線,但是中國軟體開發絕大多數是應用,能給得起錢的也是應用,對專家的需求就沒多少。 這條路才真的是獨木橋,走到後來,你會發現,你潛心研究的技術都是狗屁,不賺錢。 例如某個回答里提到的,悲觀鎖樂觀鎖。我也曾經喜歡研究這類問題,但是後來我發現,一個框架就搞定了。我們的 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...