《CLR Via C# 第3版》筆記之(十二) - 事件
熟悉C#中的事件機(jī)制,使得我們可以編寫出更加貼近于實(shí)際情況的程序。
主要內(nèi)容:
- 本例中事件的場(chǎng)景介紹
- 事件的構(gòu)造
- 注冊(cè)/注銷事件
- 事件在編譯器中的實(shí)現(xiàn)
- 顯式實(shí)現(xiàn)事件
1. 本例中事件的場(chǎng)景介紹
為了更好的介紹事件的機(jī)制,首先我們構(gòu)造一個(gè)使用事件的場(chǎng)景(是我以前面試時(shí)遇到的一個(gè)編程題)。
具體場(chǎng)景大概是這樣的:某工廠有個(gè)設(shè)備,當(dāng)這個(gè)設(shè)備的溫度達(dá)到90攝氏度時(shí),觸發(fā)警報(bào)器報(bào)警,同時(shí)發(fā)送短信通知相關(guān)工作人員。
當(dāng)時(shí)我就簡(jiǎn)單的構(gòu)造3個(gè)類:設(shè)備(Equipment),警報(bào)器(Alert),短信裝置(Message)。
傳統(tǒng)的實(shí)現(xiàn)方法:
1. 警報(bào)器類(Alert)中編寫一個(gè)報(bào)警的方法(StartAlert),短信裝置類(Message)中編寫一個(gè)發(fā)短信的方法(SendMessage)
2. 在設(shè)備類(Equipment)中編寫一個(gè)溫度90度時(shí)調(diào)用的方法(SimulateTemperature90)
3. 在設(shè)備類(Equipment)中引用警報(bào)器類(Alert)和短信裝置類(Message)
4. 當(dāng)溫度90度時(shí),SimulateTemperature90中會(huì)調(diào)用Alert.StartAlert方法和Message.SendMessage方法來(lái)完成所需功能
基于事件的實(shí)現(xiàn)方法:
1. 警報(bào)器類(Alert)中編寫一個(gè)報(bào)警的方法(StartAlert),短信裝置類(Message)中編寫一個(gè)發(fā)短信的方法(SendMessage)
2. 在設(shè)備類(Equipment)中編寫一個(gè)溫度90度時(shí)調(diào)用的方法(SimulateTemperature90)
3. 在設(shè)備類(Equipment)中編寫一個(gè)事件(Temperature90)
4. 警報(bào)器類(Alert)向設(shè)備類(Equipment)注冊(cè)StartAlert,短信裝置類(Message)向設(shè)備類(Equipment)注冊(cè)SendMessage
5. 當(dāng)溫度90度時(shí),SimulateTemperature90會(huì)觸發(fā)事件(Temperature90),
此事件會(huì)調(diào)用已注冊(cè)的Alert.StartAlert方法和Message.SendMessage方法來(lái)完成所需功能
傳統(tǒng)的方法缺陷:
1. 設(shè)備類(Equipment)關(guān)注警報(bào)器類(Alert)和短信裝置類(Message),當(dāng)警報(bào)器類(Alert)中的方法變更時(shí),
除了要修改警報(bào)器類(Alert),還必須修改設(shè)備類(Equipment)中調(diào)用警報(bào)器類(Alert)中方法的地方
2. 增加功能時(shí),比如增加另一個(gè)報(bào)警裝置(Alert2)時(shí),需要修改設(shè)備類(Equipment)中報(bào)警的地方
基于事件的實(shí)現(xiàn)方法可以很好的改善上述情況:
1. 警報(bào)器類(Alert)和短信裝置類(Message)關(guān)注設(shè)備類(Equipment),當(dāng)警報(bào)器類(Alert)中的方法變更時(shí),
只需修改警報(bào)器類(Alert)中注冊(cè)事件的地方,將新的事件注冊(cè)到設(shè)備類(Equipment)的事件(Temperature90)中即可
2. 增加功能時(shí),比如增加另一個(gè)報(bào)警裝置(Alert2)時(shí),將新的報(bào)警方法注冊(cè)到事件(Temperature90)中即可
3. 取消警報(bào)器類(Alert)和短信裝置類(Message)事件時(shí),只需在相應(yīng)的類中注銷事件就行,無(wú)需修改設(shè)備類(Equipment)
2. 事件的構(gòu)造
下面以事件的方法來(lái)構(gòu)造上述應(yīng)用。
1. 編寫事件傳遞的參數(shù),暫時(shí)只包含設(shè)備名
2. 定義事件成員
3. 定義觸發(fā)事件的方法
4. 定義模擬觸發(fā)事件的方法
代碼如下:
// 編寫事件傳遞的參數(shù),暫時(shí)只包含設(shè)備名
public class EquipmentEventArgs:EventArgs
{
// 設(shè)備名
private readonly string equipName;
public string EquipName { get { return equipName; } }
public EquipmentEventArgs(string en)
{
equipName = en;
}
}
public class Equipment
{
// 設(shè)備名
private readonly string equipName;
public string EquipName { get { return equipName; } }
public Equipment(string en)
{
equipName = en;
}
// 定義事件成員
public event EventHandler<EquipmentEventArgs> Temperature90;
// 定義觸發(fā)事件的方法
protected void OnTemperature90(EquipmentEventArgs e)
{
Temperature90(this, e);
}
// 定義模擬觸發(fā)事件的方法
public void SimulateTemperature90()
{
// 事件參數(shù)的初始化
EquipmentEventArgs e = new EquipmentEventArgs(this.EquipName);
// 觸發(fā)事件
OnTemperature90(e);
}
}
3. 注冊(cè)/注銷事件
定義警報(bào)器類(Alert)和短信裝置類(Message),并在其中實(shí)現(xiàn)注冊(cè)/注銷事件的方法。
代碼如下:
// 警報(bào)器類
public class Alert
{
// 定義要注冊(cè)的函數(shù),注意此函數(shù)的簽名是與 EventHandler<EquipmentEventArgs>一致的
private void StartAlert(Object sender, EquipmentEventArgs e)
{
Console.WriteLine("Equipment: " + e.EquipName + "'s temperature is 90 now!");
}
// 向Equipment注冊(cè)事件
public void Register(Equipment equip)
{
equip.Temperature90 += StartAlert;
}
// 向Equipment注銷事件
public void UnRegister(Equipment equip)
{
equip.Temperature90 -= StartAlert;
}
}
// 短信裝置類
public class Message
{
// 定義要注冊(cè)的函數(shù),注意此函數(shù)的簽名是與 EventHandler<EquipmentEventArgs>一致的
private void SendMessage(Object sender, EquipmentEventArgs e)
{
Console.WriteLine("Equipment: " + e.EquipName + " sends ‘temperature is 90 now!’ to administrator");
}
// 向Equipment注冊(cè)事件
public void Register(Equipment equip)
{
equip.Temperature90 += SendMessage;
}
// 向Equipment注銷事件
public void UnRegister(Equipment equip)
{
equip.Temperature90 -= SendMessage;
}
}
調(diào)用上述事件的代碼如下:
class CLRviaCSharp_12
{
static void Main(string[] args)
{
Equipment eq = new Equipment("My Equipment");
Alert alert = new Alert();
Message msg = new Message();
// 注冊(cè)Alert和Message的事件后
Console.WriteLine("=========注冊(cè)Alert和Message的事件后=================");
alert.Register(eq);
msg.Register(eq);
eq.SimulateTemperature90();
// 注銷Alert的事件后
Console.WriteLine();
Console.WriteLine("=========注銷Alert的事件后,只有Message事件==========");
alert.UnRegister(eq);
eq.SimulateTemperature90();
Console.ReadKey(true);
}
}
調(diào)用結(jié)果如下:
4. 事件在編譯器中的實(shí)現(xiàn)
在事件的注冊(cè)/注銷時(shí),我們僅僅是簡(jiǎn)單使用 +=和-=。那么編譯其是如何注冊(cè)/注銷事件的呢。
原來(lái)編譯器在編譯時(shí)會(huì)自動(dòng)根據(jù)我們定義的公共事件(public event EventHandler<EquipmentEventArgs> Temperature90)
生成私有字段Temperature90和事件的add和remove方法。通過ILSpy查看上面編譯出的程序集,如下圖:
使用 +=和-=時(shí),就是調(diào)用編譯器生成的add_***和remove_***方法。
下面就是Alert類的Registered和UnRegister方法的IL代碼。
事件是引用類型,那么事件在進(jìn)行注冊(cè)或者注銷時(shí)會(huì)不會(huì)存在線程并發(fā)的問題。比如多個(gè)線程同時(shí)向設(shè)備類(Equipment)注冊(cè)或注銷事件時(shí),會(huì)不會(huì)出現(xiàn)注冊(cè)或注銷不成功的情況呢。
我們進(jìn)一步觀察add_Temperature90的IL代碼如下
主要部分就是上圖中的紅色線框部分,可能有些人不太熟悉IL代碼,我將上面的代碼翻譯成C#大致如下:
public void add_Temperature90(EventHandler<EquipmentEventArgs> value)
{
//[0] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>
EventHandler<EquipmentEventArgs> args0;
//[1] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>
EventHandler<EquipmentEventArgs> args1;
//[2] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>
EventHandler<EquipmentEventArgs> args2;
//[3] bool
bool args3;
// IL_0000 ~ IL_0006
args0 = this.Temperature90;
// IL_0007 ~ IL_002d
do
{
// IL_0007 ~ IL_0008
args1 = args0;
// IL_0009 ~ IL_0015
args2 = (EventHandler<EquipmentEventArgs>)System.Delegate.Combine(args1, value);
// IL_0016 ~ IL_0023
args0 = System.Threading.Interlocked.CompareExchange<EventHandler<EquipmentEventArgs>>(ref this.Temperature90, args2, args1);
// IL_0024 ~ IL_002d
if (args0 != args1)
args3 = true;
else
args3 = false;
}
while (args3);
}
從上面代碼可以看出,添加新的事件后(IL_0009 ~ IL_0015),IL會(huì)繼續(xù)驗(yàn)證原有的事件是否被其他線程修改過( IL_0016 ~ IL_0023)
所以我們?cè)谧?cè)/注銷事件時(shí)不用擔(dān)心線程安全的問題。
5. 顯式實(shí)現(xiàn)事件
從上面的IL代碼,我們看出每個(gè)事件都會(huì)生成相應(yīng)的私有字段和相應(yīng)的add_***和remove_***方法。
對(duì)于一個(gè)有很多事件的類(比如Windows.Forms.Control)來(lái)說(shuō),將會(huì)生成大量的事件代碼,而我們?cè)谑褂脮r(shí)往往只是使用其中很少的一部分事件。
這樣使得在創(chuàng)建這些類的時(shí)候浪費(fèi)大量的內(nèi)存。為了避免這種現(xiàn)象和高效率的存取事件委托,我們可以構(gòu)造一個(gè)字典來(lái)維護(hù)大量的事件。
Jeffery Richard在《CLR via C#》中給了我們一個(gè)很好的例子,為了以后參考方便,摘抄如下:
using System;
using System.Collections.Generic;
using System.Threading;
// 這個(gè)類的目的是在使用EventSet時(shí),提供
// 多一點(diǎn)的類型安全型和代碼可維護(hù)性
public sealed class EventKey : Object { }
public sealed class EventSet
{
// 私有字典,用于維護(hù) EventKey -> Delegate映射
private readonly Dictionary<EventKey, Delegate> m_events =
new Dictionary<EventKey, Delegate>();
// 添加一個(gè)EventKey -> Delegate映射(如果EventKey不存在),
// 或者將一個(gè)委托與一個(gè)現(xiàn)在EventKey合并
public void Add(EventKey eventKey, Delegate handler)
{
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey, out d);
m_events[eventKey] = Delegate.Combine(d, handler);
Monitor.Exit(m_events);
}
// 從EventKey(如果它存在)刪除一個(gè)委托,并且
// 在刪除最后一個(gè)委托時(shí)刪除EventKey -> Delegate映射
public void Remove(EventKey eventKey, Delegate handler)
{
Monitor.Enter(m_events);
// 調(diào)用TryGetValue,確保在嘗試從集合中刪除一個(gè)不存在的EventKey時(shí),
// 不會(huì)拋出一個(gè)異常。
Delegate d;
if (m_events.TryGetValue(eventKey, out d))
{
d = Delegate.Remove(d, handler);
// 如果還有委托,就設(shè)置新的地址,否則刪除EventKey
if (d != null) m_events[eventKey] = d;
else m_events.Remove(eventKey);
}
Monitor.Exit(m_events);
}
// 為指定的EventKey引發(fā)事件
public void Raise(EventKey eventKey, Object sender, EventArgs e)
{
// 如果EventKey不在集合中,不拋出一個(gè)異常
Delegate d;
Monitor.Enter(m_events);
m_events.TryGetValue(eventKey, out d);
Monitor.Exit(m_events);
if (d != null)
{
// 由于字典可能包含幾個(gè)不同的委托類型,
// 所以無(wú)法在編譯時(shí)構(gòu)造一個(gè)類型安全的委托調(diào)用。
// 因此,我調(diào)用System.Delegate類型的DynamicInvoke方法
// 以一個(gè)對(duì)象數(shù)組的形式向它傳遞回調(diào)方法的參數(shù)。
// 在內(nèi)部,DynamicInvoke會(huì)向調(diào)用的回調(diào)方法查證參數(shù)的類型安全性,并調(diào)用方法。
// 如果存在類型不匹配的情況,DynamicInvoke會(huì)拋出一個(gè)異常。
d.DynamicInvoke(new Object[] { sender, e });
}
}
}
使用EventSet類的方法也很簡(jiǎn)單,修改上面的設(shè)備類(Equipment)如下:
public class Equipment
{
// 設(shè)備名
private readonly string equipName;
public string EquipName { get { return equipName; } }
public Equipment(string en)
{
equipName = en;
}
private readonly EventSet m_events = new EventSet();
// 用于標(biāo)示事件類型的Key,當(dāng)有新的事件時(shí),需要再增加一個(gè)Key
private static readonly EventKey m_eventkey = new EventKey();
// 注冊(cè)/注銷事件
public event EventHandler<EquipmentEventArgs> Temperature90
{
add { m_events.Add(m_eventkey, value); }
remove { m_events.Remove(m_eventkey, value); }
}
// 定義觸發(fā)事件的方法
protected void OnTemperature90(EquipmentEventArgs e)
{
m_events.Raise(m_eventkey, this, e);
}
// 定義模擬觸發(fā)事件的方法
public void SimulateTemperature90()
{
// 事件參數(shù)的初始化
EquipmentEventArgs e = new EquipmentEventArgs(this.EquipName);
// 觸發(fā)事件
OnTemperature90(e);
}
}
其余代碼不用修改,編譯后可正常運(yùn)行。
EventSet類是針對(duì)事件很多的類來(lái)設(shè)計(jì)的,本例只有一個(gè)事件,這樣做的優(yōu)勢(shì)并不明顯。





浙公網(wǎng)安備 33010602011771號(hào)