C#中的事件和委托那些事

概要

我们在做C#的WinForm,WPF或Web Form开发时候,总是要与事件和委托打交道。可能大家会有这些疑问,到底什么是事件,什么是委托,事件和委托是什么关系,事件模型又是什么,为什么C#的事件语法糖会引起歧义,委托是不是可以替代事件。本文希望能通过一个收短信拿快递的例子,帮助大家理清事件和委托的这些事。

基本定义

事件与委托的定义

按照MS的定义如下:

事件:Events enable a class or object to notify other classes or objects when something of interest occurs.

基于MS的定义我的理解如下:

  • 事件是可以发生的如我的手机铃声响了,这个事件。
  • 事件可以通知其他对象去做事,例如火警响了,通知大家疏散。
  • 事件可以传递数据给其他对象,例如在开车时候,当你看到信号灯转为绿色,就可以走了。绿色就是信号灯切换这个事件给所有等待的车主发送的消息。
  • 事件的发生是基于订阅关系的。例如我在办公楼A工作,就建立了我和办公楼火警的订阅关系,办公楼A的火警响了,就会疏散。如果其他办公楼的火警响了,和我们没有订阅关系,就不需要疏散。

基于我的理解,事件应该是有四要素,发生,响应,传递信息和订阅关系。其中传递信息不是必须的。例如火警响了,不用传递任何消息,所有人都知道要疏散。

委托:A delegate is a type that represents references to methods with a particular parameter list and return type.

基于MS的定义我的理解如下:

  • C#中的委托类似C++中的指向函数的指针,可以理解成指向方法的引用
  • 定义了所指向方法的参数列表
  • 定义了所指向方法的返回值。
  • 可以同时指向多个符合的的方法。

事件与委托的关系

C#中事件的最后落地,是要体现在响应方法执行上的。现实事件和委托是什么关系,其实也就很简单,就是一个各取所需,相互利用的关系。请见下图:
在这里插入图片描述

  • 事件发生需要有响应的方法,但是不是所有方法都可以作响应方法。委托帮助事件定义了响应方法的参数列表和返回值。
  • 只有符合委托对响应方法定义的方法在上图才能被委托订阅到事件的响应方法里面。
  • 如上图,一个委托可以为事件建立多个符合条件的方法的订阅关系。
  • 事件的触发,响应方法才能执行。也就是说,事件可以决定响应方法的执行。

事件与委托的实例

基于上面的理论,我们现在来看一个例子。我的手机响了,来了一条取快递的消息。通知我取楼下的快递柜取快递。然后过了一分钟,手机又响了。来了一条垃圾短信。过了一分钟,又来了一条家人的信息,让我保重身体。

我们按照事件模型,逐一拆解上面的例子:

事件主体事件事件接受者事件响应事件参数订阅关系
手机响铃我自己取快递快递柜号和取件码我和我的手机
手机响铃我自己忽略N/A我和我的手机
手机响铃我自己认真阅读N/A我和我的手机

代码实现

代码模拟

基于事件模型,我们需要定义手机类,手机机主类,事件参数类。在手机类中定义响铃事件,在机主类中定义事件响应方法。关键代码如下,可以下载完整代码

事件参数类:

using System;
namespace EventDelegate
{
    public class DeliveryMessageEventArgs : EventArgs
    {
        public string From { get; set; }
        public string Content { get; set; }
        public MessageType Type  { get; set; } = MessageType.Other;
    }
    public enum MessageType{
        Parcel = 1,
        Trash,
        Family,
        Other
    }
}
  • 事件参数包括信息发送方手机号,信息正文和信息类型,默认为其他类型。
  • 按照MS要求,该类名以EventArgs结尾,并继承自EventArgs类,表示该类作为事件的参数定义。

手机类代码:

using System;
using System.Threading;
namespace EventDelegate
{
    public delegate void RingEventHandler(object sender, DeliveryMessageEventArgs args);
    public class Phone
    {

