C# delegate 委托 event關(guān)鍵字

參考
Unity游戲開發(fā)——對(duì)委托的理解

一、委托簡(jiǎn)介

委托也就是delegate是一個(gè)引用類型,他相當(dāng)于一個(gè)裝著方法的容器,他可以把方法作為對(duì)象進(jìn)行傳遞,但前提是委托和對(duì)應(yīng)傳遞方法的簽名得是相同的,簽名指的是他們的參數(shù)類型和返回值類型

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    // 聲明一個(gè)委托類型
    public delegate void MyHandler(int a);
    
        // 聲明了委托類型的實(shí)例
    public MyHandler myHandler;

    private void Start()
    {
        // 一對(duì)一依賴
        myHandler = PrintNum;
        myHandler(10);
        myHandler = PrintNumDouble;
        myHandler(4);       
    }

    void PrintNum(int a)
    {
        Debug.Log(a);
    }

    void PrintNumDouble(int b)
    {
        Debug.Log(b * 2);
    }   
}

聲明了一個(gè)委托類型,可以粗暴的理解為我們創(chuàng)建了一個(gè)新的引用類型,我們可以使用這個(gè)新創(chuàng)建的引用類型來(lái)聲明實(shí)例變量。

        // 聲明了一個(gè)委托類型的實(shí)例變量
    public MyHandler myHandler;
    
    // 聲明一個(gè)類的實(shí)例變量
    public TestClass myTestClass;

接著我們又聲明了兩個(gè)跟委托類型具有相同簽名的方法(返回值類型和參數(shù)類型相同),最后我們?cè)趕tart方法里把具有相同簽名的方法賦值給了委托實(shí)例,然后直接進(jìn)行了方法回調(diào)

也可以一對(duì)多依賴

    private void Start()
    {       
        // 一對(duì)多依賴
        myHandler += PrintNum;
        myHandler += PrintNumDouble;
        myHandler(5); 
    }

我們還可以在別的腳本上也添加對(duì)委托實(shí)例的監(jiān)聽

using UnityEngine;

public class CallBackTest : MonoBehaviour 
{
    private void Start()
    {
        GetComponent<DelegateTest>().myHandler += PrintReceive;
    }

    private void PrintReceive(int a)
    {
        Debug.Log("reveice : " + a);
    }
}
二、消息機(jī)制
1.Unity中的消息系統(tǒng)

既然提到了委托與觀察者模式,那么Unity中是否已經(jīng)存在了消息機(jī)制呢?答案是肯定的,這套內(nèi)置的消息機(jī)制主要圍繞著SendMessage和BroadcastMessage而構(gòu)建。但是這套機(jī)制是存在一些缺陷的

  • 發(fā)送和接收消息都過(guò)于依賴反射來(lái)查找消息對(duì)應(yīng)的被調(diào)用函數(shù),頻繁使用反射自然會(huì)影響性能。
  • 使用字符串來(lái)標(biāo)識(shí)一個(gè)方法會(huì)帶來(lái)很高的維護(hù)成本,比如方法名字重構(gòu)甚至刪除了,編輯器是不會(huì)報(bào)錯(cuò)的。
  • 由于使用了反射機(jī)制,是可以調(diào)用私有方法的,很多人可能會(huì)因?yàn)榭吹搅怂接蟹椒](méi)有被調(diào)用過(guò)而刪除了這段廢棄代碼,同樣編輯器并不會(huì)報(bào)錯(cuò),甚至程序也能正常運(yùn)行,但是如果觸發(fā)了這個(gè)消息,隱患就會(huì)爆發(fā)。
2.使用C#的委托來(lái)實(shí)現(xiàn)一個(gè)自己的消息機(jī)制

參考unity-針對(duì)于消息機(jī)制的學(xué)習(xí) 一

消 息 類MMMessage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 消息類
/// </summary>
public class MMMessage  {
    //成員變量   發(fā)送消息的名字
    public string Name{
        get;
        private set;
    }
    //成員變量   發(fā)送消息的消息主體
    public object Boby {
        get;
        private set;
    }
    //構(gòu)造函數(shù)   傳值賦值
    public MMMessage(string name,object boby){
        Name = name;
        Boby = boby;
    }
}

