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的時候會想盡一切辦法通知你(包括但不限於),而你只需要等着接收通知即可。

鬧鍾負責發布消息,而你關注着鬧鍾發來的消息,這就構成了一對“發布者-訂閱者”的關系(你訂閱了鬧鍾發布的事件)。

graph LR publisher[發布者] subscriber[訂閱者] publisher -->|發布事件| subscriber -->|訂閱通知| publisher subscriber -->|采取行動| subscriber alarm[鬧鍾] you[你] alarm -->|8點啦| you -->|聽到| alarm you -->|起床| you

關於發布者和訂閱者這一名稱,可以想象一家報紙,在發生某個重大事件時,這家報紙把這個事件刊載到頭版分發出去,所有訂閱這家報紙的人就都能獲取到這一消息。

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();
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM