Delegate
delegate是C#中的一種類型,它其實是一個可以持有對某個方法的引用的類。與其它的類不一樣,delegate類可以擁有一個簽名(signature),而且它"只能持有與它的簽名相匹配的方法的引用"。它所實現的功能與C/C++中的函數指針十分類似。它容許你傳遞一個類A的方法m給另外一個類B的對象,使得類B的對象可以調用這個方法m。但與函數指針相比,delegate有許多函數委託和事件在 .Net Framework中的應用很是普遍指針不具有的優勢。首先,函數指針只能指向靜態函數,而delegate既能夠引用靜態函數,又能夠引用非靜態成員函數。在引用非靜態成員函數時,delegate不但保存了對此函數入口指針的引用,並且還保存了調用此函數的類實例的引用。其次,與函數指針相比,delegate是面向對象、類型安全、可靠的受控(managed)對象。也就是說,runtime可以保證delegate指向一個有效的方法,你無須擔憂delegate會指向無效地址或者越界地址。html
實現一個delegate是很簡單的,經過如下3個步驟便可實現一個delegate:
1. 聲明一個delegate對象,它應當與你想要傳遞的方法具備相同的參數和返回值類型。
2. 建立delegate對象,並"將你想要傳遞的函數做爲參數傳入"。
3. 在要實現異步調用的地方,經過上一步建立的對象來調用方法。程序員
using System;編程
public class MyDelegateTest
{
// 步驟1,聲明delegate對象
public delegate void MyDelegate(string name);
// 這是咱們欲傳遞的方法,它與MyDelegate具備相同的參數和返回值類型
public static void MyDelegateFunc(string name)
{
Console.WriteLine("Hello, ", name);
}
public static void Main()
{
// 步驟2,建立delegate對象(實例??)
MyDelegate md = new MyDelegate(MyDelegateTest.MyDelegateFunc);
// 步驟3,調用delegate
md("sam1111");
}
}
輸出結果是:Hello, sam1111設計模式
瞭解了delegate,下面咱們來看看,在C#中對事件是如何處理的。
C#中的事件處理其實是一種具備特殊簽名的delegate,象下面這個樣子:
public delegate void MyEventHandler(object sender, MyEventArgs e);
其中的兩個參數,sender表明事件發送者,e是事件參數類。MyEventArgs類用來包含與事件相關的數據,全部的事件參數類都必須從System.EventArgs類派生。固然,若是你的事件不含參數,那麼能夠直接用System.EventArgs類做爲參數。
就是這麼簡單,結合delegate的實現,咱們能夠將自定義事件的實現歸結爲如下幾步:
1.定義delegate對象類型,它有兩個參數,第一個參數是事件發送者對象,第二個參數是事件參數類對象。
2.定義事件參數類,此類應當從System.EventArgs類派生。若是事件不帶參數,這一步能夠省略。
3.定義"事件處理方法,它應當與delegate對象具備相同的參數和返回值類型"。
4.用event關鍵字定義事件對象,它同時也是一個delegate對象。
5.用+=操做符添加事件到事件隊列中(-=操做符可以將事件從隊列中刪除)。
6.在須要觸發事件的地方用調用delegate的方式寫事件觸發方法。通常來講,此方法應爲protected訪問限制,既不能以public方式調用,但能夠被子類繼承。名字是OnEventName。
7. 在適當的地方調用事件觸發方法觸發事件。
下面是一個簡單的例子:數組
using System;
public class EventTest
{
// 步驟1,定義delegate對象
public delegate void MyEventHandler(object sender, System.EventArgs e);
// 步驟2(定義事件參數類)省略
public class MyEventCls
{
// 步驟3,定義事件處理方法,它與delegate對象具備相同的參數和返回值類型
public void MyEventFunc(object sender, System.EventArgs e)
{
Console.WriteLine("My event is ok!");
}
}
// 步驟4,用event關鍵字定義事件對象
private event MyEventHandler myevent;
private MyEventCls myecls;
public EventTest()
{
myecls = new MyEventCls();
// 步驟5,用+=操做符將事件添加到隊列中
this.myevent += new MyEventHandler(myecls.MyEventFunc);
}
// 步驟6,以調用delegate的方式寫事件觸發函數
protected void OnMyEvent(System.EventArgs e)
{
if(myevent != null)
myevent(this, e);
}
public void RaiseEvent()
{
EventArgs e = new EventArgs();
// 步驟7,觸發事件
OnMyEvent(e);
}
public static void Main()
{
EventTest et = new EventTest();
Console.Write("Please input ''a'':");
string s = Console.ReadLine();
if(s == "a")
{
et.RaiseEvent();
}
else
{
Console.WriteLine("Error");
}
}
}
輸出結果以下,紅色爲用戶的輸入:
Please input ‘a’: a
My event is ok!安全
-----------------------------------------------app
要建立一個事件驅動的程序須要下面的步驟:異步
1. 聲明關於事件的委託;async
2. 聲明事件;異步編程
3. 編寫觸發事件的函數;
4. 建立事件處理程序;
5. 註冊事件處理程序;
6. 在適當的條件下觸發事件。
如今咱們來編寫一個自定義事件的程序。主人養了一條忠實的看門狗,晚上主人睡覺的時候,狗負責看守房子。一旦有小偷進來,狗就發出一個Alarm事件,主人接到Alarm事件後就會採起相應的行動。假設小偷於2009年元旦午夜時分到達。
//事件發送者
class Dog { //1.<strong>聲明關於事件的委託</strong>; public delegate void AlarmEventHandler(object sender, EventArgs e); //2.<strong>聲明事件</strong>; public event <strong>Alarm<span style="text-decoration: underline;">EventHandler</span></strong> Alarm; //3.<strong>編寫引起事件的函數</strong>; public void <strong><span style="text-decoration: underline;">On</span>Alarm</strong>() { if (this.Alarm != null) { Console.WriteLine("/n狗報警: 有小偷進來了,汪汪~~~~~~~"); this.Alarm(this, new EventArgs()); //發出警報 } } } //<strong>事件接收者</strong> class Host { //4.<strong>編寫事件處理程序</strong> void HostHandleAlarm(object sender, EventArgs e) { Console.WriteLine("主 人: 抓住了小偷!"); } //5.<strong>註冊事件處理程序</strong> public Host(Dog dog) { dog.Alarm += new <strong><span style="text-decoration: underline;">Dog.AlarmEventHandler</span>(<span style="text-decoration: underline;">HostHandleAlarm</span>)</strong>; } } //6.<strong>如今來觸發事件</strong> class Program { static void Main(string[] args) { Dog dog = new Dog(); Host host = new Host(dog); //當前時間,從2008年12月31日23:59:50開始計時 DateTime now = new DateTime(2008, 12, 31, 23, 59, 50); DateTime midnight = new DateTime(2009, 1, 1, 0, 0, 0); //等待午夜的到來 Console.WriteLine("時間一秒一秒地流逝... "); while (now < midnight) { Console.WriteLine("當前時間: " + now); System.Threading.Thread.Sleep(1000); //程序暫停一秒 now = now.AddSeconds(1); //時間增長一秒 } //午夜零點小偷到達,看門狗引起Alarm事件 Console.WriteLine("/n月黑風高的午夜: " + now); Console.WriteLine("小偷悄悄地摸進了主人的屋內... "); dog.OnAlarm(); } }<span lang="EN-US"> </span>
當午夜時分小偷到達時,dog調用dog.OnAlarm()函數,從而觸發Alarm事件,因而"系統"找到並執行了註冊在Alarm事件中的事件處理程序HostHandleAlarm()。
事件處理委託習慣上以EventHandler結尾,好比AlarmEventHandler。事件Alarm其實是事件處理委託AlarmEventHandler的一個實例。引起事件的代碼經常被編寫成一個函數,.NET約定這種函數的名稱爲「OnEventName」,好比OnAlarm()的函數。在Host類中,咱們定義了事件處理程序HostHandleAlarm(),並把它註冊到dog.Alarm事件中。
事件處理程序的參數應該和事件委託相同。通常狀況下,事件處理程序接受兩個參數,一個是事件的發送者sender,一個是事件參數e。事件參數用於在發送者和接收者之間傳遞信息。
.NET提供了100個事件參數類,這些都繼承於EventArgs類。通常狀況下,使用.NET自帶的類足夠了,但爲了說明原理,咱們自定義一個事件參數類。
試一試:使用事件參數
//事件參數 public <strong>class NumberOfThiefEventArgs : EventArgs</strong> { public int numberOfThief; //<strong>構造函數</strong> public NumberOfThiefEventArgs(int number) { numberOfThief = number; } } //事件發送者 class Dog { <strong>//1.聲明關於事件的委託;</strong> public delegate void AlarmEventHandler(object sender, NumberOfThiefEventArgs e); <strong> //2.聲明事件;</strong> public event AlarmEventHandler Alarm; <strong> //3.編寫引起事件的函數</strong>,注意<strong>多了個參數</strong>; public void OnAlarm(<strong>NumberOfThiefEventArgs </strong>e) { if (this.Alarm != null) { Console.WriteLine("/n狗報警: 有小偷進來了,汪汪~~~~~~~/n"); this.Alarm(this, e); } } } //事件接收者 class Host { <strong>//4.編寫事件處理程序,參數中包含着numberOfThief信息</strong> void HostHandleAlarm(object sender, NumberOfThiefEventArgs e) { if (e.numberOfThief <= 1) { Console.WriteLine("主 人: 抓住了小偷!"); } else { Console.WriteLine("主 人:打110報警,我家來了{0}個小偷!", e.numberOfThief); } } <strong>//5.註冊事件處理程序</strong> public Host(Dog dog) { dog.Alarm += new <strong><span style="text-decoration: underline;">Dog.AlarmEventHandler</span>(<span style="text-decoration: underline;">HostHandleAlarm</span>)</strong>; } } //6.如今來觸發事件 class Program { static void Main(string[] args) { Dog dog = new Dog(); Host host = new Host(dog); //當前時間,從2008年12月31日23:59:50開始計時 DateTime now = new DateTime(2008, 12, 31, 23, 59, 50); DateTime midnight = new DateTime(2009, 1, 1, 0, 0, 0); //等待午夜的到來 Console.WriteLine("時間一秒一秒地流逝... "); while (now < midnight) { Console.WriteLine("當前時間: " + now); System.Threading.Thread.Sleep(1000); //程序暫停一秒 now = now.AddSeconds(1); //時間增長一秒 } //午夜零點小偷到達,看門狗引起Alarm事件 Console.WriteLine("/n月黑風高的午夜: " + now); Console.WriteLine("小偷悄悄地摸進了主人的屋內... "); //建立事件參數 NumberOfThiefEventArgs e = new NumberOfThiefEventArgs(3); dog.OnAlarm(e); } }
運行結果以下:
在修改過的代碼中,咱們定義了一個名爲NumberOfThiefEventArgs的事件參數類,它繼承於EventArgs類。在該類中咱們聲明瞭一個名爲numberOfThief的成員變量,用來記錄來了幾個小偷。當事件發生時,狗經過事件參數傳告訴主人具體信息。
-----------------------------------------------
C# 中的委託和事件
源碼下載:http://www.tracefact.net/SourceCode/Delegates-and-Events-in-CSharp.rar
咱們先無論這個標題如何的繞口,也無論委託到底是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在屏幕上輸出一句問候的話語:
public void GreetPeople(string name) {
// 作某些額外的事情,好比初始化之類,此處略
EnglishGreeting(name);
}
public void EnglishGreeting(string name) {
Console.WriteLine("Morning, " + name);
}
暫且無論這兩個方法有沒有什麼實際意義。GreetPeople用於向某人問好,當咱們傳遞表明某人姓名的name參數,好比說「Jimmy」,進去的時候,在這個方法中,將調用EnglishGreeting方法,再次傳遞name參數,EnglishGreeting則用於向屏幕輸出 「Morning, Jimmy」。
如今假設這個程序須要進行全球化,哎呀,很差了,我是中國人,我不明白「Morning」是什麼意思,怎麼辦呢?好吧,咱們再加個中文版的問候方法:
public void ChineseGreeting(string name){
Console.WriteLine("早上好, " + name);
}
這時候,GreetPeople也須要改一改了,否則如何判斷到底用哪一個版本的Greeting問候方法合適呢?在進行這個以前,咱們最好再定義一個枚舉做爲判斷的依據:
public enum Language{
English, Chinese
}
public void GreetPeople(string name, Language lang){
//作某些額外的事情,好比初始化之類,此處略
swith(lang){
case Language.English:
EnglishGreeting(name);
break;
case Language.Chinese:
ChineseGreeting(name);
break;
}
}
OK,儘管這樣解決了問題,但我不說你們也很容易想到,這個解決方案的可擴展性不好,若是往後咱們須要再添加韓文版、日文版,就不得不反覆修改枚舉和GreetPeople()方法,以適應新的需求。
在考慮新的解決方案以前,咱們先看看 GreetPeople的方法簽名:
public void GreetPeople(string name, Language lang)
咱們僅看 string name,在這裏,string 是參數類型,name 是參數變量,當咱們賦給name字符串「jimmy」時,它就表明「jimmy」這個值;當咱們賦給它「張子陽」時,它又表明着「張子陽」這個值。而後,咱們能夠在方法體內對這個name進行其餘操做。哎,這簡直是廢話麼,剛學程序就知道了。
若是你再仔細想一想,假如GreetPeople()方法能夠接受一個參數變量,這個變量能夠表明另外一個方法,當咱們給這個變量賦值 EnglishGreeting的時候,它表明着 EnglsihGreeting() 這個方法;當咱們給它賦值ChineseGreeting 的時候,它又表明着ChineseGreeting()方法。咱們將這個參數變量命名爲 MakeGreeting,那麼不是能夠如同給name賦值時同樣,在調用 GreetPeople()方法的時候,給這個MakeGreeting 參數也賦上值麼(ChineseGreeting或者EnglsihGreeting等)?而後,咱們在方法體內,也能夠像使用別的參數同樣使用MakeGreeting。可是,因爲MakeGreeting表明着一個方法,它的使用方式應該和它被賦的方法(好比ChineseGreeting)是同樣的,好比:
MakeGreeting(name);
好了,有了思路了,咱們如今就來改改GreetPeople()方法,那麼它應該是這個樣子了:
public void GreetPeople(string name, *** MakeGreeting){
MakeGreeting(name);
}
注意到 *** ,這個位置一般放置的應該是參數的類型,但到目前爲止,咱們僅僅是想到應該有個能夠表明方法的參數,並按這個思路去改寫GreetPeople方法,如今就出現了一個大問題:這個表明着方法的MakeGreeting參數應該是什麼類型的?
NOTE:這裏已再也不須要枚舉了,由於在給MakeGreeting賦值的時候動態地決定使用哪一個方法,是ChineseGreeting仍是 EnglishGreeting,而在這個兩個方法內部,已經對使用「morning」仍是「早上好」做了區分。
聰明的你應該已經想到了,如今是委託該出場的時候了,但講述委託以前,咱們再看看MakeGreeting參數所能表明的 ChineseGreeting()和EnglishGreeting()方法的簽名:
public void EnglishGreeting(string name)
public void ChineseGreeting(string name)
如同name能夠接受String類型的「true」和「1」,但不能接受bool類型的true和int類型的1同樣。MakeGreeting的 參數類型定義 應該可以肯定 MakeGreeting能夠表明的方法種類,再進一步講,就是MakeGreeting能夠表明的方法 的 參數類型和返回類型。
因而,委託出現了:它定義了MakeGreeting參數所能表明的方法的種類,也就是MakeGreeting參數的類型。
NOTE:若是上面這句話比較繞口,我把它翻譯成這樣:string 定義了name參數所能表明的值的種類,也就是name參數的類型。
本例中委託的定義:
public delegate void GreetingDelegate(string name);
能夠與上面EnglishGreeting()方法的簽名對比一下,除了加入了delegate關鍵字之外,其他的是否是徹底同樣?
如今,讓咱們再次改動GreetPeople()方法,以下所示:
public void GreetPeople(string name, GreetingDelegate MakeGreeting){
MakeGreeting(name);
}
如你所見,委託GreetingDelegate出現的位置與 string相同,string是一個類型,那麼GreetingDelegate應該也是一個類型,或者叫類(Class)。可是委託的聲明方式和類卻徹底不一樣,這是怎麼一回事?實際上,委託在編譯的時候確實會編譯成類。由於Delegate是一個類,因此在任何能夠聲明類的地方均可以聲明委託。更多的內容將在下面講述,如今,請看看這個範例的完整代碼:
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate {
//定義委託,它定義了能夠表明的方法的類型
public delegate void GreetingDelegate(string name);
class Program {
private static void EnglishGreeting(string name) {
Console.WriteLine("Morning, " + name);
}
private static void ChineseGreeting(string name) {
Console.WriteLine("早上好, " + name);
}
//注意此方法,它接受一個GreetingDelegate類型的方法做爲參數
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
MakeGreeting(name);
}
static void Main(string[] args) {
GreetPeople("Jimmy Zhang", EnglishGreeting);
GreetPeople("張子陽", ChineseGreeting);
Console.ReadKey();
}
}
}
輸出以下:
Morning, Jimmy Zhang
早上好, 張子陽
咱們如今對委託作一個總結:
委託是一個類,它定義了方法的類型,使得能夠將方法看成另外一個方法的參數來進行傳遞,這種將方法動態地賦給參數的作法,能夠避免在程序中大量使用If-Else(Switch)語句,同時使得程序具備更好的可擴展性。
看到這裏,是否是有那麼點如夢初醒的感受?因而,你是否是在想:在上面的例子中,我不必定要直接在GreetPeople()方法中給 name參數賦值,我能夠像這樣使用變量:
static void Main(string[] args) {
string name1, name2;
name1 = "Jimmy Zhang";
name2 = "張子陽";
GreetPeople(name1, EnglishGreeting);
GreetPeople(name2, ChineseGreeting);
Console.ReadKey();
}
而既然委託GreetingDelegate 和 類型 string 的地位同樣,都是定義了一種參數類型,那麼,我是否是也能夠這麼使用委託?
static void Main(string[] args) {
GreetingDelegate delegate1, delegate2;
delegate1 = EnglishGreeting;
delegate2 = ChineseGreeting;
GreetPeople("Jimmy Zhang", delegate1);
GreetPeople("張子陽", delegate2);
Console.ReadKey();
}
如你所料,這樣是沒有問題的,程序一如預料的那樣輸出。這裏,我想說的是委託不一樣於string的一個特性:能夠將多個方法賦給同一個委託,或者叫將多個方法綁定到同一個委託,當調用這個委託的時候,將依次調用其所綁定的方法。在這個例子中,語法以下:
static void Main(string[] args) {
GreetingDelegate delegate1;
delegate1 = EnglishGreeting; // 先給委託類型的變量賦值
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法
// 將前後調用 EnglishGreeting 與 ChineseGreeting 方法
GreetPeople("Jimmy Zhang", delegate1);
Console.ReadKey();
}
輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
實際上,咱們能夠也能夠繞過GreetPeople方法,經過委託來直接調用EnglishGreeting和ChineseGreeting:
static void Main(string[] args) {
GreetingDelegate delegate1;
delegate1 = EnglishGreeting; // 先給委託類型的變量賦值
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法
// 將前後調用 EnglishGreeting 與 ChineseGreeting 方法
delegate1 ("Jimmy Zhang");
Console.ReadKey();
}
NOTE:這在本例中是沒有問題的,但回頭看下上面GreetPeople()的定義,在它之中能夠作一些對於EnglshihGreeting和ChineseGreeting來講都須要進行的工做,爲了簡便我作了省略。
注意這裏,第一次用的「=」,是賦值的語法;第二次,用的是「+=」,是綁定的語法。若是第一次就使用「+=」,將出現「使用了未賦值的局部變量」的編譯錯誤。
咱們也可使用下面的代碼來這樣簡化這一過程:
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法
看到這裏,應該注意到,這段代碼第一條語句與實例化一個類是何其的類似,你不由想到:上面第一次綁定委託時不可使用「+=」的編譯錯誤,或許能夠用這樣的方法來避免:
GreetingDelegate delegate1 = new GreetingDelegate();
delegate1 += EnglishGreeting; // 此次用的是 「+=」,綁定語法。
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法
但實際上,這樣會出現編譯錯誤: 「GreetingDelegate」方法沒有采用「0」個參數的重載。儘管這樣的結果讓咱們以爲有點沮喪,可是編譯的提示:「沒有0個參數的重載」再次讓咱們聯想到了類的構造函數。我知道你必定按捺不住想探個究竟,但再此以前,咱們須要先把基礎知識和應用介紹完。
既然給委託能夠綁定一個方法,那麼也應該有辦法取消對方法的綁定,很容易想到,這個語法是「-=」:
static void Main(string[] args) {
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
delegate1 += ChineseGreeting; // 給此委託變量再綁定一個方法
// 將前後調用 EnglishGreeting 與 ChineseGreeting 方法
GreetPeople("Jimmy Zhang", delegate1);
Console.WriteLine();
delegate1 -= EnglishGreeting; //取消對EnglishGreeting方法的綁定
// 將僅調用 ChineseGreeting
GreetPeople("張子陽", delegate1);
Console.ReadKey();
}
輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
早上好, 張子陽
讓咱們再次對委託做個總結:
使用委託能夠將多個方法綁定到同一個委託變量,當調用此變量時(這裏用「調用」這個詞,是由於此變量表明一個方法),能夠依次調用全部綁定的方法。
咱們繼續思考上面的程序:上面的三個方法都定義在Programe類中,這樣作是爲了理解的方便,實際應用中,一般都是 GreetPeople 在一個類中,ChineseGreeting和 EnglishGreeting 在另外的類中。如今你已經對委託有了初步瞭解,是時候對上面的例子作個改進了。假設咱們將GreetingPeople()放在一個叫GreetingManager的類中,那麼新程序應該是這個樣子的:
namespace Delegate {
//定義委託,它定義了能夠表明的方法的類型
public delegate void GreetingDelegate(string name);
//新建的GreetingManager類
public class GreetingManager{
public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
MakeGreeting(name);
}
}
class Program {
private static void EnglishGreeting(string name) {
Console.WriteLine("Morning, " + name);
}
private static void ChineseGreeting(string name) {
Console.WriteLine("早上好, " + name);
}
static void Main(string[] args) {
// ... ...
}
}
}
這個時候,若是要實現前面演示的輸出效果,Main方法我想應該是這樣的:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
gm.GreetPeople("Jimmy Zhang", EnglishGreeting);
gm.GreetPeople("張子陽", ChineseGreeting);
}
咱們運行這段代碼,嗯,沒有任何問題。程序一如預料地那樣輸出了:
Morning, Jimmy Zhang
早上好, 張子陽
如今,假設咱們須要使用上一節學到的知識,將多個方法綁定到同一個委託變量,該如何作呢?讓咱們再次改寫代碼:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
GreetingDelegate delegate1;
delegate1 = EnglishGreeting;
delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy Zhang", delegate1);
}
輸出:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
到了這裏,咱們不由想到:面向對象設計,講究的是對象的封裝,既然能夠聲明委託類型的變量(在上例中是delegate1),咱們何不將這個變量封裝到 GreetManager類中?在這個類的客戶端中使用不是更方便麼?因而,咱們改寫GreetManager類,像這樣:
public class GreetingManager{
//在GreetingManager類的內部聲明delegate1變量
public GreetingDelegate delegate1;
public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
MakeGreeting(name);
}
}
如今,咱們能夠這樣使用這個委託變量:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy Zhang", gm.delegate1);
}
輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
儘管這樣作沒有任何問題,但咱們發現這條語句很奇怪。在調用gm.GreetPeople方法的時候,再次傳遞了gm的delegate1字段:
gm.GreetPeople("Jimmy Zhang", gm.delegate1);
既然如此,咱們何不修改 GreetingManager 類成這樣:
public class GreetingManager{
//在GreetingManager類的內部聲明delegate1變量
public GreetingDelegate delegate1;
public void GreetPeople(string name) {
if(delegate1!=null){ //若是有方法註冊委託變量
delegate1(name); //經過委託調用方法
}
}
}
在客戶端,調用看上去更簡潔一些:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy Zhang"); //注意,此次不須要再傳遞 delegate1變量
}
輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
儘管這樣達到了咱們要的效果,可是仍是存在着問題:
在這裏,delegate1和咱們平時用的string類型的變量沒有什麼分別,而咱們知道,並非全部的字段都應該聲明成public,合適的作法是應該public的時候public,應該private的時候private。
咱們先看看若是把 delegate1 聲明爲 private會怎樣?結果就是:這簡直就是在搞笑。由於聲明委託的目的就是爲了把它暴露在類的客戶端進行方法的註冊,你把它聲明爲private了,客戶端對它根本就不可見,那它還有什麼用?
再看看把delegate1 聲明爲 public 會怎樣?結果就是:在客戶端能夠對它進行隨意的賦值等操做,嚴重破壞對象的封裝性。
最後,第一個方法註冊用「=」,是賦值語法,由於要進行實例化,第二個方法註冊則用的是「+=」。可是,無論是賦值仍是註冊,都是將方法綁定到委託上,除了調用時前後順序不一樣,再沒有任何的分別,這樣不是讓人以爲很彆扭麼?
如今咱們想一想,若是delegate1不是一個委託類型,而是一個string類型,你會怎麼作?答案是使用屬性對字段進行封裝。
因而,Event出場了,它封裝了委託類型的變量,使得:在類的內部,無論你聲明它是public仍是protected,它老是private的。在類的外部,註冊「+=」和註銷「-=」的訪問限定符與你在聲明事件時使用的訪問符相同。
咱們改寫GreetingManager類,它變成了這個樣子:
public class GreetingManager{
//這一次咱們在這裏聲明一個事件
public event GreetingDelegate MakeGreet;
public void GreetPeople(string name) {
MakeGreet(name);
}
}
很容易注意到:MakeGreet 事件的聲明與以前委託變量delegate1的聲明惟一的區別是多了一個event關鍵字。看到這裏,在結合上面的講解,你應該明白到:事件其實沒什麼很差理解的,聲明一個事件不過相似於聲明一個進行了封裝的委託類型的變量而已。
爲了證實上面的推論,若是咱們像下面這樣改寫Main方法:
static void Main(string[] args) {
GreetingManager gm = new GreetingManager();
gm.MakeGreet = EnglishGreeting; // 編譯錯誤1
gm.MakeGreet += ChineseGreeting;
gm.GreetPeople("Jimmy Zhang");
}
會獲得編譯錯誤:事件「Delegate.GreetingManager.MakeGreet」只能出如今 += 或 -= 的左邊(從類型「Delegate.GreetingManager」中使用時除外)。
這時候,咱們註釋掉編譯錯誤的行,而後從新進行編譯,再借助Reflactor來對 event的聲明語句作一探究,看看爲何會發生這樣的錯誤:
public event GreetingDelegate MakeGreet;
能夠看到,實際上儘管咱們在GreetingManager裏將 MakeGreet 聲明爲public,可是,實際上MakeGreet會被編譯成 私有字段,難怪會發生上面的編譯錯誤了,由於它根本就不容許在GreetingManager類的外面以賦值的方式訪問,從而驗證了咱們上面所作的推論。
咱們再進一步看下MakeGreet所產生的代碼:
private GreetingDelegate MakeGreet; //對事件的聲明 實際是 聲明一個私有的委託變量
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_MakeGreet(GreetingDelegate value){
this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_MakeGreet(GreetingDelegate value){
this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
}
如今已經很明確了:MakeGreet事件確實是一個GreetingDelegate類型的委託,只不過無論是否是聲明爲public,它老是被聲明爲private。另外,它還有兩個方法,分別是add_MakeGreet和remove_MakeGreet,這兩個方法分別用於註冊委託類型的方法和取消註冊。實際上也就是: 「+= 」對應 add_MakeGreet,「-=」對應remove_MakeGreet。而這兩個方法的訪問限制取決於聲明事件時的訪問限制符。
在add_MakeGreet()方法內部,實際上調用了System.Delegate的Combine()靜態方法,這個方法用於將當前的變量添加到委託鏈表中。咱們前面提到過兩次,說委託其實是一個類,在咱們定義委託的時候:
public delegate void GreetingDelegate(string name);
當編譯器遇到這段代碼的時候,會生成下面這樣一個完整的類:
public sealed class GreetingDelegate:System.MulticastDelegate{
public GreetingDelegate(object @object, IntPtr method);
public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
public virtual void EndInvoke(IAsyncResult result);
public virtual void Invoke(string name);
}
關於這個類的更深刻內容,能夠參閱《CLR Via C#》等相關書籍,這裏就再也不討論了。
上面的例子已不足以再進行下面的講解了,咱們來看一個新的範例,由於以前已經介紹了不少的內容,因此本節的進度會稍微快一些:
假設咱們有個高檔的熱水器,咱們給它通上電,當水溫超過95度的時候:一、揚聲器會開始發出語音,告訴你水的溫度;二、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。
如今咱們須要寫個程序來模擬這個燒水的過程,咱們將定義一個類來表明熱水器,咱們管它叫:Heater,它有表明水溫的字段,叫作temperature;固然,還有必不可少的給水加熱方法BoilWater(),一個發出語音警報的方法MakeAlert(),一個顯示水溫的方法,ShowMsg()。
namespace Delegate {
class Heater {
private int temperature; // 水溫
// 燒水
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
MakeAlert(temperature);
ShowMsg(temperature);
}
}
}
// 發出語音警報
private void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
}
// 顯示水溫
private void ShowMsg(int param) {
Console.WriteLine("Display:水快開了,當前溫度:{0}度。" , param);
}
}
class Program {
static void Main() {
Heater ht = new Heater();
ht.BoilWater();
}
}
}
上面的例子顯然能完成咱們以前描述的工做,可是卻並不夠好。如今假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不一樣廠商並進行了組裝。那麼,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。
這時候,上面的例子就應該變成這個樣子:
// 熱水器
public class Heater {
private int temperature;
// 燒水
private void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
}
}
}
// 警報器
public class Alarm{
private void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
}
}
// 顯示器
public class Display{
private void ShowMsg(int param) {
Console.WriteLine("Display:水已燒開,當前溫度:{0}度。" , param);
}
}
這裏就出現了一個問題:如何在水燒開的時候通知報警器和顯示器?在繼續進行以前,咱們先了解一下Observer設計模式,Observer設計模式中主要包括以下兩類對象:
在本例中,事情發生的順序應該是這樣的:
相似這樣的例子是不少的,GOF對它進行了抽象,稱爲Observer設計模式:Observer設計模式是爲了定義對象間的一種一對多的依賴關係,以便於當一個對象的狀態改變時,其餘依賴於它的對象會被自動告知並更新。Observer模式是一種鬆耦合的設計模式。
咱們以前已經對委託和事件介紹不少了,如今寫代碼應該很容易了,如今在這裏直接給出代碼,並在註釋中加以說明。
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate {
// 熱水器
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;
if (temperature > 95) {
if (BoilEvent != null) { //若是有對象註冊
BoilEvent(temperature); //調用全部註冊對象的方法
}
}
}
}
}
// 警報器
public class Alarm {
public void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
}
}
// 顯示器
public class Display {
public static void ShowMsg(int param) { //靜態方法
Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", param);
}
}
class Program {
static void Main() {
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.BoilEvent += alarm.MakeAlert; //註冊方法
heater.BoilEvent += (new Alarm()).MakeAlert; //給匿名對象註冊方法
heater.BoilEvent += Display.ShowMsg; //註冊靜態方法
heater.BoilWater(); //燒水,會自動調用註冊過對象的方法
}
}
}
輸出爲:
Alarm:嘀嘀嘀,水已經 96 度了:
Alarm:嘀嘀嘀,水已經 96 度了:
Display:水快燒開了,當前溫度:96度。
// 省略...
儘管上面的範例很好地完成了咱們想要完成的工做,可是咱們不只疑惑:爲何.Net Framework 中的事件模型和上面的不一樣?爲何有不少的EventArgs參數?
在回答上面的問題以前,咱們先搞懂 .Net Framework的編碼規範:
再作一下說明:
上面這些其實不只僅是爲了編碼規範而已,這樣也使得程序有更大的靈活性。好比說,若是咱們不光想得到熱水器的溫度,還想在Observer端(警報器或者顯示器)方法中得到它的生產日期、型號、價格,那麼委託和方法的聲明都會變得很麻煩,而若是咱們將熱水器的引用傳給警報器的方法,就能夠在方法中直接訪問熱水器了。
如今咱們改寫以前的範例,讓它符合 .Net Framework 的規範:
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate {
// 熱水器
public class Heater {
private int temperature;
public string type = "RealFire 001"; // 添加型號做爲演示
public string area = "China Xian"; // 添加產地做爲演示
//聲明委託
public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
public event BoiledEventHandler Boiled; //聲明事件
// 定義BoiledEventArgs類,傳遞給Observer所感興趣的信息
public class BoiledEventArgs : EventArgs {
public readonly int temperature;
public BoiledEventArgs(int temperature) {
this.temperature = temperature;
}
}
// 能夠供繼承自 Heater 的類重寫,以便繼承類拒絕其餘對象對它的監視
protected virtual void OnBoiled(BoiledEventArgs e) {
if (Boiled != null) { // 若是有對象註冊
Boiled(this, e); // 調用全部註冊對象的方法
}
}
// 燒水。
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
//創建BoiledEventArgs 對象。
BoiledEventArgs e = new BoiledEventArgs(temperature);
OnBoiled(e); // 調用 OnBolied方法
}
}
}
}
// 警報器
public class Alarm {
public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {
Heater heater = (Heater)sender; //這裏是否是很熟悉呢?
//訪問 sender 中的公共字段
Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
Console.WriteLine("Alarm: 嘀嘀嘀,水已經 {0} 度了:", e.temperature);
Console.WriteLine();
}
}
// 顯示器
public class Display {
public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) { //靜態方法
Heater heater = (Heater)sender;
Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", e.temperature);
Console.WriteLine();
}
}
class Program {
static void Main() {
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.Boiled += alarm.MakeAlert; //註冊方法
heater.Boiled += (new Alarm()).MakeAlert; //給匿名對象註冊方法
heater.Boiled += newHeater.BoiledEventHandler(alarm.MakeAlert); //也能夠這麼註冊
heater.Boiled += Display.ShowMsg; //註冊靜態方法
heater.BoilWater(); //燒水,會自動調用註冊過對象的方法
}
}
}
輸出爲:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Display:China Xian - RealFire 001:
Display:水快燒開了,當前溫度:96度。
// 省略 ...
引言
若是你看過了 C#中的委託和事件 一文,我想你對委託和事件已經有了一個基本的認識。但那些遠不是委託和事件的所有內容,還有不少的地方沒有涉及。本文將討論委託和事件一些更爲細節的問題,包括一些你們常問到的問題,以及事件訪問器、異常處理、超時處理和異步方法調用等內容。
在 C#中的委託和事件 中,我提出了兩個爲何在類型中使用事件向外部提供方法註冊,而不是直接使用委託變量的緣由。主要是從封裝性和易用性上去考慮,可是還漏掉了一點,事件應該由事件發佈者觸發,而不該該由客戶端(客戶程序)來觸發。這句話是什麼意思呢?請看下面的範例:
NOTE:注意這裏術語的變化,當咱們單獨談論事件,咱們說發佈者(publisher)、訂閱者(subscriber)、客戶端(client)。當咱們討論Observer模式,咱們說主題(subject)和觀察者(observer)。客戶端一般是包含Main()方法的Program類。
class Program {
static void Main(string[] args) {
Publishser pub = new Publishser();
Subscriber sub = new Subscriber();
pub.NumberChanged += newNumberChangedEventHandler(sub.OnNumberChanged);
pub.DoSomething(); // 應該經過DoSomething()來觸發事件
pub.NumberChanged(100); // 但能夠被這樣直接調用,對委託變量的不恰當使用
}
}
// 定義委託
public delegate void NumberChangedEventHandler(int count);
// 定義事件發佈者
public class Publishser {
private int count;
public NumberChangedEventHandler NumberChanged; // 聲明委託變量
//public event NumberChangedEventHandler NumberChanged; // 聲明一個事件
public void DoSomething() {
// 在這裏完成一些工做 ...
if (NumberChanged != null) { // 觸發事件
count++;
NumberChanged(count);
}
}
}
// 定義事件訂閱者
public class Subscriber {
public void OnNumberChanged(int count) {
Console.WriteLine("Subscriber notified: count = {0}", count);
}
}
上面代碼定義了一個NumberChangedEventHandler委託,而後咱們建立了事件的發佈者Publisher和訂閱者Subscriber。當使用委託變量時,客戶端能夠直接經過委託變量觸發事件,也就是直接調用pub.NumberChanged(100),這將會影響到全部註冊了該委託的訂閱者。而事件的本意應該爲在事件發佈者在其自己的某個行爲中觸發,好比說在方法DoSomething()中知足某個條件後觸發。經過添加event關鍵字來發布事件,事件發佈者的封裝性會更好,事件僅僅是供其餘類型訂閱,而客戶端不能直接觸發事件(語句pub.NumberChanged(100)沒法經過編譯),事件只能在事件發佈者Publisher類的內部觸發(好比在方法pub.DoSomething()中),換言之,就是NumberChanged(100)語句只能在Publisher內部被調用。
你們能夠嘗試一下,將委託變量的聲明那行代碼註釋掉,而後取消下面事件聲明的註釋。此時程序是沒法編譯的,當你使用了event關鍵字以後,直接在客戶端觸發事件這種行爲,也就是直接調用pub.NumberChanged(100),是被禁止的。事件只能經過調用DoSomething()來觸發。這樣纔是事件的本意,事件發佈者的封裝纔會更好。
就好像若是咱們要定義一個數字類型,咱們會使用int而不是使用object同樣,給予對象過多的能力並不見得是一件好事,應該是越合適越好。儘管直接使用委託變量一般不會有什麼問題,但它給了客戶端不該具備的能力,而使用事件,能夠限制這一能力,更精確地對類型進行封裝。
NOTE:這裏還有一個約定俗稱的規定,就是訂閱事件的方法的命名,一般爲「On事件名」,好比這裏的OnNumberChanged。
儘管並不是必需,可是咱們發現不少的委託定義返回值都爲void,爲何呢?這是由於委託變量能夠供多個訂閱者註冊,若是定義了返回值,那麼多個訂閱者的方法都會向發佈者返回數值,結果就是後面一個返回的方法值將前面的返回值覆蓋掉了,所以,實際上只能得到最後一個方法調用的返回值。能夠運行下面的代碼測試一下。除此之外,發佈者和訂閱者是鬆耦合的,發佈者根本不關心誰訂閱了它的事件、爲何要訂閱,更別說訂閱者的返回值了,因此返回訂閱者的方法返回值大多數狀況下根本沒有必要。
class Program {
static void Main(string[] args) {
Publishser pub = new Publishser();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.NumberChanged += newGeneralEventHandler(sub1.OnNumberChanged);
pub.NumberChanged += newGeneralEventHandler(sub2.OnNumberChanged);
pub.NumberChanged += newGeneralEventHandler(sub3.OnNumberChanged);
pub.DoSomething(); // 觸發事件
}
}
// 定義委託
public delegate string GeneralEventHandler();
// 定義事件發佈者
public class Publishser {
public event GeneralEventHandler NumberChanged; // 聲明一個事件
public void DoSomething() {
if (NumberChanged != null) { // 觸發事件
string rtn = NumberChanged();
Console.WriteLine(rtn); // 打印返回的字符串,輸出爲Subscriber3
}
}
}
// 定義事件訂閱者
public class Subscriber1 {
public string OnNumberChanged() {
return "Subscriber1";
}
}
public class Subscriber2 { /* 略,與上相似,返回Subscriber2*/ }
public class Subscriber3 { /* 略,與上相似,返回Subscriber3*/ }
若是運行這段代碼,獲得的輸出是Subscriber3,能夠看到,只獲得了最後一個註冊方法的返回值。
少數狀況下,好比像上面,爲了不發生「值覆蓋」的狀況(更可能是在異步調用方法時,後面會討論),咱們可能想限制只容許一個客戶端註冊。此時怎麼作呢?咱們能夠向下面這樣,將事件聲明爲private的,而後提供兩個方法來進行註冊和取消註冊:
// 定義事件發佈者
public class Publishser {
private event GeneralEventHandler NumberChanged; // 聲明一個私有事件
// 註冊事件
public void Register(GeneralEventHandler method) {
NumberChanged = method;
}
// 取消註冊
public void UnRegister(GeneralEventHandler method) {
NumberChanged -= method;
}
public void DoSomething() {
// 作某些其他的事情
if (NumberChanged != null) { // 觸發事件
string rtn = NumberChanged();
Console.WriteLine("Return: {0}", rtn); // 打印返回的字符串,輸出爲Subscriber3
}
}
}
NOTE:注意上面,在UnRegister()中,沒有進行任何判斷就使用了NumberChanged-=method語句。這是由於即便method方法沒有進行過註冊,此行語句也不會有任何問題,不會拋出異常,僅僅是不會產生任何效果而已。
注意在Register()方法中,咱們使用了賦值操做符「=」,而非「+=」,經過這種方式就避免了多個方法註冊。上面的代碼儘管能夠完成咱們的須要,可是此時你們還應該注意下面兩點:
一、將NumberChanged聲明爲委託變量仍是事件都無所謂了,由於它是私有的,即使將它聲明爲一個委託變量,客戶端也看不到它,也就沒法經過它來觸發事件、調用訂閱者的方法。而只能經過Register()和UnRegister()方法來註冊和取消註冊,經過調用DoSomething()方法觸發事件(而不是NumberChanged自己,這在前面已經討論過了)。
二、咱們還應該發現,這裏採用的、對NumberChanged委託變量的訪問模式和C#中的屬性是多麼相似啊?你們知道,在C#中一般一個屬性對應一個類型成員,而在類型的外部對成員的操做所有經過屬性來完成。儘管這裏對委託變量的處理是相似的效果,但卻使用了兩個方法來進行模擬,有沒有辦法像使用屬性同樣來完成上面的例子呢?答案是有的,C#中提供了一種叫事件訪問器(Event Accessor)的東西,它用來封裝委託變量。以下面例子所示:
class Program {
static void Main(string[] args) {
Publishser pub = new Publishser();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
pub.NumberChanged -= sub1.OnNumberChanged; // 不會有任何反應
pub.NumberChanged += sub2.OnNumberChanged; // 註冊了sub2
pub.NumberChanged += sub1.OnNumberChanged; // sub1將sub2的覆蓋掉了
pub.DoSomething(); // 觸發事件
}
}
// 定義委託
public delegate string GeneralEventHandler();
// 定義事件發佈者
public class Publishser {
// 聲明一個委託變量
private GeneralEventHandler numberChanged;
// 事件訪問器的定義
public event GeneralEventHandler NumberChanged {
add {
numberChanged = value;
}
remove {
numberChanged -= value;
}
}
public void DoSomething() {
// 作某些其餘的事情
if (numberChanged != null) { // 經過委託變量觸發事件
string rtn = numberChanged();
Console.WriteLine("Return: {0}", rtn); // 打印返回的字符串
}
}
}
// 定義事件訂閱者
public class Subscriber1 {
public string OnNumberChanged() {
Console.WriteLine("Subscriber1 Invoked!");
return "Subscriber1";
}
}
public class Subscriber2 {/* 與上類同,略 */}
public class Subscriber3 {/* 與上類同,略 */}
上面代碼中相似屬性的public event GeneralEventHandler NumberChanged {add{...}remove{...}}語句即是事件訪問器。使用了事件訪問器之後,在DoSomething方法中便只能經過numberChanged委託變量來觸發事件,而不能NumberChanged事件訪問器(注意它們的大小寫不一樣)觸發,它只用於註冊和取消註冊。下面是代碼輸出:
Subscriber1 Invoked!
Return: Subscriber1
如今假設咱們想要得到多個訂閱者的返回值,以List<string>的形式返回,該如何作呢?咱們應該記得委託定義在編譯時會生成一個繼承自MulticastDelegate的類,而這個MulticastDelegate又繼承自Delegate,在Delegate內部,維護了一個委託鏈表,鏈表上的每個元素,爲一個只包含一個目標方法的委託對象。而經過Delegate基類的GetInvocationList()靜態方法,能夠得到這個委託鏈表。隨後咱們遍歷這個鏈表,經過鏈表中的每一個委託對象來調用方法,這樣就能夠分別得到每一個方法的返回值:
class Program4 {
static void Main(string[] args) {
Publishser pub = new Publishser();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged);
pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged);
pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged);
List<string> list = pub.DoSomething(); //調用方法,在方法內觸發事件
foreach (string str in list) {
Console.WriteLine(str);
}
}
}
public delegate string DemoEventHandler(int num);
// 定義事件發佈者
public class Publishser {
public event DemoEventHandler NumberChanged; // 聲明一個事件
public List<string> DoSomething() {
// 作某些其餘的事
List<string> strList = new List<string>();
if (NumberChanged == null) return strList;
// 得到委託數組
Delegate[] delArray = NumberChanged.GetInvocationList();
foreach (Delegate del in delArray) {
// 進行一個向下轉換
DemoEventHandler method = (DemoEventHandler)del;
strList.Add(method(100)); // 調用方法並獲取返回值
}
return strList;
}
}
// 定義事件訂閱者
public class Subscriber1 {
public string OnNumberChanged(int num) {
Console.WriteLine("Subscriber1 invoked, number:{0}", num);
return "[Subscriber1 returned]";
}
}
public class Subscriber3 {與上面類同,略}
public class Subscriber3 {與上面類同,略}
若是運行上面的代碼,能夠獲得這樣的輸出:
Subscriber1 invoked, number:100
Subscriber2 invoked, number:100
Subscriber3 invoked, number:100
[Subscriber1 returned]
[Subscriber2 returned]
[Subscriber3 returned]
可見咱們得到了三個方法的返回值。而咱們前面說過,不少狀況下委託的定義都不包含返回值,因此上面介紹的方法彷佛沒有什麼實際意義。其實經過這種方式來觸發事件最多見的狀況應該是在異常處理中,由於頗有可能在觸發事件時,訂閱者的方法會拋出異常,而這一異常會直接影響到發佈者,使得發佈者程序停止,然後面訂閱者的方法將不會被執行。所以咱們須要加上異常處理,考慮下面一段程序:
class Program5 {
static void Main(string[] args) {
Publisher pub = new Publisher();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged);
pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged);
pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged);
}
}
public class Publisher {
public event EventHandler MyEvent;
public void DoSomething() {
// 作某些其餘的事情
if (MyEvent != null) {
try {
MyEvent(this, EventArgs.Empty);
} catch (Exception e) {
Console.WriteLine("Exception: {0}", e.Message);
}
}
}
}
public class Subscriber1 {
public void OnEvent(object sender, EventArgs e) {
Console.WriteLine("Subscriber1 Invoked!");
}
}
public class Subscriber2 {
public void OnEvent(object sender, EventArgs e) {
throw new Exception("Subscriber2 Failed");
}
}
public class Subscriber3 {/* 與Subsciber1類同,略*/}
注意到咱們在Subscriber2中拋出了異常,同時咱們在Publisher中使用了try/catch語句來處理異常。運行上面的代碼,咱們獲得的結果是:
Subscriber1 Invoked!
Exception: Subscriber2 Failed
能夠看到,儘管咱們捕獲了異常,使得程序沒有異常結束,可是卻影響到了後面的訂閱者,由於Subscriber3也訂閱了事件,可是卻沒有收到事件通知(它的方法沒有被調用)。此時,咱們能夠採用上面的辦法,先得到委託鏈表,而後在遍歷鏈表的循環中處理異常,咱們只須要修改一下DoSomething方法就能夠了:
public void DoSomething() {
if (MyEvent != null) {
Delegate[] delArray = MyEvent.GetInvocationList();
foreach (Delegate del in delArray) {
EventHandler method = (EventHandler)del; // 強制轉換爲具體的委託類型
try {
method(this, EventArgs.Empty);
} catch (Exception e) {
Console.WriteLine("Exception: {0}", e.Message);
}
}
}
}
注意到Delegate是EventHandler的基類,因此爲了觸發事件,先要進行一個向下的強制轉換,以後才能在其上觸發事件,調用全部註冊對象的方法。除了使用這種方式之外,還有一種更靈活方式能夠調用方法,它是定義在Delegate基類中的DynamicInvoke()方法:
public object DynamicInvoke(params object[] args);
這多是調用委託最通用的方法了,適用於全部類型的委託。它接受的參數爲object[],也就是說它能夠將任意數量的任意類型做爲參數,並返回單個object對象。上面的DoSomething()方法也能夠改寫成下面這種通用形式:
public void DoSomething() {
// 作某些其餘的事情
if (MyEvent != null) {
Delegate[] delArray = MyEvent.GetInvocationList();
foreach (Delegate del in delArray) {
try {
// 使用DynamicInvoke方法觸發事件
del.DynamicInvoke(this, EventArgs.Empty);
} catch (Exception e) {
Console.WriteLine("Exception: {0}", e.Message);
}
}
}
}
注意如今在DoSomething()方法中,咱們取消了向具體委託類型的向下轉換,如今沒有了任何的基於特定委託類型的代碼,而DynamicInvoke又能夠接受任何類型的參數,且返回一個object對象。因此咱們徹底能夠將DoSomething()方法抽象出來,使它成爲一個公共方法,而後供其餘類來調用,咱們將這個方法聲明爲靜態的,而後定義在Program類中:
// 觸發某個事件,以列表形式返回全部方法的返回值
public static object[] FireEvent(Delegate del, params object[] args){
List<object> objList = new List<object>();
if (del != null) {
Delegate[] delArray = del.GetInvocationList();
foreach (Delegate method in delArray) {
try {
// 使用DynamicInvoke方法觸發事件
object obj = method.DynamicInvoke(args);
if (obj != null)
objList.Add(obj);
} catch { }
}
}
return objList.ToArray();
}
隨後,咱們在DoSomething()中只要簡單的調用一下這個方法就能夠了:
public void DoSomething() {
// 作某些其餘的事情
Program5.FireEvent(MyEvent, this, EventArgs.Empty);
}
注意FireEvent()方法還能夠返回一個object[]數組,這個數組包括了全部訂閱者方法的返回值。而在上面的例子中,我沒有演示如何獲取並使用這個數組,爲了節省篇幅,這裏也再也不贅述了,在本文附帶的代碼中,有關於這部分的演示,有興趣的朋友能夠下載下來看看。
訂閱者除了能夠經過異常的方式來影響發佈者之外,還能夠經過另外一種方式:超時。通常說超時,指的是方法的執行超過某個指定的時間,而這裏我將含義擴展了一下,凡是方法執行的時間比較長,我就認爲它超時了,這個「比較長」是一個比較模糊的概念,2秒、3秒、5秒均可以視爲超時。超時和異常的區別就是超時並不會影響事件的正確觸發和程序的正常運行,卻會致使事件觸發後須要很長才可以結束。在依次執行訂閱者的方法這段期間內,客戶端程序會被中斷,什麼也不能作。由於當執行訂閱者方法時(經過委託,至關於依次調用全部註冊了的方法),當前線程會轉去執行方法中的代碼,調用方法的客戶端會被中斷,只有當方法執行完畢並返回時,控制權纔會回到客戶端,從而繼續執行下面的代碼。咱們來看一下下面一個例子:
class Program6 {
static void Main(string[] args) {
Publisher pub = new Publisher();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.MyEvent += new EventHandler(sub1.OnEvent);
pub.MyEvent += new EventHandler(sub2.OnEvent);
pub.MyEvent += new EventHandler(sub3.OnEvent);
pub.DoSomething(); // 觸發事件
Console.WriteLine(" Control back to client!"); // 返回控制權
}
// 觸發某個事件,以列表形式返回全部方法的返回值
public static object[] FireEvent(Delegate del, params object[] args) {
// 代碼與上同,略
}
}
public class Publisher {
public event EventHandler MyEvent;
public void DoSomething() {
// 作某些其餘的事情
Console.WriteLine("DoSomething invoked!");
Program6.FireEvent(MyEvent, this, EventArgs.Empty); //觸發事件
}
}
public class Subscriber1 {
public void OnEvent(object sender, EventArgs e) {
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!");
}
}
public class Subscriber2 {
public void OnEvent(object sender, EventArgs e) {
Console.WriteLine("Subscriber2 immediately Invoked!");
}
}
public class Subscriber3 {
public void OnEvent(object sender, EventArgs e) {
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("Waited for 2 seconds, subscriber2 invoked!");
}
}
在這段代碼中,咱們使用Thread.Sleep()靜態方法模擬了方法超時的狀況。其中Subscriber1.OnEvent()須要三秒鐘完成,Subscriber2.OnEvent()當即執行,Subscriber3.OnEvent須要兩秒完成。這段代碼徹底能夠正常輸出,也沒有異常拋出(若是有,也僅僅是該訂閱者被忽略掉),下面是輸出的狀況:
DoSomething invoked!
Waited for 3 seconds, subscriber1 invoked!
Subscriber2 immediately Invoked!
Waited for 2 seconds, subscriber2 invoked!
Control back to client!
可是這段程序在調用方法DoSomething()、打印了「DoSomething invoked」以後,觸發了事件,隨後必須等訂閱者的三個方法所有執行完畢了以後,也就是大概5秒鐘的時間,才能繼續執行下面的語句,也就是打印「Control back to client」。而咱們前面說過,不少狀況下,尤爲是遠程調用的時候(好比說在Remoting中),發佈者和訂閱者應該是徹底的鬆耦合,發佈者不關心誰訂閱了它、不關心訂閱者的方法有什麼返回值、不關心訂閱者會不會拋出異常,固然也不關心訂閱者須要多長時間才能完成訂閱的方法,它只要在事件發生的那一瞬間告知訂閱者事件已經發生並將相關參數傳給訂閱者就能夠了。而後它就應該繼續執行它後面的動做,在本例中就是打印「Control back to client!」。而訂閱者無論失敗或是超時都不該該影響到發佈者,但在上面的例子中,發佈者卻不得不等待訂閱者的方法執行完畢才能繼續運行。
如今咱們來看下如何解決這個問題,先回顧一下以前我在C#中的委託和事件一文中提到的內容,我說過,委託的定義會生成繼承自MulticastDelegate的完整的類,其中包含Invoke()、BeginInvoke()和EndInvoke()方法。當咱們直接調用委託時,其實是調用了Invoke()方法,它會中斷調用它的客戶端,而後在客戶端線程上執行全部訂閱者的方法(客戶端沒法繼續執行後面代碼),最後將控制權返回客戶端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,異步執行的方法一般都會配對出現,而且以Begin和End做爲方法的開頭(最多見的可能就是Stream類的BeginRead()和EndRead()方法了)。它們用於方法的異步執行,便是在調用BeginInvoke()以後,客戶端從線程池中抓取一個閒置線程,而後交由這個線程去執行訂閱者的方法,而客戶端線程則能夠繼續執行下面的代碼。
BeginInvoke()接受「動態」的參數個數和類型,爲何說「動態」的呢?由於它的參數是在編譯時根據委託的定義動態生成的,其中前面參數的個數和類型與委託定義中接受的參數個數和類型相同,最後兩個參數分別是AsyncCallback和Object類型,對於它們更具體的內容,能夠參見下一節委託和方法的異步調用部分。如今,咱們僅須要對這兩個參數傳入null就能夠了。另外還須要注意幾點:
如今咱們修改一下上面的程序,使用異步調用來解決訂閱者方法執行超時的狀況:
class Program6 {
static void Main(string[] args) {
Publisher pub = new Publisher();
Subscriber1 sub1 = new Subscriber1();
Subscriber2 sub2 = new Subscriber2();
Subscriber3 sub3 = new Subscriber3();
pub.MyEvent += new EventHandler(sub1.OnEvent);
pub.MyEvent += new EventHandler(sub2.OnEvent);
pub.MyEvent += new EventHandler(sub3.OnEvent);
pub.DoSomething(); // 觸發事件
Console.WriteLine("Control back to client! "); // 返回控制權
Console.WriteLine("Press any thing to exit...");
Console.ReadKey(); // 暫停客戶程序,提供時間供訂閱者完成方法
}
}
public class Publisher {
public event EventHandler MyEvent;
public void DoSomething() {
// 作某些其餘的事情
Console.WriteLine("DoSomething invoked!");
if (MyEvent != null) {
Delegate[] delArray = MyEvent.GetInvocationList();
foreach (Delegate del in delArray) {
EventHandler method = (EventHandler)del;
method.BeginInvoke(null, EventArgs.Empty, null, null);
}
}
}
}
public class Subscriber1 {
public void OnEvent(object sender, EventArgs e) {
Thread.Sleep(TimeSpan.FromSeconds(3)); // 模擬耗時三秒才能完成方法
Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!");
}
}
public class Subscriber2 {
public void OnEvent(object sender, EventArgs e) {
throw new Exception("Subsciber2 Failed"); // 即便拋出異常也不會影響到客戶端
//Console.WriteLine("Subscriber2 immediately Invoked!");
}
}
public class Subscriber3 {
public void OnEvent(object sender, EventArgs e) {
Thread.Sleep(TimeSpan.FromSeconds(2)); // 模擬耗時兩秒才能完成方法
Console.WriteLine("Waited for 2 seconds, subscriber3 invoked!");
}
}
運行上面的代碼,會獲得下面的輸出:
DoSomething invoked!
Control back to client!
Press any thing to exit...
Waited for 2 seconds, subscriber3 invoked!
Waited for 3 seconds, subscriber1 invoked!
須要注意代碼輸出中的幾個變化:
一般狀況下,若是須要異步執行一個耗時的操做,咱們會新起一個線程,而後讓這個線程去執行代碼。可是對於每個異步調用都經過建立線程來進行操做顯然會對性能產生必定的影響,同時操做也相對繁瑣一些。.Net中能夠經過委託進行方法的異步調用,就是說客戶端在異步調用方法時,自己並不會由於方法的調用而中斷,而是從線程池中抓取一個線程去執行該方法,自身線程(主線程)在完成抓取線程這一過程以後,繼續執行下面的代碼,這樣就實現了代碼的並行執行。使用線程池的好處就是避免了頻繁進行異步調用時建立、銷燬線程的開銷。
如同上面所示,當咱們在委託對象上調用BeginInvoke()時,便進行了一個異步的方法調用。上面的例子中是在事件的發佈和訂閱這一過程當中使用了異步調用,而在事件發佈者和訂閱者之間每每是鬆耦合的,發佈者一般不須要得到訂閱者方法執行的狀況;而當使用異步調用時,更多狀況下是爲了提高系統的性能,而並不是專用於事件的發佈和訂閱這一編程模型。而在這種狀況下使用異步編程時,就須要進行更多的控制,好比當異步執行方法的方法結束時通知客戶端、返回異步執行方法的返回值等。本節就對BeginInvoke()方法、EndInvoke()方法和其相關的IAysncResult作一個簡單的介紹。
NOTE:注意此處我已經再也不使用發佈者、訂閱者這些術語,由於咱們再也不是討論上面的事件模型,而是討論在客戶端程序中異步地調用方法,這裏有一個思惟的轉變。
咱們看這樣一段代碼,它演示了不使用異步調用的一般狀況:
class Program7 {
static void Main(string[] args) {
Console.WriteLine("Client application started! ");
Thread.CurrentThread.Name = "Main Thread";
Calculator cal = new Calculator();
int result = cal.Add(2, 5);
Console.WriteLine("Result: {0} ", result);
// 作某些其它的事情,模擬須要執行3秒鐘
for (int i = 1; i <= 3; i++) {
Thread.Sleep(TimeSpan.FromSeconds(i));
Console.WriteLine("{0}: Client executed {1} second(s).",
Thread.CurrentThread.Name, i);
}
Console.WriteLine(" Press any key to exit...");
Console.ReadKey();
}
}
public class Calculator {
public int Add(int x, int y) {
if (Thread.CurrentThread.IsThreadPoolThread) {
Thread.CurrentThread.Name = "Pool Thread";
}
Console.WriteLine("Method invoked!");
// 執行某些事情,模擬須要執行2秒鐘
for (int i = 1; i <= 2; i++) {
Thread.Sleep(TimeSpan.FromSeconds(i));
Console.WriteLine("{0}: Add executed {1} second(s).",
Thread.CurrentThread.Name, i);
}
Console.WriteLine("Method complete!");
return x + y;
}
}
上面代碼有幾個關於對於線程的操做,若是不瞭解能夠看一下下面的說明,若是你已經瞭解能夠直接跳過:
經過這幾個方法和屬性,有助於咱們更好地調試異步調用方法。上面代碼中除了加入了一些對線程的操做之外再沒有什麼特別之處。咱們建了一個Calculator類,它只有一個Add方法,咱們模擬了這個方法須要執行2秒鐘時間,而且每隔一秒進行一次輸出。而在客戶端程序中,咱們使用result變量保存了方法的返回值並進行了打印。隨後,咱們再次模擬了客戶端程序接下來的操做須要執行2秒鐘時間。運行這段程序,會產生下面的輸出:
Client application started!
Method invoked!
Main Thread: Add executed 1 second(s).
Main Thread: Add executed 2 second(s).
Method complete!
Result: 7
Main Thread: Client executed 1 second(s).
Main Thread: Client executed 2 second(s).
Main Thread: Client executed 3 second(s).
Press any key to exit...
若是你確實執行了這段代碼,會看到這些輸出並非一瞬間輸出的,而是執行了大概5秒鐘的時間,由於線程是串行執行的,因此在執行完Add()方法以後纔會繼續客戶端剩下的代碼。
接下來咱們定義一個AddDelegate委託,並使用BeginInvoke()方法來異步地調用它。在上面已經介紹過,BeginInvoke()除了最後兩個參數爲AsyncCallback類型和Object類型之外,前面的參數類型和個數與委託定義相同。另外BeginInvoke()方法返回了一個實現了IAsyncResult接口的對象(實際上就是一個AsyncResult類型實例,注意這裏IAsyncResult和AysncResult是不一樣的,它們均包含在.Net Framework中)。
AsyncResult的用途有這麼幾個:傳遞參數,它包含了對調用了BeginInvoke()的委託的引用;它還包含了BeginInvoke()的最後一個Object類型的參數;它能夠鑑別出是哪一個方法的哪一次調用,由於經過同一個委託變量能夠對同一個方法調用屢次。
EndInvoke()方法接受IAsyncResult類型的對象(以及ref和out類型參數,這裏不討論了,對它們的處理和返回值相似),因此在調用BeginInvoke()以後,咱們須要保留IAsyncResult,以便在調用EndInvoke()時進行傳遞。這裏最重要的就是EndInvoke()方法的返回值,它就是方法的返回值。除此之外,當客戶端調用EndInvoke()時,若是異步調用的方法沒有執行完畢,則會中斷當前線程而去等待該方法,只有當異步方法執行完畢後纔會繼續執行後面的代碼。因此在調用完BeginInvoke()後當即執行EndInvoke()是沒有任何意義的。咱們一般在儘量早的時候調用BeginInvoke(),而後在須要方法的返回值的時候再去調用EndInvoke(),或者是根據狀況在晚些時候調用。說了這麼多,咱們如今看一下使用異步調用改寫後上面的代碼吧:
public delegate int AddDelegate(int x, int y);
class Program8 {
static void Main(string[] args) {
Console.WriteLine("Client application started! ");
Thread.CurrentThread.Name = "Main Thread";
Calculator cal = new Calculator();
AddDelegate del = new AddDelegate(cal.Add);
IAsyncResult asyncResult = del.BeginInvoke(2,5,null,null); // 異步調用方法
// 作某些其它的事情,模擬須要執行3秒鐘
for (int i = 1; i <= 3; i++) {
Thread.Sleep(TimeSpan.FromSeconds(i));
Console.WriteLine("{0}: Client executed {1} second(s).",
Thread.CurrentThread.Name, i);
}
int rtn = del.EndInvoke(asyncResult);
Console.WriteLine("Result: {0} ", rtn);
Console.WriteLine(" Press any key to exit...");
Console.ReadKey();
}
}
public class Calculator { /* 與上面同,略 */}
此時的輸出爲:
Client application started!
Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Main Thread: Client executed 3 second(s).
Result: 7
Press any key to exit...
如今執行完這段代碼只須要3秒鐘時間,兩個for循環所產生的輸出交替進行,這也說明了這兩段代碼並行執行的狀況。能夠看到Add()方法是由線程池中的線程在執行,由於Thread.CurrentThread.IsThreadPoolThread返回了True,同時咱們對該線程命名爲了Pool Thread。另外咱們能夠看到經過EndInvoke()方法獲得了返回值。
有時候,咱們可能會將得到返回值的操做放到另外一段代碼或者客戶端去執行,而不是向上面那樣直接寫在BeginInvoke()的後面。好比說咱們在Program中新建一個方法GetReturn(),此時能夠經過AsyncResult的AsyncDelegate得到del委託對象,而後再在其上調用EndInvoke()方法,這也說明了AsyncResult能夠惟一的獲取到與它相關的調用了的方法(或者也能夠理解成委託對象)。因此上面獲取返回值的代碼也能夠改寫成這樣:
static int GetReturn(IAsyncResult asyncResult) {
AsyncResult result = (AsyncResult)asyncResult;
AddDelegate del = (AddDelegate)result.AsyncDelegate;
int rtn = del.EndInvoke(asyncResult);
return rtn;
}
而後再將int rtn = del.EndInvoke(asyncResult);語句改成int rtn = GetReturn(asyncResult);。注意上面IAsyncResult要轉換爲實際的類型AsyncResult才能訪問AsyncDelegate屬性,由於它沒有包含在IAsyncResult接口的定義中。
BeginInvoke的另外兩個參數分別是AsyncCallback和Object類型,其中AsyncCallback是一個委託類型,它用於方法的回調,便是說當異步方法執行完畢時自動進行調用的方法。它的定義爲:
public delegate void AsyncCallback(IAsyncResult ar);
Object類型用於傳遞任何你想要的數值,它能夠經過IAsyncResult的AsyncState屬性得到。下面咱們將獲取方法返回值、打印返回值的操做放到了OnAddComplete()回調方法中:
public delegate int AddDelegate(int x, int y);
class Program9 {
static void Main(string[] args) {
Console.WriteLine("Client application started! ");
Thread.CurrentThread.Name = "Main Thread";
Calculator cal = new Calculator();
AddDelegate del = new AddDelegate(cal.Add);
string data = "Any data you want to pass.";
AsyncCallback callBack = new AsyncCallback(OnAddComplete);
del.BeginInvoke(2, 5, callBack, data); // 異步調用方法
// 作某些其它的事情,模擬須要執行3秒鐘
for (int i = 1; i <= 3; i++) {
Thread.Sleep(TimeSpan.FromSeconds(i));
Console.WriteLine("{0}: Client executed {1} second(s).",
Thread.CurrentThread.Name, i);
}
Console.WriteLine(" Press any key to exit...");
Console.ReadKey();
}
static void OnAddComplete(IAsyncResult asyncResult) {
AsyncResult result = (AsyncResult)asyncResult;
AddDelegate del = (AddDelegate)result.AsyncDelegate;
string data = (string)asyncResult.AsyncState;
int rtn = del.EndInvoke(asyncResult);
Console.WriteLine("{0}: Result, {1}; Data: {2} ",
Thread.CurrentThread.Name, rtn, data);
}
}
public class Calculator { /* 與上面同,略 */}
它產生的輸出爲:
Client application started!
Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Pool Thread: Result, 7; Data: Any data you want to pass.
Main Thread: Client executed 3 second(s).
Press any key to exit...
這裏有幾個值得注意的地方:一、咱們在調用BeginInvoke()後再也不須要保存IAysncResult了,由於AysncCallback委託將該對象定義在了回調方法的參數列表中;二、咱們在OnAddComplete()方法中得到了調用BeginInvoke()時最後一個參數傳遞的值,字符串「Any data you want to pass」;三、執行回調方法的線程並不是客戶端線程Main Thread,而是來自線程池中的線程Pool Thread。另外如前面所說,在調用EndInvoke()時有可能會拋出異常,因此在應該將它放到try/catch塊中,這裏我就再也不示範了。
----------------------------------------------------------
什麼是委託
首先要知道什麼是委託,用最通俗易懂的話來說,你就能夠把委託當作是用來執行方法(函數)的一個東西。
如何使用委託
在使用委託的時候,你能夠像對待一個類同樣對待它。即先聲明,再實例化。只是有點不一樣,類在實例化以後叫對象或實例,但委託在實例化後仍叫委託。
聲明,如:
1 namespace Vczx.ProCSharp.Exc
2 {
3 delegate double MathsOp( double x );
4 //class defination here
5 }
這就聲明瞭一個委託,意義:任何一個返回值爲double,且只有一個形參爲double的函數,均可以用這個委託來調用。
注意:委託的聲明位置在namespace裏面,類的外面。其實,委託的聲明也能夠在類的裏面,甚至是任何一個能夠聲明類的地方。
實例化:
首先咱們要先有一個知足委託聲明的方法,假設一個返回一個數的2倍的方法:
1class MathsOperations
2{
3 public static double MultiplyBy2( double value )
4 {
5 return value * 2;
6 }
7}
有了這樣一個方法,咱們就能夠實例化一個委託了:
MathsOp operation = new MathsOp( MathsOperations.MultiplyBy2 );
在實例化一個委託時,要給它一個參數,這個參數就是委託執行的方法,它能夠是靜態方法,也能夠是實例方法(這一點有別於函數指針,函數指針只能調用靜態方法),如:
MathsOp operation = new MathsOp( new Class1().Method1 );
在實例化完一個委託以後,就能夠用這個委託來調用方法了:
double result = operation( 1.23 );
例子代碼:
1namespace Vczx.ProCSharp.Exc
2{
3 delegate double MathsOp( double x );
4 class Start
5 {
6 public class MyDelegate
7 {
8 public static double MultiplyBy2( double x )
9 {
10 return x * 2;
11 }
12 }
13 [STAThread]
14 static void Main(string[] args)
15 {
16 MathsOp operation = new MathsOp( MyDelegate.MultiplyBy2 );
17 double x = 1.23;
18 double result = operation( x );
19 Console.WriteLine( "{0} multiply by 2 is {1}", x, result );
20 Console.Read();
21 }
22 }
23}
多路廣播委託
前面使用的委託只包含一個方法調用。調用委託的次數與調用方法的次數相同。若是要調用多個方法,就須要屢次顯示調用這個委託。其實委託也能夠包含多個方法,這種委託就是多路廣播委託。多路廣播委託派生於System.MulticastDelegate,它的Combine方法容許把多個方法調用連接在一塊兒,咱們能夠經過+=來向委託添加調用方法,也能夠用-=刪除其中的調用方法。如:
1namespace Vczx.ProCSharp.Exc
2{
3 public class MyDelegate
4 {
5 public static void MultiplyBy2( double value )
6 {
7 double result = value * 2;
8 Console.WriteLine( "Multiplying by 2: {0} gives {1}", value, result );
9 }
10
11 public static void Squre( double value )
12 {
13 double result = value * value;
14 Console.WriteLine( "Squaring: {0} gives {1}", value, result );
15 }
16 }
17
18 delegate void MathsOp( double x );
19
20 class Start
21 {
22 [STAThread]
23 static void Main(string[] args)
24 {
25 MathsOp operation = new MathsOp( MyDelegate.MultiplyBy2 );
26 operation += new MathsOp( MyDelegate.Squre );
27 double x = 1.23;
28 operation( x );
29
30 operation -= new MathsOp( MyDelegate.MultiplyBy2 );
31 operation( x );
32
33 Console.Read();
34 }
35 }
36}
輸出:
Multiplying by 2: 1.23 gives 2.46
Squaring: 1.23 gives 1.5129
Squaring: 1.23 gives 1.5129
注意,多路廣播委託聲明時必須返回void,不然返回值不知道應該送回什麼地方。對此,我作了一個測試:若是不將委託的聲明返回void,則返回值返回的是最後一個鏈入委託鏈的方法的返回值,編譯不會出錯。
爲何要用委託 使用委託使程序員能夠將方法引用封裝在委託對象內。而後能夠將該委託對象傳遞給可調用所引用方法的代碼,而沒必要在編譯時知道將調用哪一個方法。與C或C++中的函數指針不一樣,委託是面向對象,並且是類型安全的。