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

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

1、C#1.0 從委託開始

1. 基本方式

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

  某HR說他須要招聘一個6年 .NET5 研發經驗的「高級」工程師,他想找人(委託)別人把這條招聘消息發出去。這樣的HR不少,因此你們定義了一個通用的發消息規則:函數

public delegate string SendDelegate(string message);

  這就像一個接口的方法,沒有實際的實現代碼,只是定義了這個方法有一個string的參數和返回值。全部想發招聘消息的HR只要遵照這樣的規則便可。ui

委託本質上是一個類,因此它能夠被定義在其餘類的內部或外部,根據實際引用關係考慮便可。本例單獨定義在外部。this

爲HR定義了一個名爲HR的類:spa

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

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

當知道這個HR須要發送消息的時候,獵頭張三接了這個幫忙招人的工做。獵頭的類爲Sender,他有一個用於發送消息的方法Send,該方法剛好符合衆人定義的名爲SendDelegate的發消息規則。這有點像實現了一個接口方法,但這裏不要求方法名一致,只是要求方法的簽名一致。orm

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招人的邏輯以下:blog

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);
        }
    }
}

2、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;
    }
}

 

 

3、隨着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

 

這在之前的版本中是不容許的。

相關文章
相關標籤/搜索