C# delegate 委托 event关键字

参考
Unity游戏开发——对委托的理解

一、委托简介

委托也就是delegate是一个引用类型,他相当于一个装着方法的容器,他可以把方法作为对象进行传递,但前提是委托和对应传递方法的签名得是相同的,签名指的是他们的参数类型和返回值类型

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    // 声明一个委托类型
    public delegate void MyHandler(int a);
    
        // 声明了委托类型的实例
    public MyHandler myHandler;

    private void Start()
    {
        // 一对一依赖
        myHandler = PrintNum;
        myHandler(10);
        myHandler = PrintNumDouble;
        myHandler(4);       
    }

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

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

声明了一个委托类型,可以粗暴的理解为我们创建了一个新的引用类型,我们可以使用这个新创建的引用类型来声明实例变量。

        // 声明了一个委托类型的实例变量
    public MyHandler myHandler;
    
    // 声明一个类的实例变量
    public TestClass myTestClass;

接着我们又声明了两个跟委托类型具有相同签名的方法(返回值类型和参数类型相同),最后我们在start方法里把具有相同签名的方法赋值给了委托实例,然后直接进行了方法回调

也可以一对多依赖

    private void Start()
    {       
        // 一对多依赖
        myHandler += PrintNum;
        myHandler += PrintNumDouble;
        myHandler(5); 
    }

我们还可以在别的脚本上也添加对委托实例的监听

using UnityEngine;

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

    private void PrintReceive(int a)
    {
        Debug.Log("reveice : " + a);
    }
}
二、消息机制
1.Unity中的消息系统

既然提到了委托与观察者模式,那么Unity中是否已经存在了消息机制呢?答案是肯定的,这套内置的消息机制主要围绕着SendMessage和BroadcastMessage而构建。但是这套机制是存在一些缺陷的

  • 发送和接收消息都过于依赖反射来查找消息对应的被调用函数,频繁使用反射自然会影响性能。
  • 使用字符串来标识一个方法会带来很高的维护成本,比如方法名字重构甚至删除了,编辑器是不会报错的。
  • 由于使用了反射机制,是可以调用私有方法的,很多人可能会因为看到了私有方法没有被调用过而删除了这段废弃代码,同样编辑器并不会报错,甚至程序也能正常运行,但是如果触发了这个消息,隐患就会爆发。
2.使用C#的委托来实现一个自己的消息机制

参考unity-针对于消息机制的学习 一

消 息 类MMMessage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 消息类
/// </summary>
public class MMMessage  {
    //成员变量   发送消息的名字
    public string Name{
        get;
        private set;
    }
    //成员变量   发送消息的消息主体
    public object Boby {
        get;
        private set;
    }
    //构造函数   传值赋值
    public MMMessage(string name,object boby){
        Name = name;
        Boby = boby;
    }
}

消息名称列表MMMessageName

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 发送的消息名
/// </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 {
    //定义一个消息类委托 类型 
    public delegate void messageDelHandle(MMMessage message);

    //定义消息控制类的单例
    private static MMMessageCenter instance;
    public static MMMessageCenter Instance {
        get {
            //若是instance为空,初始实例化一个消息控制中心
            if (instance == null) {
                instance = new MMMessageCenter();
            }
            return instance;

        }
    }
    //**定义一个字典  <消息名称, 委托消息>    消息列表**
    private Dictionary<string, messageDelHandle> messageMap = new Dictionary<string, messageDelHandle>();

    /// <summary>
    /// **注册监听**
    /// </summary>
    /// <param name="messageName">消息名称</param>
    /// <param name="handle">消息内容</param>
    public void RigisterListener(string messageName, messageDelHandle handle) {
        if (handle == null) return;  //若是消息为空  退出方法
        if (!messageMap.ContainsKey( messageName )) {
            messageMap.Add( messageName, handle );
        }//若是消息名称不存在,添加到消息列表
    }

    /// <summary>
    /// **移除/注销监听**
    /// </summary>
    /// <param name="messageName">消息名称</param>
    /// <param name="handle">消息内容</param>
    public void RemoveoListener(string messageName, messageDelHandle handle) {
        if (!messageMap.ContainsKey( messageName ))
            return; //若是消息列表里没有这个消息名称键值,已经注销直接退出
        messageMap[ messageName ] -= handle;  //若是存在,去除当前handle的消息内容
        if (messageMap[ messageName ] == null) {
            messageMap.Remove( messageName );
        }   //若是消息列表里当前消息数量为空,则清除该消息名称

    }
    /// <summary>
    /// **发送消息**
    /// </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 () {
        //发送消息
        MMMessageCenter.Instance.sendMessage(MMMessageName.START_UP);
}
    void Awake () {
        //注册监听事件
        MMMessageCenter.Instance.RigisterListener( MMMessageName.START_UP, StartUp );
    }

    private void StartUp(MMMessage message) {
        Debug.Log("游戏启动");
    }
    private void OnDestroy() {
        //注销移除监听事件
        MMMessageCenter.Instance.RemoveoListener( MMMessageName.START_UP, StartUp );
    }

