C# 深入浅出 委托与事件
C#中的委托和事件的概念接觸很久了,但是一直以來總沒有特別透徹的感覺,現在我在這里總結一下:
首先我們要知道委托的由來,為什么要使用委托了?
我們先看一個例子:
假設我們有這樣一個需求,需要計算在不同方式下的總價,如下面代碼所示,這里假設只有兩種方式,一種是正常價格,一種是折扣價格:
1 public enum CalcMethod 2 { 3 Normal, 4 Debate 5 } 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 CalcPrice(10, 100,CalcMethod.Normal); 11 CalcPrice(10, 100, CalcMethod.Debate); 12 Console.ReadLine(); 13 } 14 15 /// <summary> 16 /// 計算總價 17 /// </summary> 18 /// <param name="count">數量</param> 19 /// <param name="price">單價</param> 20 public static double CalcPrice(int count,int price,CalcMethod method) 21 { 22 switch (method) 23 { 24 case CalcMethod.Normal: 25 return NormalPrice(count, price); 26 case CalcMethod.Debate: 27 return DebatePrice(count, price); 28 default: 29 return 0; 30 } 31 } 32 33 public static double NormalPrice(int count,int price) 34 { 35 Console.WriteLine("正常的價格是:{0}", count * price); 36 return count * price; 37 } 38 public static double DebatePrice(int count, int price) 39 { 40 Console.WriteLine("折扣的價格是:{0}", count * price*0.7); 41 return count * price*0.7; 42 } 43 } View Code但是我們想一想,如果還要增加總價計算方式,那么我們是不是要不斷的修改CalcPrice方法,CalcMethod枚舉呢?
那么是不是有更好的方式呢?
我們可以把真正的計算價格的方式委托給一個函數來計算,這樣委托就誕生了。
首先我們定義一個跟方法參數和返回值類型一樣的委托類型:
?public delegate double CalcPriceDelegate(int count, int price);
然后修改計算方法:
??????? /// <summary>
??????? /// 計算總價
??????? /// </summary>
??????? /// <param name="count">數量</param>
??????? /// <param name="price">單價</param>
??????? public static double CalcPrice(int count, int price, CalcPriceDelegate calcDelegate)
??????? {
??????????? return calcDelegate(count, price);
??????? }
然后調用的時候直接用簽名相同的方法傳遞就可以了:
???????? ? CalcPrice(10, 100, NormalPrice);
??????????? CalcPrice(10, 100, DebatePrice);
?????到這里我們大體明白了委托可以使用方法作為參數,這樣就避免了程序中出現大量的條件分支語句,程序的擴展性好。
???接下來我要對委托做一個深入的探討:
委托首先其實也是一個類,
????????? public delegate double CalcPriceDelegate(int count, int price);
上面這句話其實就是申明一種委托類型,這個類型在編譯的時候會生成以下成員:
1)public?extern CalcPriceDelegate(object @object, IntPtr method);
第一個參數是記錄委托對象包裝的實例方法所在的對象(this,如果包裝的是靜態方法,就為NULL),第二個參數就是表示要回調的方法。
2) public virtual extern double Invoke(int count, int price);//同步調用委托方法
3)public virtual extern IAsynResult BeginInvoke(int count, int price, AsyncCallback callback, object @object);??這個是異步執行委托方法,前面兩個參數是委托的方法的輸入參數,callback是回調方法,也就是說方法執行完成后的回調方法,AsyncCallback本身也是一個委托類型,其原型是:
public delegate void AsyncCallback(IAsyncResult ar);
??最后一個參數是回調所需要的輸入參數,這個參數會隱含在IAsyncResult的AsyncState中。
另外返回值也是一個IAsyncResult結果。
????????????與之相對應的,public virtual extern double?EndInvoke(IAsyncResult result)
結束異步回調。
以前對IAsyncResult,還有AsyncCallback都有點陌生,其實我們可以這樣來理解,我想要一個方法異步來執行,那么肯定就需要調用BeginInvoke,那么我如何又能知道什么時候這個異步的調用結束呢?這就需要用到AsyncCallback這個異步回調,如果這個回調需要參數,就賦值給object,如果不確定是否異步執行完,就要用EndInvoke來確保結束,輸入參數就是BeginInvoke的返回值IAsynResult,就相當于BeginInvoke的時候開出了一個收據,EndInvoke又把這個收據還了。
??????????這個話題要想深入下去就太多了,我們還是回到委托上來,委托時一個類,其編譯后就是這么些個成員。
我平時調用的時候經常會被各種各樣的調用方法給搞糊涂了,這里總結下各種調用方式:
?假設委托類型為:public delegate double CalcPriceDelegate(int count, int price);?這就相當于定義了一個類,
接下來就是賦值了(相當于申明對象):
1)CalcPriceDelegate calcDelegate = new CalcPriceDelegate(NormalPrice).?這是最完整的賦值方式。
2)?CalcPriceDelegate calcDelegate = NormalPrice;?直接賦值方法,編譯器會自動幫我們構造成第一種賦值方式。
賦值完成后接下來就是如何調用了:
1)calcDelegate(10,100);
2)calcDelegate.Invoke(10,100)??與1)方法是一樣的。
3)calcDelegate.BeginInvoke(10,100,null,null)?異步執行
委托還可以通過+=來添加方法,委托給多個方法,但是第一個必須是=,否則沒有初始化,相對的,可以使用-=來移除方法。
至于匿名委托,lambda表達式是一樣的,把握本質就可以了,還需要了解MS定義的委托類型,這里暫時不講了。
?
接下來講事件:
第一步,我們還是引用上面的例子,只是把相關的代碼放到一個類里面:
1 public delegate double CalcPriceDelegate(int count, int price); 2 public class CalcPriceClass 3 { 4 public double Calc(int count, int price, CalcPriceDelegate calcDelegate) 5 { 6 return calcDelegate(count, price); 7 } 8 } View Code
????????其中主函數里面的調用如下:
1 Console.WriteLine("演示引入事件的第一步:"); 2 CalcPriceClass cp = new CalcPriceClass(); 3 cp.Calc(10, 100, NormalPrice); 4 cp.Calc(10, 100, DebatePrice); 5 Console.ReadLine(); View Code??????????這種方法,我們破壞了對象的封裝性,我們可以把委托類型的變量放到CalcPriceClass類里面。
于是我們就有了第二步:
???
1 public class CalcPriceClass2 2 { 3 public CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price, CalcPriceDelegate calcDelegate) 5 { 6 return calcDelegate(count, price); 7 } 8 } View Code?其中主函數的調用如下:
1 Console.WriteLine("演示引入事件的第二步:"); 2 CalcPriceClass2 cp2 = new CalcPriceClass2(); 3 cp2.m_delegate = NormalPrice; 4 cp2.m_delegate += DebatePrice; 5 cp2.Calc(10, 100, cp2.m_delegate); 6 Console.ReadLine(); View Code我們發現其調用有點怪怪的,? cp2.Calc(10, 100, cp2.m_delegate);既然我為cp2的委托對象賦值了,這個時候其實沒有必要再去傳遞這個委托對象了,于是就有了第三步:
1 public class CalcPriceClass3 2 { 3 public CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price) 5 { 6 if (m_delegate != null) 7 return m_delegate(count, price); 8 else return 0; 9 } 10 } View Code其中主函數的調用如下:
1 Console.WriteLine("演示引入事件的第三步:"); 2 CalcPriceClass3 cp3 = new CalcPriceClass3(); 3 cp3.m_delegate = NormalPrice; 4 cp3.m_delegate += DebatePrice; 5 cp3.Calc(10, 100); 6 Console.ReadLine(); View Code在這步完成后,貌似達到了我們想要的結果,但是還是有些隱患的,因為我們可以隨意的給委托變量賦值,所以就有了第四步,加上了事件:
1 public class CalcPriceClass4 2 { 3 public event CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price) 5 { 6 if (m_delegate != null) 7 return m_delegate(count, price); 8 else return 0; 9 } 10 } View Code其中主函數的調用如下:
1 Console.WriteLine("演示引入事件的第四步:"); 2 CalcPriceClass4 cp4 = new CalcPriceClass4(); 3 cp4.m_delegate += NormalPrice; 4 cp4.m_delegate += DebatePrice; 5 cp4.Calc(10, 100); 6 Console.ReadLine(); View Code我們給委托變量加上event后有什么不一樣呢?
這個時候我們不能直接給這個事件對象進行賦值,因為其內部是一個私有變量了,另外編譯器會增加兩個公共函數,
一個是add_m_delegate(對應+=),?
public void add_m_delegate(CalcPriceDelegate value)
{
this.m_delegate = (CalcPriceDelegate)Delegate.Combine(this.m_delegate, value);
}
一個是remove_m_delegate(對應-=),
public void remove_m_delegate(CalcPriceDelegate value)
{
this.m_delegate = (CalcPriceDelegate)Delegate.Remove(this.m_delegate, value);
}
另外內部的私有字段是這樣子的:private CalcPriceDelegate m_delegate;
通過這四步,我們可以知道了從委托到事件的一個過程,其實事件也是委托,只是編譯器會幫我們做一些事情而已。
?
事件?與 Observer設計模式
?
假設一個熱水器,在溫度達到95度以上的時候,警報器報警,并且顯示器顯示溫度。
代碼如下:
1 public class Heater 2 { 3 private int temperature; 4 public void BoilWater() 5 { 6 for (int i = 0; i < 100; i++) 7 { 8 temperature = i; 9 if (temperature > 95) 10 { 11 MakeAlert(temperature); 12 ShowMsg(temperature); 13 } 14 } 15 } 16 private void MakeAlert(int param) 17 { 18 Console.WriteLine("Alarm:滴滴滴,水已經{0}度了", param); 19 20 } 21 private void ShowMsg(int param) 22 { 23 Console.WriteLine("Display:水快開了,當前溫度:{0}度。", param); 24 } 25 } View Code假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自于不同廠商并進行了組裝。那么,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。如果是上面的代碼可能就不合適了,就要使用下面的代碼:
1 public class Heater2 2 { 3 private int temperature; 4 public delegate void BoilHanlder(int param); 5 public event BoilHanlder BoilEvent; 6 public void BoilWater() 7 { 8 for (int i = 0; i < 100; i++) 9 { 10 temperature = i; 11 if (temperature > 95) 12 { 13 if (BoilEvent != null) 14 BoilEvent(temperature); 15 } 16 } 17 } 18 } 19 public class Alarm 20 { 21 public static void MakeAlert(int param) 22 { 23 Console.WriteLine("Alarm:滴滴滴,水已經{0}度了", param); 24 25 } 26 } 27 public class Display 28 { 29 public static void ShowMsg(int param) 30 { 31 Console.WriteLine("Display:水快開了,當前溫度:{0}度。", param); 32 } 33 } View Code調用的代碼:
1 Console.WriteLine("演示熱水器熱水機報警及顯示水溫 觀察者模式"); 2 Heater2 ht2 = new Heater2(); 3 ht2.BoilEvent += new Heater2.BoilHanlder(Alarm.MakeAlert); 4 ht2.BoilEvent += Display.ShowMsg; 5 ht2.BoilWater(); 6 Console.ReadLine(); View Code比較以上兩種方式的不同,后面的這種更加符合面向對象的思想,因為作為熱水器而言,主要的工作是熱水,至于報警,顯示溫度是由其他器件來顯示,是需要顯示器,報警器這些觀察者來觀察熱水器這個觀察對象主體的溫度。
到這里為止,我們知道了如何聲明委托類型,申明委托對象,調用委托方法,委托類的實際成員,以及從委托到事件的演變,事件的表象與在編譯后的實際成員,以及作為觀察者模式使用事件的過程。
不過微軟的規范寫法卻不是這樣,下面改用微軟的規范寫法:
1 public class Heater3 2 { 3 public string type = "RealFire 001"; 4 public string area = "China Xian"; 5 private int temperature; 6 public delegate void BoiledEventHandler(object sender,BoiledEventArgs e); 7 public event BoiledEventHandler Boiled; 8 protected virtual void OnBoiled(BoiledEventArgs e) 9 { 10 if (Boiled != null) 11 { 12 Boiled(this, e); 13 } 14 } 15 public void BoilWater() 16 { 17 for (int i = 0; i < 100; i++) 18 { 19 temperature = i; 20 if (temperature > 95) 21 { 22 BoiledEventArgs e = new BoiledEventArgs(temperature); 23 OnBoiled(e); 24 } 25 } 26 } 27 } 28 public class BoiledEventArgs : EventArgs 29 { 30 public readonly int temperature; 31 public BoiledEventArgs(int temp) 32 { 33 temperature = temp; 34 } 35 } 36 public class Alarm3 37 { 38 public void MakeAlert(object sender, BoiledEventArgs e) 39 { 40 Heater3 ht = (Heater3)sender; 41 Console.WriteLine("Alarm:{0}-{1}", ht.area, ht.type); 42 Console.WriteLine("Alarm:滴滴滴,水已經{0}度了:", e.temperature); 43 Console.WriteLine(); 44 } 45 } 46 public class Display3 47 { 48 public static void ShowMsg(object sender, BoiledEventArgs e) 49 { 50 Heater3 ht = (Heater3)sender; 51 Console.WriteLine("Display:{0}-{1}",ht.area,ht.type); 52 Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", e.temperature); 53 Console.WriteLine(); 54 } 55 56 } View Code調用代碼:
1 Console.WriteLine("演示熱水器熱水機報警及顯示水溫 符合微軟模式"); 2 Heater3 ht3 = new Heater3(); 3 Alarm3 al3 = new Alarm3(); 4 ht3.Boiled += al3.MakeAlert; 5 ht3.Boiled += Display3.ShowMsg; 6 ht3.BoilWater(); 7 Console.ReadLine(); View Code微軟的規范寫法,如果要傳遞參數一般使用EventArgs或者其繼承類,且繼承類的命名以EventArgs結尾。
委托類型的名稱以EventHandler結束。
委托的原型定義:有一個void返回值,并接受兩個輸入參數:一個Object 類型,一個 EventArgs類型(或繼承自EventArgs)。
一般這個Object類型指的是觀察的對象,我們可以這樣來記憶,因為委托就相當于方法,其實執行的就是觀察者,那么觀察者總要知道觀察誰,以及觀察所需要的參數。
其實事件還有一些概念:事件訂閱者,事件接收者,事件發送者,這些以后再補充
?
這里引用了這位仁兄的博客:
http://www.tracefact.net/csharp-programming/delegates-and-events-in-csharp.aspx
?代碼:
?http://files.cnblogs.com/files/monkeyZhong/CSharpDelegateAndEvent.zip
?
?
?
?
轉載于:https://www.cnblogs.com/monkeyZhong/p/4595444.html
總結
以上是生活随笔為你收集整理的C# 深入浅出 委托与事件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一款WP小游戏代码分享
- 下一篇: 【WEB HTTP】缓存