随笔 - 148  文章 - 1  评论 - 15  阅读 - 30万

C# 从1到Core--委托与事件

委托与事件在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);
            }
        }

     

     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的代码如下:

    实际上定义了最多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

     这在以前的版本中是不允许的。

     

posted on   冰魂雪魄  阅读(360)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

WPF框架交流群:C#.net. WPF.core 技术交流�      C#WPF技术交流群:C#.net. WPF.core 技术交流�     WPF技术大牛交流群:C#.net. WPF.core 技术交流�
点击右上角即可分享
微信分享提示