image.png
三、委托的简化方式
1、Action和Func

委托这种机制每次使用之前都要先创建一个新的引用类型,然后再创建实例,会显得比较臃肿、麻烦,所以C#提供了一种简化方式,使用Action和Func来创建委托实例

using System;
using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    //// 声明一个委托类型
    //public delegate void MyHandler(int a);
    //// 声明了委托类型的实例
    //public MyHandler myHandler;

    public Action<int> myHandler;

    private void Start()
    {
        // 一对多依赖
        myHandler += PrintNum;
        myHandler += PrintNumDouble;
        //myHandler(5);
        myHandler.Invoke(5);
    }

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

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

}

可以看到一个需要两行代码,一个需要一行代码

那Action和Func有什么区别呢?

  • Action提供的是无返回值的委托类型,它提供了从从无参数到最多5个参数的定义形式
  • 而Func提供的是有返回值的委托类型,在Action的基础上,每种形式又指定了一个返回值类型
using System;
using UnityEngine;

public class DelegateTest : MonoBehaviour
{
    //// 声明一个委托类型
    //public delegate void MyHandler(int a);
    //// 声明了委托类型的实例
    //public MyHandler myHandler;

    public Action<int> myHandler;

    public Func<int, int> myHander3;

    private void Start()
    {
        // 一对多依赖
        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< > :

  • 无需定义委托类型
  • 不能带有返回值,必须有参数

举两个栗子:

Action<int> del = (a) => { }; //int为参数类型
del ( 1 ); //调用
Action<int,string> del = (a,str) => { }; //int 和 string 都是参数类型
del ( 1 , "我是栗子" ); //调用

Fun< > :

  • 无需定义委托类型
  • 可以没有参数,必须有返回值
  • 最后一个参数一定是返回值类型

举两个栗子:

//无参 int为返回值类型 , 花括号内必须有返回值且必须是int类型
Fun<int> del = () => {return 9;} ; 
del (); //调用

//带参 两个int钧为参数类型,boll为返回值类型,花括号内必须返回bool类型的值
Fun<int,int,bool> del = (a,b) => {return ture;}; 
del (1,2 ); //调用
2.匿名函数

首先匿名方法的价值在于简化代码

之前介绍的Action和Func简化了委托的声明过程,而匿名方法则简化了委托对应的方法声明,这样我们在处理简单逻辑的时候,可以直接关注与实现部分,而不用经过一些繁琐的步骤

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

        printNumAdd(2);
    }
3.lambda表达式

lambda表达式是匿名方法的进一步演化和简化,但是本身并非委托类型,不过它可以通过多种方式隐式或显式转换成一个委托实例。

               // 将lambda表达式用于Action委托类型
        Action<int> printNumDouble = (int a) =>
        {
            Debug.Log(a * a);
        };

        printNumDouble(3);

C# Lambda表达式
C# Lambda表达式详解,及Lambda表达式树的创建

在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法。 C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表达式取代了匿名方法,作为编写内联代码的首选方式。

四、event关键字
1.参考知乎 unity的委托是什么? event 关键字有什么用?

委托是一个容器,可以放函数对象,并且可以触发委托面的每个函数调用。委托主要用户回调函数。

// 定义一个委托类型 
public delegate void GreetingDelegate(int lhs, int rhs) ; 
// 定义一个委托变量。
public GreetingDelegate MakeGreet; 
// 触发容器里面所有函数调用
MakeGreet(3, 4); 

我们如果在外部给委托变量加函数进来,那么委托要定义成public, 这样做又有一个问题,public外部的人也可以触发这个委托,如果我希望设计成外部可以加回调,但是只能是模块内部触发委托,那么我可以加一个event来修饰,这样虽然是public,但是外部无法触发委托,只能类的内部触发。

public event GreetingDelegate MakeGreet;
2.C# event关键字
using System;

namespace ConsoleAppTest
{
    class Program
    {
        class Test
        {
            static void Main(string[] args)
            {
                FileUploader f1 = new FileUploader();
                //委托设置为空
                f1.FileUploaded = null;
                f1.FileUploaded = Progress;
                //重置委托
                f1.FileUploaded = ProgressAnother;
                f1.Upload();
                //外部直接调用
                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("另一个方法:{0}", progress);
        }
    }

}

以上调用者代码本身是和FileUploader类一起的,这起码存在两个问题:

1)如果在Main中另起一个线程,该工作线程则可以将FileProgress委托链置为空:

f1.FileUploaded = null;

2)可以在外部调用FileUploaded,如:

f1.FileUploaded(6) ;

这应该是不允许的,因为什么时候通知调用者,应该是FileUploader类自己的职责,而不是调用者本身来决定的。event关键字正是在这种情况下被提出来的,它为委托加了保护。

使用event的写法,如下:


image.png

添加event关键字后,上面提到的几种情况会被阻止。

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

首先是delegate的设计其实是有一些历史问题的,并不能说是最好的一种设计。

一个典型的问题就在于所有的delegate实例都是MulticastDelegate类型的,但事实上多播委托的使用范围并没有那么大。更有意思的是多播委托本质上是个串行委托,委托方法是一个接一个的执行的。而实际应用场景中我们会遇到并发多播,异步多播,当某个出现错误时继续执行其他方法的多播委托,所有这些都是MulticastDelegate搞不定的。所以变得意义不大。

到今天为止MulticatsDelegate和+=的运算符重载还是多用于事件处理,而事件用默认的多播委托实现还会有可能导致对象不被释放的坑。

其次就是delegate这个概念意义并不大,尽管在强类型语言里面我们的确需要发明一种东西来描述函数签名,并将单个函数签名固化成一种强类型。但绝大多数时候专门去强调这个概念的意义并不大。很多语言都支持这个特性,但是一般可以直接用函数来描述就可以了,不必另外发明一个委托的概念。

另外就是传统的delegate强类型还有一个缺陷,即使两个delegate类型所代表的函数签名是一模一样的,那他们俩也是两个类型。这在实际运用中是个麻烦。如果你需要用到两个函数库,而这两个库分别将某个类型的函数签名定义了一个委托,即使你只需要写一个函数就能满足两个函数库的要求,但你仍然不得不莫名其妙的创建两个委托实例分别给到两个不同类型的委托对象。

这个缺陷直接催生了Func和Action系列的委托。当Func和Action系列委托出现以及泛型委托类型参数的协变和逆变出现后,我们发现委托这个概念大部分时候变得很多余。也就是说我们可以轻松地写出很多代码根本用不着了解委托这个概念,我们最终的着眼点还是函数签名。

但是别忘了泛型和匿名方法是C# 2.0才出现的(省略委托实例创建表达式直接用方法名称代替委托实例也是2.0才引入的),而泛型委托类型参数的协变和逆变是C#3.0才出现的,C#一直在发展的过程中。还有大家所说的lambda表达式也是3.0才引入的。

无论怎样,现在设计一个语言在语言内部保留委托的概念是很正常的,但是再花时间去把这个概念作为亮点来介绍,以及专门去学习和阐述是没有什么意义的。即使是Java,其实那个SAM Type就是委托的别名,或者换句话说delegate就是SAM Type的别名和语法糖。

当然不管怎么样,C#的delegate语法相较于C/C++的函数指针的语法是一个巨大的飞越,而委托这种语法糖也远比所谓的SAM Type直观和简便。

当然我也看到很多人说委托的学习成本太高,我想说其实OOP和强类型编程语言的学习成本本来就略高于平均智商水平,早日发现并作出正确的选择是非常对的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容