• 【C#学习笔记】事件


    在这里插入图片描述


    前言

    在之前我学习委托的时候,写到了

    学习了委托,事件其实也就学习了,事件和委托基本上一模一样:

    然而在实际工作中通过对事件的深入学习后发现,实际上事件的使用比委托要严格一些,本节将详细讲解事件的使用。

    视频参考:【事件•语法篇】如何声明自定义的事件以及事件的完整/简略声明格式



    事件的定义

    事件(event)有能力使一个类或者对象在发生相关事情的时候去通知其他类,对象们。简单来说一个事件在发生后会去通知所有的监听事件的成员函数,让它们进行对应的事件处理。

    乍一看事件和多播委托很像,实际上事件也是委托的一种特殊的封装。

    事件和事件模型

    在这里插入图片描述
    事件模型拥有五大要素,分别是:

    • 事件的拥有者
    • 事件
    • 事件的响应者
    • 事件处理器
    • 事件定义(+=)

    五大要素也很好理解,事件的拥有者就是定义事件的类或者对象,事件的响应者就是事件多播时注册处理器Handler方法的那些类或者对象。事件就是指这个特殊的委托封装,事件的处理器就是一种在委托约束下的方法。事件定义就是注册方法的操作符(只能是+=-=)。

    事件区别于委托,有一个重要的限制,就是事件Event和事件处理器EventHandler必须属于同一委托类型,如果不是同一委托类型,则事件处理器和事件就是不匹配的。

    本质上,事件是基于委托的,一方面,事件的注册需要使用委托类型进行约束,它约束了该事件应该处理什么类型的事件数据EventArgs以保证类型兼容。另一方面,事件中注册的各种Handler的调度也是基于多播委托的。

    使用事件的好处

    使用事件的好处在于,通过对委托的封装增加了一些更严格的使用规则:例如事件只能放在+=-=的左侧,就避免了对委托直接用=赋值导致整个委托被重置的问题。例如事件必须定义senderFooEventArgs,就方便我们对拥有者以及传递的数据进行适当的处理。


    事件的声明格式

    .Net中规定,声明事件的委托必须使用EventHandler作为结尾,提高代码可读性。而实际上这个EventHandler也是官方给出的一种标准的委托类型:

    public delegate void EventHandler(object sender, EventArgs e);
    
    • 1

    其中,响应者或者处理者是sender,类型是万物之父object,也就是可接收所有类。数据类型是EventArgs,这是事件的“处理数据”的基类,任何事件中用于传递或处理的数据都必须继承于EventArgs这个基类。同样的,继承于EventArgs类型的处理数据也需要以XXXEventArgs来命名,表示它是XXXEventHandler的事件数据。

    使用事件的方法是仿照上述委托类型声明一个全新的事件委托,当然也可以直接使用EventHandler这个事件,但是要避免由于object的类型转换所产生的装箱拆箱,在直接使用EventHandler的时候,如果传入不同类型的sender,为了避免强转使用导致的装箱拆箱,通常用as来进行隐式转换。

    事件的完整声明

    让我们来写一段完整的自定义事件声明的格式代码,以视频中的代码为例,这个事件的拥有者是客户,事件是一个点单的事件,事件的响应者是服务员,事件处理器是客户的点单事件EventHandler:

    // .Net中规定,声明事件的委托必须使用EventHandler作为结尾,提高代码可读性
    // 该委托指定了事件的类型约束,其中响应者Sender的类型是Customer,处理数据是OrderEventArgs
    public delegate void OrderEventHandler(Customer _customer,OrderEventArgs _e);
    
    public class Customer
    {
    	public float Bill {get;set;}
    	public void PayTheBill()
    	{
    		Debug.Log("I have to pay:" + this.Bill);
    	}
    	// 定义完整的事件声明格式
    	// 这个orderEventHandler私有委托被封装在public的事件当中,用于限制对委托的访问
    	private OrderEventHandler orderEventHandler;
    	// 定义事件OnOrder,完整声明类似于属性,需要定义基本的添加器和移除器
    	public event OrderEventHandler OnOrder
    	{
    		add
    		{
    			orderEventHandler += value;
    		}
    		remove
    		{
    			orderEventHandler -= value;
    		}
    	}
    }
    
    // 继承了EventArgs基类的对应事件的处理数据,并定义其内部属性
    public class OrderEventArgs : EventArgs
    {
    	public string CoffeeName {get;set;}
    	public string CoffeeSize {get;set;}
    	public float CoffeePrice {get;set;}
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35

    现在,我们已经准备好了一个事件和它的拥有者,接下来需要一个响应者来处理事件。

    public class EventEx : MonoBehavior
    {
    	Customer customer = new Customer();
    	Waiter waiter = new Waiter();
    	
    	private void Start()
    	{
    		customer.OnOrder += waiter.TakeAction;
    	}
    }
    
    public class Waiter
    {
    	事件响应通过事件传递的事件数据中的咖啡size的类型来判断每个客户的订单应该收什么价格。
    	internal void TakeAction(Customer _customer, OrderEventArgs _e)
    	{
    		float finalPrice = 0;
    		switch(_e.CoffeeSize)
    		{
    			case "Tall":
    				finalPrice  = _e.CoffeePrice;break;
    			case "Grand":
    				finalPrice  = _e.CoffeePrice + 3;break;
    			case "Venti":
    				finalPrice  = _e.CoffeePrice + 6;break;
    		}
    		_customer.Bill += finalPrice;
    	}
    }
    
    • 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

    最后我们还需要触发这个事件,因此我们在Customer中定义一个Order函数来触发委托。只需要为委托传入类型匹配的参数,即可触发所有绑定的事件处理器EventHandler:

    public class EventEx : MonoBehaviour
    {
    	Customer customer = new Customer();
    	Waiter waiter = new Waiter();
    	OrderEventArgs e = new OrderEventArgs();
    	private void Start()
    	{
    		customer.OnOrder += waiter.TakeAction;
    		customer.Order();
    		// 输出结果:I have to pay:64
    		customer.PayTheBill();
    	}
    }
    
    public delegate void OrderEventHandler(Customer _customer, OrderEventArgs _e);
    
    public class Customer
    {
    	public float Bill { get; set; }
    	public void PayTheBill()
    	{
    		Debug.Log("I have to pay:" + this.Bill);
    	}
    	private OrderEventHandler orderEventHandler;
    	public event OrderEventHandler OnOrder
    	{
    		add
    		{
    			orderEventHandler += value;
    		}
    		remove
    		{
    			orderEventHandler -= value;
    		}
    	}
    	public void Order()
        {
        	// 为两杯咖啡触发了两次点单事件
    		if(orderEventHandler != null)
            {
    			OrderEventArgs e = new OrderEventArgs();
    			e.CoffeeName = "Mocha";
    			e.CoffeeSize = "Tall";
    			e.CoffeePrice = 28;
    
    			orderEventHandler(this, e);
    
    			OrderEventArgs e1 = new OrderEventArgs();
    			e1.CoffeeName = "Latte";
    			e1.CoffeeSize = "Venti";
    			e1.CoffeePrice = 30;
    
    			orderEventHandler(this, e1);
    		}
        }
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    小结

    小结一下刚才讲的内容:
    首先我们应当确定好事件的拥有者和响应者之间的关系,例如顾客和服务员,因为我们需要顾客点单,服务员才会有反应。因此顾客是事件的拥有者,当其点单之后服务员作为响应者去响应这个事件。

    然后需要定义事件,在成员外部定义事件的FooEventHandler的委托约束,并定义内部senderFooEventArgs的类型。在事件进行完整定义的时候,需要在成员内部(事件拥有者)定义委托fooEventHandler和事件OnFoo(包括对添加器Add和移除器Remove的定义)。

    最后,将事件与响应Handler绑定,想要使用的时候就直接调用即可。


    事件的简略声明

    通常事件的声明,往往使用更简略的声明方式。简略声明的好处是提供了一些特殊的语法糖。

    	public event OrderEventHandler OnOrder;
    	public void Order()
        {
    		if(OnOrder != null)
            {
    			OrderEventArgs e = new OrderEventArgs();
    			e.CoffeeName = "Mocha";
    			e.CoffeeSize = "Tall";
    			e.CoffeePrice = 28;
    
    			OnOrder(this, e);
    
    			OrderEventArgs e1 = new OrderEventArgs();
    			e1.CoffeeName = "Latte";
    			e1.CoffeeSize = "Venti";
    			e1.CoffeePrice = 30;
    
    			OnOrder.Invoke(this, e1);
    		}
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们修改一下顾客类中的事件声明和代码。发现几个特点:

    1. OnOrder直接用event关键字声明了一个事件,而不是先声明一个委托,再声明事件中对委托的添加器和移除器的定义。
    2. Order直接用!=来比较委托是否为空,我们说事件的操作符只能是-=+=,在此处却可以使用!=甚至=(仅限成员函数内部),这也是迫不得已,因为我们没有定义委托,所以直接用事件来代替委托进行操作。然而委托真的没有被定义吗?只是编译器内部帮我们定义好了一个委托,我们看不到而已。
    3. 在触发事件的时候,不仅用通常的方法OnOrder(this, e);来触发,还可以使用OnOrder?.Invoke(this, e1);OnOrder.Invoke(this, e1);来进行触发,更加灵活了。

    从上述代码来看,简略声明的事件更灵活,更强大。

    此外,由于简略声明事件的定义格式public event OrderEventHandler OnOrder;,不要误以为它是一个字段,只是语法糖的存在让它看起来长得像一个字段。实际上还是一个事件。


    泛型委托定义下的事件

    除了常态的委托类型之外,定义事件我们也可以用到泛型委托,例如微软官方提供的泛型委托:

    public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
    
    • 1

    所以我们也可以定义一个泛型委托,例如不止顾客点单,服务员自己也可以给自己点咖啡,在不继承同一个基类的情况下就可以用泛型委托来接受不同类型对象的事件响应。

    public delegate void OrderEventHandler<Tsender>(Tsender sender, OrderEventArgs _e);
    
    • 1

    为什么使用事件?

    如果我们将下列事件中的event关键字去掉,可以正常处理上述代码吗?答案是可以

    public event OrderEventHandler OnOrder;
    
    • 1
    //变成了委托
    public  OrderEventHandler OnOrder;
    
    • 1
    • 2

    既然如此,我们为什么要使用事件呢?
    因为委托的封装不够严密,不符合我们对于事件的想象。我们可以用如下方式去访问类中public的委托:

    customer1.OnOrder(customer1,e1);
    customer2.OnOrder(customer1,e2);
    
    • 1
    • 2

    在上述代码中,顾客1为自己点了一份名为e1的订单,这是没有问题的。
    但是顾客2也为顾客1点了一份名为e2的订单,顾客2直接访问了顾客1中public出来的委托字段,一般而言,我们不希望通过这样的方式去为其他类触发事件。这会造成一些逻辑上的错误。使用事件,就可以把其对应的委托封装起来,避免一些奇怪的用法。

    事件的存在就是为了阻止一些委托调度的“非法操作”,更安全,更有约束。

  • 相关阅读:
    【电源专题】案例:电源芯片对输入索取大电流的异常开发板测试能出现,但整机就无法复现?
    你知道聊天机器人在医疗保健行业发挥了什么作用吗?
    win环境安装Node.js的多种方式
    git远程操作
    学生党双11有哪些值得入手的蓝牙耳机?适合学生党的蓝牙耳机推荐
    Docker介绍
    myj的尘歌壶
    Docker部署RustDesk Server 设置开机自启
    使用自定义数据训练 YOLOv10
    商鼎云新版来袭 | 收藏夹功能已上线,满足个人使用需求​
  • 原文地址:https://blog.csdn.net/milu_ELK/article/details/134271671