本文地址: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的時候會想盡一切辦法通知你(包括但不限於),而你只需要等着接收通知即可。
鬧鍾負責發布消息,而你關注着鬧鍾發來的消息,這就構成了一對“發布者-訂閱者”的關系(你訂閱了鬧鍾發布的事件)。
關於發布者和訂閱者這一名稱,可以想象一家報紙,在發生某個重大事件時,這家報紙把這個事件刊載到頭版分發出去,所有訂閱這家報紙的人就都能獲取到這一消息。
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
中聲明Alarm
和Person
各一個實例,然后手動+=
來進行實際的訂閱,像這樣:
static void Main(string[] args)
{
Person p = new Person();
Alarm a = new Alarm(6);
a.Time8 += p.OnTime8; // <<--2
// ...
}
<<--2
處就完成了p
對a.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
訂閱自己Alarm
的Time8
事件,這么寫語義上更能體現這個鬧鍾是我的鬧鍾的含義,這種寫法我會在下面代碼匯總中提供。
總結
這一部分我們初步了解了事件在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();
}
}