        public event RingEventHandler Ring{
            add{
                this.ringEventHandler += value;
            }
            remove{
                this.ringEventHandler -= value;
            }
        } 
        public MessageType CheckMessage(DeliveryMessageEventArgs args){
            if (args.From.StartsWith("123-")){
                args.Type = MessageType.Parcel;
            }else if (args.From.StartsWith("456-")){
                args.Type = MessageType.Family;
            }else if (args.From.StartsWith("789-")){
                 args.Type = MessageType.Trash;
            }
            return args.Type;
        }
        public void MessageComing(DeliveryMessageEventArgs args){
            System.Console.WriteLine("Message is coming ...");
            Thread.Sleep(1000);
            System.Console.WriteLine("Checking the message ...");
            Thread.Sleep(1000);
            MessageType typeMessage = CheckMessage(args);
            System.Console.WriteLine($"It is a {typeMessage.ToString()} message");
             if (this.ringEventHandler != null && typeMessage != MessageType.Trash{
                this.ringEventHandler.Invoke(this,args);
            } else{
                System.Console.WriteLine($"Ingore trash message from {args.From}");
            }
        }
    }
}
  • 定义一个响铃事件的委托,该委托规定响铃事件的响应函数的参数列表有两个参数,
    • 按照MS的定义习惯,第一个参数是事件主体类的实例,为了可扩展性,定义为object类型;
    • 第二个参数为DeliveryMessageEventArgs 类型;
    • 返回值为空。
  • 在响铃事件字段Ring的定义中,响铃事件字段被RingEventHandler委托修饰。RingEventHandler委托决定了响铃事件的响应方法的参数和返回值。
  • Ring响铃事件通过委托ringEventHandler实例添加具体的方法的订阅。
  • Phone类应为响铃事件字段Ring,具体有了通知功能。
  • 收到短信后,手机会根据短信发送方的手机号来确认短信类型。
  • 垃圾短信会被拦截,不会触发订阅的方法。
  • 其他类型短信会被处理。

手机机主类:

using System.Text.RegularExpressions;
using System.Threading;
namespace EventDelegate
{
    public class PhoneOwner
    {
        public void Action(object sender, DeliveryMessageEventArgs args){
            Thread.Sleep(1000);
            if (args.Type == MessageType.Parcel){
                GetParcel(args);
            }else{
                ReadingMessage(args);
            }
        }

        private void GetParcel(DeliveryMessageEventArgs args){
            var content = args.Content;
            var regContent = @"Address:\s*(?<Address>.*?);\s*Code:\s*(?<Code>.*)";
            Regex patternContent = new Regex(regContent, RegexOptions.None);
            Match ms = patternContent.Match(content);
            var address = ms.Groups["Address"].Value;
            var code = ms.Groups["Code"].Value;
            System.Console.WriteLine($"Customer go to {address} and enter {code} to get package.");
        }

        private void ReadingMessage(DeliveryMessageEventArgs args){
            System.Console.WriteLine($"Reading the message {args.Content} from {args.From}.");
        }
    }
}

机主类主要定义响应方法:

  • 如果是快递短信,调用GetParcel方法过滤出取件地址和快递柜的开锁码。让后取回快递包裹。
  • 如果是家人短信,调用ReadingMessage方法。

代码入口类:

using System;

namespace EventDelegate
{
    class Program
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            PhoneOwner owner = new PhoneOwner();
            phone.Ring += owner.Action;
            DeliveryMessageEventArgs deliveryMsg = new DeliveryMessageEventArgs(){
                From = "123-34534534534",
                Content = "Address: Hive Box 02; Code: 324324324"
            };
            DeliveryMessageEventArgs trashMsg = new DeliveryMessageEventArgs(){
                From = "789-43534534",
                Content = "Please choose our product"
            };
            DeliveryMessageEventArgs familyMsg = new DeliveryMessageEventArgs(){
                From = "456-34534534534",
                Content = "Take care of yourself"
            };
            phone.MessageComing(deliveryMsg);
            phone.MessageComing(trashMsg);
            phone.MessageComing(familyMsg);
        }
    }
}
  • 定义手机和机主类,即事件主体和事件接受者。
  • 建立手机和机主的订阅关系。
  • 模拟快递短信,垃圾短信和家人短信数据。
  • 调用手机实例的MessageComing方法触发响铃事件的响应方法。

