1. 簡介
-
事件是一種類型安全的委托(具體實例說明見《精通C#》 --10.5 C#事件)
為什么這么說呢?可以類比屬性和字段,屬性是對字段的封裝,其實 事件也就是封裝了一個委托。但是你要知道:委托是一種自定義的數據類型,事件只是一種類的成員。
-
那么現在你要問:事件到底有什么具體的作用?
事件呢?就是有一段代碼(方法)觸發(raise)事件,事件被觸發后則訂閱事件的事件處理程序(event handler) 就會開始執行。
從這點看,事件類似異常,因為他們都是由對象引發的,並且可以由我們提供的代碼處理
-
訂閱一個事件的含義:是提供代碼在事件發生時執行這些代碼,這些代碼稱為事件處理程序
訂閱事件的語法如下:
事件名+=new 委托名(方法名);
事件名+=函數名注意訂閱事件的可以是委托對象,也可以直接是一個函數或是匿名函數或Lambda表達式
只要這個函數的返回值類型和簽名與事件一致即可
-
我們都知道委托是類似c語言中的函數針,是一種函數指針在面向對象中的封裝,是一種函數回調機制(回調函數就是一通過函數指針調用函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的數時,我們就說這是回調函數。);
事件是用戶與應用程序交互的基礎,它是回調機制的一種應用。
簡而言之:委托的本質是引用類型,用包裝回調函數,用於實現回調機制。事件的本質是一個類型安全的(多播)委托,事件是回調機制的一種應用。
-
事件和委托的關系
事件的委托類似屬性和字段,事件是對委托的封裝,這句話意思就是:事件是一類型安全的委托
事件比委托有更多的限制
1、事件只能同“+=”和“-=”來綁定方(在事件中這個方法叫事件的處理程序,其實這這種綁定方法就是多播委托的綁定方法
2、只能在類的內部調用(觸發)事件,但是委托就可以在類外調用函數那樣調用
2.實際案例
2.1實際案例1
代碼實際背景:
當裁判的發令槍響起,觸發事件,事件觸發執行的動作就是運動員跑起來了
裁判是發布者,觸發事件的方法是發令槍響,
運動員是事件的訂閱者,事件發生后就開跑。
class Program
{
static void Main(string[] args)
{
//實例化事件發布者
Judgment judgment = new Judgment();
//實例化事件訂閱者
RunSporters runsporter = new RunSporters();
// 訂閱者類RunSporters中的方法Run()和RunFail()訂閱事件eventRun
//通過委托訂閱
judgment.eventRun += new Judgment.delegateRun(runsporter.Run);
//通過函數訂閱
judgment.eventRun += runsporter.RunFail;
//執行引發事件的方法,這句代碼之后事件的訂閱者中的事件處理程序Run()和RunFail()開始運行
judgment.Begin();
Console.ReadKey();
}
}
//事件發布者,也稱發布器(publisher)
//發布器(publisher) 是一個包含事件和委托定義的對象。事件和委托之間的聯系也定義在這個對象中。
//發布器(publisher)類的對象調用這個事件,並通知其他的對象。
class Judgment
{
//定義一個委托(你要明白:這個委托類型就是事件處理程序的函數類型)
public delegate void delegateRun();
//定義一個事件
//事件的聲明與之前委托變量delegate1的聲明唯一的區別是多了一個event關鍵字,且委托寫“()”,事件不需要
public event delegateRun eventRun;
//引發事件的方法
//當方法Begin()被執行了,此時就會觸發事件 eventRun
public void Begin()
{
if (eventRun != null)
{
//被引發的事件
eventRun();
//☆☆☆☆☆☆☆☆☆☆☆☆
//注意這里就是體現“事件只能在類的內部調用”的地方,你在這個發布者類之外不能調用事件eventRun
}
}
}
//事件訂閱者,也稱訂閱器(subscriber)
//訂閱器(subscriber) 是一個接受事件並提供事件處理程序的對象。
//在發布器(publisher)類中的委托調用訂閱器(subscriber)類中的方法(事件處理程序)。
//一個事件可以有多個訂閱者,事件的發布者也可以是事件的訂閱者。
public class RunSporters
{
//事件處理程序
public void Run()
{
Console.WriteLine("運動員跑起來了");
}
//事件處理程序
public void RunFail()
{
Console.WriteLine("有一個起跑失敗");
}
}
2.2實際案例2--帶有參數的事件
代碼實際背景:
熱水器僅僅負責燒水,有一個溫度字段temperature ,它不能發出警也不能顯示水溫;
在水燒開時由警報器發出警報;
顯示器顯示提示和水溫。
觀察者模式:
Subject:監視對象,它往往包含着其他對象所感興趣的內容。
在本范例中,熱水器就是一個監視對象,它包含的其他對象所感興趣的內容,就是temprature字段,當這個字段的值快到100時,會不斷把數據發給監視它的對象。
Observer:監視者,它監視Subject,當Subject中的某件事發生的時候,會告知Observer,而Observer則會采取相應的行動。
在本范例中,Observer有警報器和顯示器,它們采取的行動分別是發出警報和顯示水溫。
在本例中,事情發生的順序應該是這樣的:
警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(注冊)。
熱水器知道后保留對警報器和顯示器的引用。
熱水器進行燒水這一動作,當水溫超過95度時,通過對警報器和顯示器的引用,自動調用警報器的MakeAlert()方法、顯示器的ShowMsg()方法。
//發布者--熱水器
public class Heater
{
private int temperature;
public delegate void BoilHandler(int param); //聲明委托
public event BoilHandler BoilEvent; //聲明事件
// 燒水
public void BoilWater()
{
for (int i = 0; i <= 100; i++)
{
temperature = i;
//當溫度大於95度開始觸發事件
if (temperature > 95)
{
//如果有對象注冊
if (BoilEvent != null)
{
BoilEvent(temperature); //調用所有注冊對象的方法(事件處理程序)
}
}
}
}
}
//訂閱者--顯示器
public class Display
{
//靜態方法
public static void ShowMsg(int param)
{
Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。\n", param);
}
}
// 訂閱器--- 警報器
public class Alarm
{
public void MakeAlert(int param)
{
Console.WriteLine("Alarm:嘀嘀嘀,水已經{0} 度了:", param);
}
}
class Program
{
static void Main(string[] args)
{
//注意我們不需要實例化Display類,為什么呢?因為我們只是使用它里面的一個靜態方法,直接使用類名點
Heater heater = new Heater();
Alarm alarm = new Alarm();
//注冊方法(訂閱事件)
//法1
heater.BoilEvent += new Heater.BoilHandler(alarm.MakeAlert);
//法2
heater.BoilEvent += (new Alarm()).MakeAlert; //給匿名對象注冊方法
heater.BoilEvent += Display.ShowMsg; //注冊靜態方法,靜態類中的方法調用直接使用類名點
heater.BoilWater(); //觸發事件,會自動調用注冊過對象的方法
Console.ReadKey();
}
}
3.標准事件的用法
C#的BCL(基礎類庫)中針對事件定義的時候所需要的委托有一個標准委托
public delegate void EventHandler(Object sender, EventArgs e);
其中:
-
第一個參數sender 用來保存觸發事件的對象的引用。由於是Object類型的,所以可以匹配任何類型的實例。
-
第二個參數用來保存有該事件相關的信息
3.1通過擴展EventArgs來傳遞數據
注意到Eventhandler的第二個參數是EventArgs類型的,
那么我們來看看EventArgs類
public class EventArgs
{
public static readonly EventArgs Empty;
public EventArgs();
}
EventArgs被設計為不能傳遞任何數據,如果你想要傳遞數據怎么辦?
我們可以通過擴展EventArgs來傳遞數據:也就是聲明一個EventArgs的子類,使用合適的字段來保存需要傳遞的數據
比如,,我們要傳遞一個int類型的參數:
public class ExtendEventArgs:EventArgs
{
public int Tem;
public ExtendEventArgs(int tem)
{
Tem=tem;
}
}
那么這時候我們就可以這樣聲明EventHandler委托
public delegate void EventHandler(Object sender,ExtendEventArgs e);
你要知道的是C#2.0中引入了EventHandler泛型委托:
所以你不需要因為修改參數而重新聲明EventHandler委托
你直接這樣定義事件:
public event EventHandler<ExtendEventArgs> MyEvent;
3.2代碼實例
代碼背景:
汽車銷售類CarDealer和顧客類Consumer
CarDealer提供一個新車到達出發事件,Consumer類訂閱該事件
class Program
{
static void Main(string[] args)
{
CarDealer dealer = new CarDealer();
Consumer consumer=new Consumer ("志銘");
dealer.NewCarEvent += consumer.ConsumerReply;
dealer.RaiseNewCarInfo("BMW");
Console.ReadKey();
}
//擴展EventArgs類,添加一個Car屬性用於傳遞數據
public class CarInfoEventArgs : EventArgs
{
public CarInfoEventArgs(string car)
{
this.Car = car;
}
public string Car;
}
//發布類
public class CarDealer
{
//public delegate EventHandler(object sender, CarInfoEventArgs e);//聲明委托EventHandler
//public event EventHandler NewCarEvent;//聲明事件NewCarEvent
//下面一行就可以代替上面注釋的兩行
//聲明事件NewCarIfno
public event EventHandler<CarInfoEventArgs> NewCarEvent;
//觸發事件NewCarIfno的函數
public void RaiseNewCarInfo(string car)
{
Console.WriteLine($"CarDealer :new car {car}");
if (NewCarEvent != null)
{
NewCarEvent(this, new CarInfoEventArgs(car));
//注意:事件的參數類型,聲明事件時所用的委托的參數類型和事件處理程序的參數類型三者一樣
//注意這里的參數this
//你想想為什么寫this?
//EventHandler委托的兩個參數,第一個參數就是觸發事件的事件對象
}
}
}
//訂閱類
public class Consumer
{
public string Name;
public Consumer (string name)
{
this.Name = name;
}
//事件處理程序,注意參數和事件的委托EventHandler<CarInfoEventArgs>一樣
public void ConsumerReply(object sender, CarInfoEventArgs e)
{
Console.WriteLine($"{this.Name }:car {e.Car } very good!");
}
}
}
運行結果
CarDealer:new car BMW
志銘:car BMW very good!