消息名稱列表MMMessageName

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 發(fā)送的消息名
/// </summary>
public class MMMessageName {

    public const string START_UP = "startUp";
}

消息控制中心MMMessageCenter

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 消息中心    消息控制類
/// </summary>
public class MMMessageCenter {
    //定義一個(gè)消息類委托 類型 
    public delegate void messageDelHandle(MMMessage message);

    //定義消息控制類的單例
    private static MMMessageCenter instance;
    public static MMMessageCenter Instance {
        get {
            //若是instance為空,初始實(shí)例化一個(gè)消息控制中心
            if (instance == null) {
                instance = new MMMessageCenter();
            }
            return instance;

        }
    }
    //**定義一個(gè)字典  <消息名稱, 委托消息>    消息列表**
    private Dictionary<string, messageDelHandle> messageMap = new Dictionary<string, messageDelHandle>();

    /// <summary>
    /// **注冊(cè)監(jiān)聽**
    /// </summary>
    /// <param name="messageName">消息名稱</param>
    /// <param name="handle">消息內(nèi)容</param>
    public void RigisterListener(string messageName, messageDelHandle handle) {
        if (handle == null) return;  //若是消息為空  退出方法
        if (!messageMap.ContainsKey( messageName )) {
            messageMap.Add( messageName, handle );
        }//若是消息名稱不存在,添加到消息列表
    }

    /// <summary>
    /// **移除/注銷監(jiān)聽**
    /// </summary>
    /// <param name="messageName">消息名稱</param>
    /// <param name="handle">消息內(nèi)容</param>
    public void RemoveoListener(string messageName, messageDelHandle handle) {
        if (!messageMap.ContainsKey( messageName ))
            return; //若是消息列表里沒(méi)有這個(gè)消息名稱鍵值,已經(jīng)注銷直接退出
        messageMap[ messageName ] -= handle;  //若是存在,去除當(dāng)前handle的消息內(nèi)容
        if (messageMap[ messageName ] == null) {
            messageMap.Remove( messageName );
        }   //若是消息列表里當(dāng)前消息數(shù)量為空,則清除該消息名稱

    }
    /// <summary>
    /// **發(fā)送消息**
    /// </summary>
    /// <param name="messageName">消息名字</param>
    /// <param name="boby">消息主體,可以為空</param>
    public void sendMessage(string messageName,object boby = null) {
        if (!messageMap.ContainsKey( messageName ))
            return;  //若是消息名稱不存在   返回
        messageDelHandle handle;  //聲明定義
        messageMap.TryGetValue(messageName,out handle);
        if (handle != null) {
            handle(new MMMessage(messageName,boby));
        }
    }
}

使用

void Start () {
        //發(fā)送消息
        MMMessageCenter.Instance.sendMessage(MMMessageName.START_UP);
}
    void Awake () {
        //注冊(cè)監(jiān)聽事件
        MMMessageCenter.Instance.RigisterListener( MMMessageName.START_UP, StartUp );
    }

    private void StartUp(MMMessage message) {
        Debug.Log("游戲啟動(dòng)");
    }
    private void OnDestroy() {
        //注銷移除監(jiān)聽事件
        MMMessageCenter.Instance.RemoveoListener( MMMessageName.START_UP, StartUp );
    }

image.png
三、委托的簡(jiǎn)化方式
1、Action和Func

委托這種機(jī)制每次使用之前都要先創(chuàng)建一個(gè)新的引用類型,然后再創(chuàng)建實(shí)例,會(huì)顯得比較臃腫、麻煩,所以C#提供了一種簡(jiǎn)化方式,使用Action和Func來(lái)創(chuàng)建委托實(shí)例

using System;
using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    //// 聲明一個(gè)委托類型
    //public delegate void MyHandler(int a);
    //// 聲明了委托類型的實(shí)例
    //public MyHandler myHandler;

    public Action<int> myHandler;

    private void Start()
    {
        // 一對(duì)多依賴
        myHandler += PrintNum;
        myHandler += PrintNumDouble;
        //myHandler(5);
        myHandler.Invoke(5);
    }

    void PrintNum(int a)
    {
        Debug.Log(a);
    }

    void PrintNumDouble(int b)
    {
        Debug.Log(b * 2);
    }

}