执行结果如下:
在这里插入图片描述
整个过程的模拟成功。响铃事件发生,消息按照规定格式发送个机主,机主收到消息,进行响应,取回快递。其他消息同理。

一块语法糖引发的问题

本文Phone类中响铃事件的定义是按照MS文档,最完整的或者说是最繁琐的一种定义方式。MS后来优化了事件的定义方式,不再需要私有的委托字段去管理事件订阅和响应函数触发。具体如下:

  • 事件定义阶段不再需要私有的委托字段取添加和删除响应函数。
  • 事件发生时候,不再通过私有委托字段取触发响应函数。
using System;
using System.Threading;
namespace EventDelegate
{
    public delegate void RingEventHandler(object sender, DeliveryMessageEventArgs args);
    public class Phone
    {
        public event RingEventHandler Ring;
        /*private RingEventHandler ringEventHandler;
          public event RingEventHandler Ring {
            add{
                this.ringEventHandler += value;
            }
            remove{
                this.ringEventHandler -= value;
            }
        } */
        public MessageType CheckMessage(DeliveryMessageEventArgs args){
            if (args.From.StartsWith("123-")){
                args.Type = MessageType.Parcel;
            }else if (args.From.StartsWith("456-")){
                args.Type = MessageType.Family;
            }else if (args.From.StartsWith("789-")){
                 args.Type = MessageType.Trash;
            }
            return args.Type;
        }
        public void MessageComing(DeliveryMessageEventArgs args){
            System.Console.WriteLine("Message is coming ...");
            Thread.Sleep(1000);
            System.Console.WriteLine("Checking the message ...");
            Thread.Sleep(1000);
            MessageType typeMessage = CheckMessage(args);
            System.Console.WriteLine($"It is a {typeMessage.ToString()} message");
            /* if (this.ringEventHandler != null && typeMessage != MessageType.Trash){
                this.ringEventHandler.Invoke(this,args);
            } */
            if (this.Ring != null && typeMessage != MessageType.Trash){
                this.Ring.Invoke(this,args);
            }else{
                System.Console.WriteLine($"Ingore trash message from {args.From}");
            }
        }
    }
}

上述代码执行可以得到和之前代码执行同样的结果。

现在看起来事件似乎不再需要委托这个管家去管理响应方法,事件可以直接管理响应方法。真的是这样吗,我们打开反编译工具,看看Phone类的实例代码编译后时什么样的。

在这里插入图片描述
通过反编译器,我们不难看出,编译器自己生成了一个私有的委托字段Ring,该字段的功能和我们自己定义的委托字段一样。

所以MS增加这个语法糖的本意并不是将委托从事件里面剔除,仅仅是简化代码。

C++函数指针滥用的问题不再重现

到现在为止,我们发现似乎委托帮助事件做了大多数的工作,除了触发响应函数,似乎事件这个类型变得一无是处。

我们看下面的代码,修改Phone类事件的定义,去掉event关键字,将Ring字段彻底变成一个委托字段。上述代码执行可以得到和之前代码执行同样的结果。

现在我们看看这么做到底为什么不行,现在添加一个广告类。

