• C# Event (1) —— 我想搞个事件


    本文地址:https://www.cnblogs.com/oberon-zjt0806/p/15975299.html
    本文最初来自于博客园
    本文遵循CC BY-NC-SA 4.0协议,转载请注明出处
    本文还会有后续,所以本篇只是一个非常简单的场景

    Event(事件)

    何谓Event,我们不妨看一下词典对Event的解释:

    something that happens, especially something important, interesting or unusual.
    在发生的某件事,特指比较重要、有趣或者非同寻常的事情
    - Longman Dictionary

    历史上或社会上发生的不平常的大事情。
    - 现代汉语词典

    可能会有人觉得,这说了跟没说一样,其实不然,无论是中文还是英文的解释,都侧重于一点——

    不寻常,值得关注

    而我们的目的也在于此,我们需要关注程序内部可能发生的一些特定的状况,然后在这个时间点下采取行动,这个特定的发生状况的时间点就是我们需要关注的事件。

    当然,词典可能把事件这个此上升到了社会这种高度,认为特别重大的才叫做事件。但在程序中,其实任何需要关注的事情我们都可以冠以事件的名头。

    比如??

    你每天早上9点上班,但是你家到目的地需要差不多1个小时,那么8点钟到了(OnTime8)这件事就可以成为你需要关注的事件

    如果你不希望因为迟到导致自己遭受惩罚的时候,那么你就必须保证在8点钟到了的时候采取正确的行动——起床(GetUp)。
    换言之,你不能错过这个事件。

    那我如何不错过这个事件??

    当然,作为一个顶级大忙人,你可不会在这个事件发生之前一直盯着钟看,不然你可能就没有心思做别的事情了,毕竟在8点之前你还有别的事情要忙,比如……忙着睡觉

    在这种情况下,如果你不想因为睡过头而错过这一事件的时候(而你自己又没办法一直盯着钟发呆)的情况下,就需要有别的东西(或者人,如果可以的话)帮你盯着时间,然后在适当的时机告知你。

    比如,一台性能强劲的闹钟(强劲到无论你睡多死都能把你叫醒),就是一个不错的选择。

    发布者-订阅者(Publisher-Subscriber)

    好了,现在你发现你和闹钟之间构成了这样一种关系:

    闹钟(当然和世界时间保持一致)负责盯着时间,在观察到指针从7到达8的时候会想尽一切办法通知你(包括但不限于),而你只需要等着接收通知即可。

    闹钟负责发布消息,而你关注着闹钟发来的消息,这就构成了一对“发布者-订阅者”的关系(你订阅了闹钟发布的事件)。

    发布事件
    订阅通知
    采取行动
    8点啦
    听到
    起床
    发布者
    订阅者
    闹钟

    关于发布者和订阅者这一名称,可以想象一家报纸,在发生某个重大事件时,这家报纸把这个事件刊载到头版分发出去,所有订阅这家报纸的人就都能获取到这一消息。

    C#的Event

    现在我们可以用C#来表示上面我们提到的这个关系。

    假设一个人是这样的:

    public partial class Person
    {
    	private bool isSleeping;
    	public bool IsSleeping{get => isSleeping;}
    	public Person()
    	{
    		isSleeping = true;
    		Console.WriteLine("Sleeping...");
    	}
    	public void GetUp()
    	{
    		isSleeping = false;
    		Console.WriteLine("Wake up!");
    	}
    }
    

    So far so good,这个Person将作为订阅者,但是这个人一旦睡着,除非他自己主动想GetUp以外没有任何可以醒来的方式。

    于是,我们把发布者(也就是闹钟)引入进来,需要说明的是,我们这里并不打算做一个可以真的会走的闹钟,只是做一个简单的闹钟模型来模拟一下经过。

    public partial class Alarm
    {
    	private int hour;
    	public int Hour
    	{
    		get => hour;
    		private set
    		{
    			hour = value % 24;
    			Console.WriteLine($"It's {hour} o'clock.");
    		}
    	}
    	public Alarm(int hour)
    	{
    		this.Hour = hour;
    	}
    	public void StepHour() //往前走1小时
    	{
    		Hour++;
    	}
    }
    

    现在两个角色都建立起来了,但是没有建立事件的联系,也就是说现在的闹钟和你之间各玩各的,谁也管不着谁。

    声明一个事件

    C#中使用event关键字来声明一个事件……

    ……

    那事件本身如何处理呢??实际上C#中事件处理器本质上是一个特殊的委托

    我们知道,委托是可执行的方法集,就像一把枪,装上若干可执行的函数作为子弹,然后在调用委托的时候把子弹全部打出去(里面的方法挨个调用一遍),当然了,和子弹不同,方法打出去之后并不会消失,而是还留在委托内(术语上称之为多路广播机制)。C#事件就是使用委托这套机制来实现的。

    在这个例子中我们姑且先定义一个无参数的委托:

    public void delegate Time8Handler();
    

    然后在Alarm中加上:

    public partial class Alarm
    {
    	public event Time8Handler Time8;
    }
    

    这样就在Alarm侧声明了一个名为Time8事件,需要注意这句声明,public event Time8Handler Time8;如果拿走event关键字,那么这句话就是一个普通的委托声明,但具备event之后它就变成了事件委托。

    事件委托的特点

    事件委托和普通委托有什么区别呢。在事件发布类的范围内,事件和一个委托没有什么区别,但在事件外,事件有如下特点:

    对于发布器以外的对象,不能使用()Invoke主动调用事件委托(实际上是只能出现在+=-=之左侧)

    了解了上述特点之后我们就能更进一步理解C#中的发布-订阅机制是如何运作的:

    发布者提供一个事件委托作为事件入口,比如Alarm.Time8,发布者的事件允许订阅者将自己的事件行为(处理函数)装入这个事件。
    订阅者决定了自身在事件中采取的行动,而发布者则只决定事件产生/行动执行的时机(何时调用)。
    在发布者调用事件的这一刻,事件内所有被装入的行为方法会被一炮打出,订阅者就会执行相应的行为。

    发布者(Publisher) - Alarm

    于是在Alarm中,我们需要确定执行的Time8的执行时机,由于我们希望这个事件在hour达到8的时候执行,由于这里我们使用属性Hour做了封装了,因此我们直接在setter里以发布者的名义调用该事件委托,setter里面做如下修改

    public partial class Alarm
    {
    	//...
    	public int Hour
    	{
    		// getter ...
    
    		private set
    		{
    			hour = value % 24;
    			Console.WriteLine($"It's {hour} o'clock.");
    			if(hour == 8)
    				Time8?.Invoke(); // <<--1
    		}
    	}
    	//...
    }
    

    注意<<--1处标记的代码,语法上这里实际上可以直接用Time8()调用。但是我们不能排除Time8委托里没有装入任何方法以及Time8 == null的情形,在这种情况下直接调用事件委托会抛出System.NullReferenceException异常。

    因此MSDN中推荐使用?.Invoke()的方式调用可以轻松避免这种问题,当然如果不喜欢这样做的话那就谨记在Time8();之前用if(Time8 != null)来判断一下。

    至此发布者的角色准备就绪。

    订阅者(Subscriber) - 你

    好了,现在闹钟可以响了,但是如果你不在意闹钟的声音的话,那闹钟就算把自己敲烂也不会叫醒你,毕竟常言说得好:

    你永远叫不醒一个装睡的人

    因此只有你关注(订阅)了发布者的事件时,你才会受到事件的影响。

    首先我们考虑当Alarm.Time8事件发生时,作为Person的你会采取什么行为,显然,在本例中,你需要GetUp

    不过注意一下,尽管在本例中你可以直接把public void GetUp()订阅到Time8上(因为委托型相同),但这并非一个很好的做法,在更加复杂的情形下,我们可能需要GetUp同时还要执行别的事情(比如Wash或者Breakfast什么的),种种原因可能会使你不能把这些行为都订阅到这个事件上(1是委托型不保证相同,2是难以保证在多路广播下委托装入的函数执行顺序是什么样的),因此更明智的做法是单独为事件委托提供一个事件函数,然后让这个事件函数单独订阅专门的事件,所以我们这里在定义一个OnTime8函数:

    public partial class Person
    {
    	public void OnTime8()
    	{
    		GetUp();
    	}
    }
    

    到目前为止我们只是定义了打算用于事件的函数,但这个函数本身还没有和Time8事件建立联系。

    接下来我们有两种做法来完成这件事,一是在Main中声明AlarmPerson各一个实例,然后手动+=来进行实际的订阅,像这样:

    static void Main(string[] args)
    {
    	Person p = new Person();
    	Alarm a = new Alarm(6);
    	a.Time8 += p.OnTime8; // <<--2
    	// ...
    }
    

    <<--2处就完成了pa.Time8事件的订阅。如果此刻我们继续往下写……

    static void Main(string[] args)
    {
    	// ...
    	a.StepHour(); // a变成了7点,没到8点不会起床
    	a.StepHour(); // a变成了8点,执行Time8委托,其中包含了p.OnTime8
    }
    

    执行之后的结果就是

    Sleeping...
    It's 6 o'clock.
    It's 7 o'clock.
    It's 8 o'clock.
    Wake up!
    

    还有一种做法,就是我在Person里声明一个Alarm然后让Person订阅自己AlarmTime8事件,这么写语义上更能体现这个闹钟是我的闹钟的含义,这种写法我会在下面代码汇总中提供。

    总结

    这一部分我们初步了解了事件在C#中表现的角色和作用,通过一个十分简单的例子体验了一下事件的基本用法,在后续的几篇中我们会遇到更加复杂的情形,敬请期待……

    代码汇总

    public delegate void Time8Handler();
    
    public class Alarm
    {
    	private int hour;
    	public int Hour
    	{
    		get => hour;
    		private set
    		{
    			hour = value % 24;
    			Console.WriteLine($"It's {hour} o'clock.");
    			if (hour == 8)
    				Time8?.Invoke();
    		}
    	}
    	public Alarm(int hour)
    	{
    		this.Hour = hour;
    	}
    	public void StepHour()
    	{
    		Hour++;
    	}
    
    	public event Time8Handler Time8;
    
    }
    
    public class Person
    {
    	public Alarm alarm;
    	private bool isSleeping;
    	public bool IsSleeping { get => isSleeping; }
    	public Person(int hour)
    	{
    		isSleeping = true;
    		Console.WriteLine("Sleeping...");
    		alarm = new Alarm(hour);
    		alarm.Time8 += OnTime8;
    	}
    	public void GetUp()
    	{
    		isSleeping = false;
    		Console.WriteLine("Wake up!");
    	}
    
    	public void OnTime8()
    	{
    		GetUp();
    	}
    }
    
    public class Program
    {
    	static void Main(string[] args)
    	{
    		Person pYou = new Person(6);
    		pYou.alarm.StepHour();
    		pYou.alarm.StepHour();
    	}
    }
    
  • 相关阅读:
    神经网络反向传播的数学原理
    Java项目-文件搜索工具
    MySQL锁概述
    spark ui的job数,stage数以及task数
    [dp]Task Computing 2022牛客多校第4场 A
    http客户端Feign使用
    基于内容的图像检索系统设计与实现--颜色信息--纹理信息--形状信息--PHASH--SHFT特征点的综合检测项目,包含简易版与完整版的源码及数据!
    Redisson实现分布式锁
    前端入门 —— 了解 webpack 和 各类插件的配置
    (迷宫问题)DFS(递归+非递归)
  • 原文地址:https://www.cnblogs.com/oberon-zjt0806/p/15975299.html