事件学习——2. 事件的声明(自定义事件)

本文深入探讨事件的本质,将其比喻为委托的“蒙版”包装器,强调事件对外隐藏委托功能并仅暴露添加/移除处理器的能力。通过示例代码详细阐述事件的声明、命名约定及其与委托的关联。同时,讨论了为何使用事件而非直接的委托字段,以确保程序逻辑的安全性。最后,分析了事件与委托的关系,澄清了事件并非委托的误解,并指出事件是委托的封装,增加了额外的安全层。
摘要由CSDN通过智能技术生成

 首先回顾下事件的本质,正式一点的说法是这样的:
       事件本质:委托字段的一个包装器
               这个包装器对委托字段的访问起限制作用,相当于一个“蒙版”
               封装的一个重要功能就是隐藏
               事件对外界隐藏了委托实例的大部分功能,仅暴露添加/移除事件处理器的功能
       简单来说,委托是一头猛兽,用的好了帮你咬人,用不好咬你,事件相当于一个笼子,平时把他关进去,用的时候给你个开关可以开笼子和关笼子,开笼子它出去干活,关笼子他就回来,为了保护自己,你只能控制笼子的开关,不能控制它用什么方式咬人

用于声明事件的委托类型的命名约定:
        1. 用于声明Foo事件的委托,一般命名为FooEventHandler(除非是一个非常通用的事件约束)
        2. FooEventHandler委托类型的参数一般有两个(由WIN32 API演化而来,历史悠久)
            第一个是object类型,名字为sender,实际上就是事件的拥有者,事件的Source
            第二个是EventArgs类的派生类,类名一般为FooEventArgs,参数名为e,也就是前面讲过的事件参数
            虽然没有官方的说法,但是我们可以把委托的参数列表看作是事件发生后发送给事件响应者的“事件消息”
        3. 触发Foo事件的方法一般命名为OnFoo,即“因何引发”、“事出有因”
            访问级别为Protected,不能为public,不然又成了可以“借刀杀人”了
 事件的命名约定:
        1. 带有时态的动词或者动词短语
        2. 事件拥有者“正在做”什么事情,用进行时;事件拥有者“做完了”什么事情,用完成时;

介绍事件声名之前我们先来了解下事件完整的声明是什么样的