using System.Threading;
namespace EventDelegate
{
    public class Advertisement
    {
        public void Action(object sender, DeliveryMessageEventArgs args){
            Thread.Sleep(1000);
            System.Console.WriteLine($"Reading the message {args.Content} from {args.From}.");
        }

    }
}
using System.Threading;
namespace EventDelegate
{
    public delegate void RingEventHandler(object sender, DeliveryMessageEventArgs args);
    public class Phone
    {
        //public event RingEventHandler Ring;
        public RingEventHandler Ring;
         
        public MessageType CheckMessage(DeliveryMessageEventArgs args){
            if (args.From.StartsWith("123-")){
                args.Type = MessageType.Parcel;
            }else if (args.From.StartsWith("456-")){
                args.Type = MessageType.Family;
            }else if (args.From.StartsWith("789-")){
                 args.Type = MessageType.Trash;
            }
            return args.Type;
        }
        public void MessageComing(DeliveryMessageEventArgs args){
            System.Console.WriteLine("Message is coming ...");
            Thread.Sleep(1000);
            System.Console.WriteLine("Checking the message ...");
            Thread.Sleep(1000);
            MessageType typeMessage = CheckMessage(args);
            System.Console.WriteLine($"It is a {typeMessage.ToString()} message");
            if (this.Ring != null && typeMessage != MessageType.Trash){
                this.Ring.Invoke(this,args);
            }else{
                System.Console.WriteLine($"Ingore trash message from {args.From}");
            }
        }
    }
}
using System;

namespace EventDelegate
{
    class Program
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            PhoneOwner owner = new PhoneOwner();
            phone.Ring += owner.Action;
            DeliveryMessageEventArgs deliveryMsg = new DeliveryMessageEventArgs(){
                From = "123-34534534534",
                Content = "Address: Hive Box 02; Code: 324324324"
            };
            DeliveryMessageEventArgs trashMsg = new DeliveryMessageEventArgs(){
                From = "789-43534534",
                Content = "Please choose our product"
            };
            DeliveryMessageEventArgs familyMsg = new DeliveryMessageEventArgs(){
                From = "456-34534534534",
                Content = "Take care of yourself"
            };
            phone.MessageComing(deliveryMsg);
            phone.MessageComing(trashMsg);
            phone.MessageComing(familyMsg);


            DeliveryMessageEventArgs adMsg = new DeliveryMessageEventArgs(){
                From = "789-43534534",
                Content = "Please choose our product"
            };

            Advertisement ad = new Advertisement();
            phone.Ring += ad.Action;
            phone.Ring.Invoke(ad,adMsg); 

        }
    }
}
  • 在程序入口类新加广告短信信息。
  • 既然现在响铃事件字段只是一个委托类型,我们可以在任何地方触发其对应的方法。

上述代码执行后,我们看到结果如下:

在这里插入图片描述
从执行结果来看,由于Phone类的响铃委托在入口函数中被触发,跳过了手机检查短信的过程,所以广告短信并没有被过滤掉,也提示用户去读了。

也就是说现在出现了类似C++函数指针滥用的情况。

下面我们在上文Phone类的基础上,给Ring字段加上回event关键字 =>

public event RingEventHandler Ring;

再执行上述代码,结果如下:

在这里插入图片描述
我们看到报错了,我们无法在入口函数中去触发事件的响应函数。这也就对应的MS官网上的这句话:

在这里插入图片描述
当一个委托前面加上event关键字后,变为了一个事件,该事件的响应方法只能在声明该事件的类或结构体中被调用。

也就是说,对于event和委托的关系,event除了可以在发生后触发内部委托订阅的响应方法外,还有一个重要的作用就是约束方法执行的位置,以避免任何人都可以给事件添加新的订阅并触发执行。避免像C++那样,滥用指向函数的指针。

总结

委托和事件的关系就是各取所需,相互利用。委托处于下游,它规范事件响应方法的参数列表和返回值,并为事件提供订阅服务。事件处于上游,它负责根据用户的需要,触发下游的响应方法,并限制响应方法的执行位置。二者各司其职,无法相互替代。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值