本文地址: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();
}
}