可以看到一個(gè)需要兩行代碼,一個(gè)需要一行代碼

那Action和Func有什么區(qū)別呢?

  • Action提供的是無(wú)返回值的委托類型,它提供了從從無(wú)參數(shù)到最多5個(gè)參數(shù)的定義形式
  • 而Func提供的是有返回值的委托類型,在Action的基礎(chǔ)上,每種形式又指定了一個(gè)返回值類型
using System;
using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    //// 聲明一個(gè)委托類型
    //public delegate void MyHandler(int a);
    //// 聲明了委托類型的實(shí)例
    //public MyHandler myHandler;

    public Action<int> myHandler;

    public Func<int, int> myHander3;

    private void Start()
    {
        // 一對(duì)多依賴
        myHandler += PrintNum;
        myHandler += PrintNumDouble;
        //myHandler(5);
        myHandler.Invoke(5);

        myHander3 += PrintNumDoubleFunc;
        myHander3 += PrintNumDoubleFunc3;
        Debug.Log("TestFunc:" + myHander3(10));
    }

    void PrintNum(int a)
    {
        Debug.Log(a);
    }

    void PrintNumDouble(int b)
    {
        Debug.Log(b * 2);
    }
    int PrintNumDoubleFunc(int b)
    {
        Debug.Log(b * 2);
        return b * 2;
    }

    int PrintNumDoubleFunc3(int b)
    {
        Debug.Log(b * 3);
        return b * 3;
    }
}
image.png

這里最后的返回值是30

Action< > :

  • 無(wú)需定義委托類型
  • 不能帶有返回值,必須有參數(shù)

舉兩個(gè)栗子:

Action<int> del = (a) => { }; //int為參數(shù)類型
del ( 1 ); //調(diào)用
Action<int,string> del = (a,str) => { }; //int 和 string 都是參數(shù)類型
del ( 1 , "我是栗子" ); //調(diào)用

Fun< > :

  • 無(wú)需定義委托類型
  • 可以沒(méi)有參數(shù),必須有返回值
  • 最后一個(gè)參數(shù)一定是返回值類型

舉兩個(gè)栗子:

//無(wú)參 int為返回值類型 , 花括號(hào)內(nèi)必須有返回值且必須是int類型
Fun<int> del = () => {return 9;} ; 
del (); //調(diào)用

//帶參 兩個(gè)int鈞為參數(shù)類型,boll為返回值類型,花括號(hào)內(nèi)必須返回bool類型的值
Fun<int,int,bool> del = (a,b) => {return ture;}; 
del (1,2 ); //調(diào)用
2.匿名函數(shù)

首先匿名方法的價(jià)值在于簡(jiǎn)化代碼

之前介紹的Action和Func簡(jiǎn)化了委托的聲明過(guò)程,而匿名方法則簡(jiǎn)化了委托對(duì)應(yīng)的方法聲明,這樣我們?cè)谔幚砗?jiǎn)單邏輯的時(shí)候,可以直接關(guān)注與實(shí)現(xiàn)部分,而不用經(jīng)過(guò)一些繁瑣的步驟

    private void Start()
    {
        
        // 將匿名方法用于Action委托類型
        Action<int> printNumAdd = delegate(int a)
        {
            int b = 3;
            Debug.Log(a + b);
        };

        printNumAdd(2);
    }
3.lambda表達(dá)式

lambda表達(dá)式是匿名方法的進(jìn)一步演化和簡(jiǎn)化,但是本身并非委托類型,不過(guò)它可以通過(guò)多種方式隱式或顯式轉(zhuǎn)換成一個(gè)委托實(shí)例。

               // 將lambda表達(dá)式用于Action委托類型
        Action<int> printNumDouble = (int a) =>
        {
            Debug.Log(a * a);
        };

        printNumDouble(3);

C# Lambda表達(dá)式
C# Lambda表達(dá)式詳解,及Lambda表達(dá)式樹的創(chuàng)建