按照注释顺序查看代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace _9.事件的声明
{


class Program
    {
        #region 先来看完整的事件声明方式
        static void Main(string[] args)
        {
            //例子说明:当你去饭店的时候,服务员过来订阅你点菜这个事件,这个事件发生服务员就给你上菜并在你账单上记一笔,等你走的时候付费

            //7. 声明一个Customer和Waiter成员,并添加事件处理器
            Customer customer = new Customer();
            Waiter waiter = new Waiter();
            customer.Order += waiter.Action;//Alt+Enter选择生成方法,自动在Waiter类中生成

            //10. 开始调用
            customer.Action();
            customer.PayTheBill();
            Console.ReadLine();
        }

    }

    //3. 新建一个数据类型用来传递消息,类名应该为对应的事件名+EventArgs后缀
    /// <summary>
    /// .Net规定,当某个类用途是作为EventArgs使用的,就应该继承于EventArgs
    /// </summary>
    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }//菜名

        public string Size { get; set; }//分量
    }


    //2. 为点菜这个Order事件创建一个委托,参数一为消费的人,参数二为点的菜,菜的分量,菜的价格等信息传递消息,因为没有这种数据类型,所以我们自己建一个
    //命名规则,如果是为一个事件创建委托,就用事件名后加EventHandler作为名字
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); //参数名简称为e
    /// <summary>
    /// 命名规则解释——OrderEventHandler,OrderEventArgs
    /// 1. 当别人看到这个委托后面有EventHandler后缀的时候就明白这个委托是专门用来声明事件的,就不会动他
    /// 2. 当你使用这个EventHandler后缀就表明了这个委托是用来约束事件处理器的
    /// 3. 表示这个委托未来创建出来的实例是专门用于存储事件处理器的
    /// </summary>



    //1. 创建一个消费者
    public class Customer//事件拥有者
    {
        //4. 不希望被外界访问使用private,创建一个委托字段
        private OrderEventHandler orderEventHandler { get; set; }//这个委托字段用来存储或者引用那些事件处理器

        //5. 声明一个事件
        public event OrderEventHandler Order//用OrderEventHandler类型约束的事件
        {
            //事件处理器的添加器
            add
            {
                this.orderEventHandler += value;
                //这里要+=一个从外界传进来的EventHandler,但是我们不知道传进来什么样的,所以用到上下文关键字value
            }

            //事件处理器的移除器
            remove
            {
                this.orderEventHandler -= value;
            }

        }

        public double Bill { get; set; }//账单
        public void PayTheBill()
        {
            Console.WriteLine("I will pay ${0}. ", this.Bill);
        }

        //8. 东西都齐了,我们需要一个方法触发事件,这个方法就是消费者进去餐馆坐下来想想点什么菜
        public void WalkIn()
        {
            Console.WriteLine("Walk into de restaurant");
        }
        public void SitDown()
        {
            Console.WriteLine("SitDown");
        }
        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think...");
                Thread.Sleep(1000);
            }

            if (this.orderEventHandler != null)//等于空就说明没人订阅这个事件
            {
                //没有事件处理器订阅这个事件的时候触发这个事件会得到一个异常,所以要先判断这个用来存储或者引用那些事件处理器的委托是否为空

                //如果不为空
                OrderEventArgs e = new OrderEventArgs();
                e.DishName = "Kongpao Chicken";
                e.Size = "large";
                this.orderEventHandler.Invoke(this, e);
                //参数一为这个消费者,参数二为想点什么菜,大份小份

            }

        }

        //9. 准备一个Action方法,他会一连串调用我们的Walkin,sitdown等方法
        public void Action()
        {
            Console.ReadLine();//程序会等我们敲个回车在执行
            this.WalkIn();
            this.SitDown();
            this.Think();

        }


        /*
            * 第一层意思
            事件需要一个委托类型作为约束,这个约束规定了事件能发送什么样的消息给事件响应者,也规定了事件响应者
        能收到什么样的事件消息,这就决定了事件响应者的事件处理器必须能跟这个约束匹配上,才能订阅这个事件
            * 
            * 第二层意思 
            当事件的响应者向事件的拥有者提供了能够匹配这个事件的事件处理器之后,要找个地方把事件处理器保存或者
        记录下来,那么能够记录或者引用方法的任务只有委托类型的实例才能做到,所以这里又会用到委托

        总而言之,事件这种成员无论从表层约束来讲还是底层实现来讲,他都是依赖于委托类型的

        委托是事件的底层基础,事件是委托的上层建筑,为了声明事件就得为他选一个委托


            */

    }

    //6. 创建事件响应者,服务员
    public class Waiter
    {
        public void Action(Customer customer, OrderEventArgs e)//服务员用自己的Action订阅着事件
        {
            //处理事件顺序,上菜
            Console.WriteLine("I will serve you the dish {0}", e.DishName);
            //记账
            double price = 10;
            switch (e.Size)
            {
                case "small":
                    price = price * 0.5;
                    break;
                case "large":
                    price *= 1.5;
                    break;
                default:
                    break;
            }

            customer.Bill += price;

        }
    }
}

结果如下:

 由上面的例子可知:
        事件需要一个委托类型作为约束,这个约束规定了事件能发送什么样的消息给事件响应者,也规定了事件响应者能收到什么样的事件消息,这就决定了事件响应者的事件处理器必须能跟这个约束匹配上,才能订阅这个事件
         当事件的响应者向事件的拥有者提供了能够匹配这个事件的事件处理器之后,要找个地方把事件处理器保存或者记录下来,那么能够记录或者引用方法的任务只有委托类型的实例才能做到,所以这里又会用到委托
        总而言之,事件这种成员无论从表层约束来讲还是底层实现来讲,他都是依赖于委托类型的
        委托是事件的底层基础,事件是委托的上层建筑,为了声明事件就得为他选一个委托

 再来看看平时简单的声明方式