在 2.0 之前的 C# 版本中,聲明委托的唯一方法是使用命名方法。 C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表達(dá)式取代了匿名方法,作為編寫內(nèi)聯(lián)代碼的首選方式。

四、event關(guān)鍵字
1.參考知乎 unity的委托是什么? event 關(guān)鍵字有什么用?

委托是一個(gè)容器,可以放函數(shù)對(duì)象,并且可以觸發(fā)委托面的每個(gè)函數(shù)調(diào)用。委托主要用戶回調(diào)函數(shù)。

// 定義一個(gè)委托類型 
public delegate void GreetingDelegate(int lhs, int rhs) ; 
// 定義一個(gè)委托變量。
public GreetingDelegate MakeGreet; 
// 觸發(fā)容器里面所有函數(shù)調(diào)用
MakeGreet(3, 4); 

我們?nèi)绻谕獠拷o委托變量加函數(shù)進(jìn)來(lái),那么委托要定義成public, 這樣做又有一個(gè)問(wèn)題,public外部的人也可以觸發(fā)這個(gè)委托,如果我希望設(shè)計(jì)成外部可以加回調(diào),但是只能是模塊內(nèi)部觸發(fā)委托,那么我可以加一個(gè)event來(lái)修飾,這樣雖然是public,但是外部無(wú)法觸發(fā)委托,只能類的內(nèi)部觸發(fā)。

public event GreetingDelegate MakeGreet;
2.C# event關(guān)鍵字
using System;

namespace ConsoleAppTest
{
    class Program
    {
        class Test
        {
            static void Main(string[] args)
            {
                FileUploader f1 = new FileUploader();
                //委托設(shè)置為空
                f1.FileUploaded = null;
                f1.FileUploaded = Progress;
                //重置委托
                f1.FileUploaded = ProgressAnother;
                f1.Upload();
                //外部直接調(diào)用
                f1.FileUploaded(6);
                Console.Read();
            }
        }

        class FileUploader
        {
            public delegate void FileUploadedHandler(int progress);
            public FileUploadedHandler FileUploaded;

            public void Upload()
            {
                int fileProgress = 5;
                while (fileProgress > 0)
                {
                    //傳輸代碼,省略
                    fileProgress--;
                    if (FileUploaded != null)
                    {
                        FileUploaded(fileProgress);
                    }
                }
            }
        }

        static void Progress(int progress)
        {
            Console.WriteLine(progress);
        }

        static void ProgressAnother(int progress)
        {
            Console.WriteLine("另一個(gè)方法:{0}", progress);
        }
    }

}

以上調(diào)用者代碼本身是和FileUploader類一起的,這起碼存在兩個(gè)問(wèn)題:

1)如果在Main中另起一個(gè)線程,該工作線程則可以將FileProgress委托鏈置為空:

f1.FileUploaded = null;

2)可以在外部調(diào)用FileUploaded,如:

f1.FileUploaded(6) ;

這應(yīng)該是不允許的,因?yàn)槭裁磿r(shí)候通知調(diào)用者,應(yīng)該是FileUploader類自己的職責(zé),而不是調(diào)用者本身來(lái)決定的。event關(guān)鍵字正是在這種情況下被提出來(lái)的,它為委托加了保護(hù)。

使用event的寫法,如下:


image.png

添加event關(guān)鍵字后,上面提到的幾種情況會(huì)被阻止。

    static void Main(string[] args)
    {
        FileUploader f1 = new FileUploader();
        f1.FileUploaded += Progress;
        f1.Upload();
        Console.Read();
    }
五、知乎 Ivony C#的Delegate 為什么沒(méi)在其他主流語(yǔ)言中普及?

首先是delegate的設(shè)計(jì)其實(shí)是有一些歷史問(wèn)題的,并不能說(shuō)是最好的一種設(shè)計(jì)。