using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace _9.事件的声明
{

class Program
    {
        static void Main(string[] args)
        {
            //例子说明:当你去饭店的时候,服务员过来订阅你点菜这个事件,这个事件发生服务员就给你上菜并在你账单上记一笔,等你走的时候付费

            //7. 声明一个Customer和Waiter成员,并添加事件处理器
            Customer customer = new Customer();
            Waiter waiter = new Waiter();
            customer.Order += waiter.Action;//Alt+Enter选择生成方法,自动在Waiter类中生成

            //10. 开始调用
            customer.Action();
            customer.PayTheBill();
            Console.ReadLine();
        }

    }


    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }//菜名

        public string Size { get; set; }//分量
    }

    //2. 为点菜这个Order事件创建一个委托,参数一为消费的人,参数二为点的菜,菜的分量,菜的价格等信息传递消息,因为没有这种数据类型,所以我们自己建一个
    public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); //参数名简称为e



    //1. 创建一个消费者
    public class Customer//事件拥有者
    {
        //4. 不希望被外界访问使用private,创建一个委托字段
        //private OrderEventHandler orderEventHandler { get; set; }//这个委托字段用来存储或者引用那些事件处理器
        //5. 声明一个事件
        //public event OrderEventHandler Order//用OrderEventHandler类型约束的事件
        //{
        //    //事件处理器的添加器
        //    add
        //    {
        //        this.orderEventHandler += value;
        //        //这里要+=一个从外界传进来的EventHandler,但是我们不知道传进来什么样的,所以用到上下文关键字value
        //    }
        //    //事件处理器的移除器
        //    remove
        //    {
        //        this.orderEventHandler -= value;
        //    }
        //}

        //简化声明
        public event OrderEventHandler Order;//声明完成,很像一个委托字段
                                             //此时没有声明委托来存储事件处理器的引用,那么他的引用存储在哪?

        public double Bill { get; set; }//账单
        public void PayTheBill()
        {
            Console.WriteLine("I will pay ${0}. ", this.Bill);
        }
        public void WalkIn()
        {
            Console.WriteLine("Walk into de restaurant");
        }
        public void SitDown()
        {
            Console.WriteLine("SitDown");
        }
        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think...");
                Thread.Sleep(1000);
            }

            if (this.Order != null)
            {
                OrderEventArgs e = new OrderEventArgs();
                e.DishName = "Kongpao Chicken";
                e.Size = "large";
                this.Order.Invoke(this, e);
            }

        }

        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }
    public class Waiter
    {
        public void Action(Customer customer, OrderEventArgs e)//服务员用自己的Action订阅着事件
        {
            Console.WriteLine("I will serve you the dish {0}", e.DishName);
            double price = 10;
            switch (e.Size)
            {
                case "small":
                    price = price * 0.5;
                    break;
                case "large":
                    price *= 1.5;
                    break;
                default:
                    break;
            }
            customer.Bill += price;
        }
    }
}

 结果同上

事件的借刀杀人

那么问题就来了
既然可以声明委托类型的字段,为什么还要事件这种成员呢?
        因为事件成员可以让程序的逻辑,以及对象之间的关系变得更加“有道理”,更加安全,谨防借刀杀人

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace _9.事件的声明
{ 

class Program
   {
    static void Main(string[] args)
    {
        Customer customer = new Customer();
        Waiter waiter = new Waiter();
        customer.Order += waiter.Action;
        //waiter提供的事件处理器是被customer使用自己的内部逻辑调用的
        //那么我把下一行 customer.Action();注释掉正常应该没人帮我点菜,运行不了
        //customer.Action();
        //由于现在使用的是委托类型的字段,而不是一个事件
        //有可能发生customer随便访问字段
        //例如:
        OrderEventArgs e = new OrderEventArgs();
        e.DishName = "满汉全席";
        e.Size = "large";

        OrderEventArgs e1 = new OrderEventArgs();
        e1.DishName = "beer";
        e1.Size = "large";
            //结果是明明是别人点的菜,全算在customer身上了

            Customer badGuy = new Customer();
            badGuy.Order += waiter.Action;
            badGuy.Order.Invoke(customer, e);
            badGuy.Order.Invoke(customer, e1);



            customer.PayTheBill();
        Console.ReadLine();
    }

}


public class OrderEventArgs : EventArgs
{
    public string DishName { get; set; }//菜名

    public string Size { get; set; }//分量
}


public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

public class Customer//事件拥有者
{
    //EventHandler是事件平台中最通用的委托
    public OrderEventHandler Order;//把Event删掉,Order就变成一个委托类型的字段了,运行发现效果一样

    //public event OrderEventHandler Order//用OrderEventHandler类型约束的事件
    //{
    //    //事件处理器的添加器
    //    add
    //    {
    //        this.orderEventHandler += value;
    //        //这里要+=一个从外界传进来的EventHandler,但是我们不知道传进来什么样的,所以用到上下文关键字value
    //    }
    //    //事件处理器的移除器
    //    remove
    //    {
    //        this.orderEventHandler -= value;
    //    }
    //}

    public double Bill { get; set; }//账单
    public void PayTheBill()
    {
        Console.WriteLine("I will pay ${0}. ", this.Bill);
    }
    public void WalkIn()
    {
        Console.WriteLine("Walk into de restaurant");
    }
    public void SitDown()
    {
        Console.WriteLine("SitDown");
    }
    public void Think()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine("Let me think...");
            Thread.Sleep(1000);
        }

        if (this.Order != null)
        {
            OrderEventArgs e = new OrderEventArgs();
            e.DishName = "Kongpao Chicken";
            e.Size = "large";
            this.Order.Invoke(this, e);
        }

    }

    public void Action()
    {
        Console.ReadLine();
        this.WalkIn();
        this.SitDown();
        this.Think();
    }
}
public class Waiter
{
    public void Action(Customer customer, OrderEventArgs e)//服务员用自己的Action订阅着事件
    {
        Console.WriteLine("I will serve you the dish {0}", e.DishName);
        double price = 10;
        switch (e.Size)
        {
            case "small":
                price = price * 0.5;
                break;
            case "large":
                price *= 1.5;
                break;
            default:
                break;
        }
        customer.Bill += price;
    }
}
}