一個(gè)典型的問(wèn)題就在于所有的delegate實(shí)例都是MulticastDelegate類型的,但事實(shí)上多播委托的使用范圍并沒(méi)有那么大。更有意思的是多播委托本質(zhì)上是個(gè)串行委托,委托方法是一個(gè)接一個(gè)的執(zhí)行的。而實(shí)際應(yīng)用場(chǎng)景中我們會(huì)遇到并發(fā)多播,異步多播,當(dāng)某個(gè)出現(xiàn)錯(cuò)誤時(shí)繼續(xù)執(zhí)行其他方法的多播委托,所有這些都是MulticastDelegate搞不定的。所以變得意義不大。

到今天為止MulticatsDelegate和+=的運(yùn)算符重載還是多用于事件處理,而事件用默認(rèn)的多播委托實(shí)現(xiàn)還會(huì)有可能導(dǎo)致對(duì)象不被釋放的坑。

其次就是delegate這個(gè)概念意義并不大,盡管在強(qiáng)類型語(yǔ)言里面我們的確需要發(fā)明一種東西來(lái)描述函數(shù)簽名,并將單個(gè)函數(shù)簽名固化成一種強(qiáng)類型。但絕大多數(shù)時(shí)候?qū)iT去強(qiáng)調(diào)這個(gè)概念的意義并不大。很多語(yǔ)言都支持這個(gè)特性,但是一般可以直接用函數(shù)來(lái)描述就可以了,不必另外發(fā)明一個(gè)委托的概念。

另外就是傳統(tǒng)的delegate強(qiáng)類型還有一個(gè)缺陷,即使兩個(gè)delegate類型所代表的函數(shù)簽名是一模一樣的,那他們倆也是兩個(gè)類型。這在實(shí)際運(yùn)用中是個(gè)麻煩。如果你需要用到兩個(gè)函數(shù)庫(kù),而這兩個(gè)庫(kù)分別將某個(gè)類型的函數(shù)簽名定義了一個(gè)委托,即使你只需要寫一個(gè)函數(shù)就能滿足兩個(gè)函數(shù)庫(kù)的要求,但你仍然不得不莫名其妙的創(chuàng)建兩個(gè)委托實(shí)例分別給到兩個(gè)不同類型的委托對(duì)象。

這個(gè)缺陷直接催生了Func和Action系列的委托。當(dāng)Func和Action系列委托出現(xiàn)以及泛型委托類型參數(shù)的協(xié)變和逆變出現(xiàn)后,我們發(fā)現(xiàn)委托這個(gè)概念大部分時(shí)候變得很多余。也就是說(shuō)我們可以輕松地寫出很多代碼根本用不著了解委托這個(gè)概念,我們最終的著眼點(diǎn)還是函數(shù)簽名。

但是別忘了泛型和匿名方法是C# 2.0才出現(xiàn)的(省略委托實(shí)例創(chuàng)建表達(dá)式直接用方法名稱代替委托實(shí)例也是2.0才引入的),而泛型委托類型參數(shù)的協(xié)變和逆變是C#3.0才出現(xiàn)的,C#一直在發(fā)展的過(guò)程中。還有大家所說(shuō)的lambda表達(dá)式也是3.0才引入的。

無(wú)論怎樣,現(xiàn)在設(shè)計(jì)一個(gè)語(yǔ)言在語(yǔ)言內(nèi)部保留委托的概念是很正常的,但是再花時(shí)間去把這個(gè)概念作為亮點(diǎn)來(lái)介紹,以及專門去學(xué)習(xí)和闡述是沒(méi)有什么意義的。即使是Java,其實(shí)那個(gè)SAM Type就是委托的別名,或者換句話說(shuō)delegate就是SAM Type的別名和語(yǔ)法糖。

當(dāng)然不管怎么樣,C#的delegate語(yǔ)法相較于C/C++的函數(shù)指針的語(yǔ)法是一個(gè)巨大的飛越,而委托這種語(yǔ)法糖也遠(yuǎn)比所謂的SAM Type直觀和簡(jiǎn)便。

當(dāng)然我也看到很多人說(shuō)委托的學(xué)習(xí)成本太高,我想說(shuō)其實(shí)OOP和強(qiáng)類型編程語(yǔ)言的學(xué)習(xí)成本本來(lái)就略高于平均智商水平,早日發(fā)現(xiàn)并作出正確的選擇是非常對(duì)的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容