结果如下:

使用.Net平台的EventHandler,来看看微软为我们准备的EventHandler

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace _9.事件的声明
{

class Program
    {
        static void Main(string[] args)
        {
            Customer customer = new Customer();
            Waiter waiter = new Waiter();
            customer.Order += waiter.Action;
            customer.Action();
            customer.PayTheBill();
            Console.ReadLine();
        }

    }


    public class OrderEventArgs : EventArgs
    {
        public string DishName { get; set; }//菜名

        public string Size { get; set; }//分量
    }

    //1. 这个委托其实没有比要声明,使用.Net平台为我们准备好的EventHandler就行
    //public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);

    public class Customer//事件拥有者
    {
        //2. 在这里更改 
        public event EventHandler Order;

        //public event OrderEventHandler Order//用OrderEventHandler类型约束的事件
        //{
        //    //事件处理器的添加器
        //    add
        //    {
        //        this.orderEventHandler += value;
        //        //这里要+=一个从外界传进来的EventHandler,但是我们不知道传进来什么样的,所以用到上下文关键字value
        //    }
        //    //事件处理器的移除器
        //    remove
        //    {
        //        this.orderEventHandler -= value;
        //    }
        //}

        public double Bill { get; set; }//账单
        public void PayTheBill()
        {
            Console.WriteLine("I will pay ${0}. ", this.Bill);
        }
        public void WalkIn()
        {
            Console.WriteLine("Walk into de restaurant");
        }
        public void SitDown()
        {
            Console.WriteLine("SitDown");
        }
        public void Think()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Let me think...");
                Thread.Sleep(1000);
            }
            this.OnOrder("kongpao chicken", "large");
        }

        // 按照命名规则和方法一致性原则,此处应该变更如下
        protected void OnOrder(string dishName, string size)//注意访问级别,以防外界调用,借刀杀人,子类也可以访问
        {
            if (this.Order != null)
            {
                OrderEventArgs e = new OrderEventArgs();
                e.DishName = dishName;
                e.Size = size;
                this.Order.Invoke(this, e);//事件必须由事件拥有者触发,此处this就是
            }
            //上面的IF语句中,之所以能用事件去比较是否为空,用事件调用事件处理器,是因为事件背后的委托类型字段是由
            //编译器自动生成的,我们访问不到,这时候就只能使用事件的名字来做这件事情
        }


        public void Action()
        {
            Console.ReadLine();
            this.WalkIn();
            this.SitDown();
            this.Think();
        }
    }
    public class Waiter
    {
        public void Action(object sender, EventArgs e)//3. 在此将参数类型改为object和EventArgs,服务员用自己的Action订阅着事件
        {
            //4. 为了处理第3项改动导致的异常,在这里进行类型转换
            Customer customer = sender as Customer;
            OrderEventArgs orderInfo = e as OrderEventArgs;

            Console.WriteLine("I will serve you the dish {0}", orderInfo.DishName);
            double price = 10;
            switch (orderInfo.Size)
            {
                case "small":
                    price = price * 0.5;
                    break;
                case "large":
                    price *= 1.5;
                    break;
                default:
                    break;
            }
            customer.Bill += price;
        }
}

 结果如下:
  

 最后,确认事件与委托的关系

事件是“以特殊方式声明的委托吗”? 
1. 不是!只是声明的时候“看起来像”(对比委托字段与事件的简化声明,field-like(像字段))
2. 事件声明的时候使用了委托类型,简化声明造成事件看起来像一个委托的字段(实例),而Event关键字则更像是一个修饰符——这就是错觉的来源之一
3. 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这让事件看起来更像是一个委托字段——这是错觉的又一来源
4. 重申:事件的本质是加装在委托上的一个“蒙版”,是个起掩蔽作用的包装器。这个用于阻挡非法操作的“蒙版”绝对不是委托字段本身

为什么使用委托类型来声明事件? 
1. 站在source的角度来看,是为了表明source能对外传递哪些消息
2. 站在subscribe的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件
3. 创建委托类型的实例将用于存储(引用)事件处理器

对比事件和属性 
1. 属性不是字段——很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用
2. 事件不是委托字段——它是委托字段的包装器,这个包装器用来保护委托字段不被滥用,委托字段被事件包装以后从外界就只能访问+=和-=,即只能为他添加或者移除事件处理器
3. 包装器永远都不可能是被包装的东西

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值