在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知識點。照此,ASP.NET MVC 大牛之路系列也先給大家普及一下C#.NET中的高級知識點。每個知識點不太會過於詳細,但足矣。要深入研究還需要去查看更多的專業資料。 要成為大牛,必然要有扎實的基本功,不然時間再長項目再多 ...
在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知識點。照此,ASP.NET MVC 大牛之路系列也先給大家普及一下C#.NET中的高級知識點。每個知識點不太會過於詳細,但足矣。要深入研究還需要去查看更多的專業資料。
要成為大牛,必然要有扎實的基本功,不然時間再長項目再多也很難有大的提升。本系列講的C# 高級知識點,是非常值得去撐握的,不僅可以讓你寫代碼時游刃有餘,而且去研究和學習一些開源項目時,也不會顯得那麼吃力了。
希望大家記住,這裡講的所有的知識點,不僅僅是瞭解了就可以了,還要會靈活用,一定要多思考,撐握其中的編程思想。
本文講的是委托和事件,這兩個詞可能你早就耳熟能詳,但你是否真正撐握了呢?
本系列講的C#高級知識點都是要求開發時能達到可以徒手寫出來的水平(不依賴搜索引擎、找筆記等)。建議開發時儘量自己寫(時間允許的話),如果覺得自己寫的不好,再Google。寫多了就自然會靈活運用。
本文目錄:http://www.jinhusns.com/Products/Download/?type=xcj
委托
委托太常見了,能靈活運用可以使你在編程中游刃有餘。
簡單說它就是一個能把方法當參數傳遞的對象,而且還知道怎麼調用這個方法。
委托的簡單使用
一個委托類型定義了該類型的實例能調用的一類方法,這些方法含有同樣的返回類型和同樣參數(類型和個數相同)。委托和介面一樣,可以定義在類的外部。如下定義了一個委托類型 - Calculator:
delegate int Calculator (int x);
此委托適用於任何有著int返回類型和一個int類型參數的方法,如:
static int Double (int x) { return x * 2; }
創建一個委托實例,將該此方法賦值給該委托實例:
Calculator c = new Calculator(Double);
也可以簡寫成:
Calculator c = Double;
這個方法可以通過委托調用:
int result = c(2);
下麵是完整代碼:
delegate int Calculator(int x);class Program { static int Double(int x) { return x * 2; } static void Main(string[] args) { Calculator c = Double; int result = c(2); Console.Write(result); Console.ReadKey(); } }
用委托實現插件式編程
我們可以利用“委托是一個能把方法作為參數傳遞的對象”這一特點,來實現一種插件式編程。
例如,我們有一個Utility類,這個類實現一個通用方法(Calculate),用來執行任何有一個整型參數和整型返回值的方法。這樣說有點抽象,下麵來看一個例子:
delegate int Calculator(int x);class Program { static int Double(int x) { return x * 2; } static void Main(string[] args) { int[] values = { 1,2,3,4}; Utility.Calculate(values, Double); foreach (int i in values) Console.Write(i + " "); // 2 4 6 8 Console.ReadKey(); } }class Utility { public static void Calculate(int[] values, Calculator c) { for (int i = 0; i < values.Length; i++) values[i] = c(values[i]); } }
這個例子中的Utility是固定不變的,程式實現了整數的Double功能。我們可以把這個Double方法看作是一個插件,如果將來還要實現諸如求平方、求立方的計算,我們只需向程式中不斷添加插件就可以了。
如果Double方法是臨時的,只調用一次,若在整個程式中不會有第二次調用,那麼我們可以在Main方法中更簡潔更靈活的使用這種插件式編程,無需先定義方法,使用λ表達式即可,如:
... Utility.Calculate(values, (x) => { return x * 2; }); ...
以後我們會經常寫這樣的代碼。
多播委托
所 有的委托實例都有多播的功能。所謂多播,就像一群程式員在瞬聘網填好了求職意向後,某天有個公司發佈了一個和這些程式員求職意向剛好相匹配的工作,然後這 些求職者都被通知了 - “有一份好工作招人啦,你們可以直接申請去上班了!”。PS:為了公司,我也算滿拼的。懇請大家允許我在博文中不忘提到瞬聘網。在後續博文中會有不少案例 確實是我在瞬聘網系統開發時使用過的。:)
也就是說,一個委托實例不僅可以指向一個方法,還可以指向多個方法。例如:
MyDelegate d = MyMethod1;// “+=” 用來添加,同理“-=”用來移除。d += MyMethod2;// d -= MyMethod2
調用時,按照方法被添加的順序依次執行。註意,對於委托,+= 和 -= 對null是不會報錯的,如:
MyDelegate d; d += MyMethod1;// 相當於MyDelegate d = MyMethod1;
為 了更好的理解多播在實際開發中的應用,我用模擬瞬聘網的職位匹配小工具來做示例。在職位匹配過程中會有一段處理時間,所以在執行匹配的時候要能看到執行的 進度,而且還要把執行的進度和執行情況寫到日誌文件中。在處理完一個步驟時,將分別執行兩個方法來顯示和記錄執行進度。
我們先定義一個委托(ProgressReporter),然後定義一個匹配方法(Match)來執行該委托中的所有方法。如下:
public delegate void ProgressReporter(int percentComplete);public class Utility { public static void Match(ProgressReporter p) { if (p != null) { for (int i = 0; i <= 10; i++) { p(i * 10); System.Threading.Thread.Sleep(100); } } } }
然後我們需要兩個監視進度的方法,一個把進度寫到Console,另一個把進度寫到文件。如下:
class Program { static void Main(string[] args) { ProgressReporter p = WriteProgressToConsole; p += WriteProgressToFile; Utility.Match(p); Console.WriteLine("Done."); Console.ReadKey(); } static void WriteProgressToConsole(int percentComplete) { Console.WriteLine(percentComplete+"%"); } static void WriteProgressToFile(int percentComplete) { System.IO.File.AppendAllText("progress.txt", percentComplete + "%"); } }
運行結果:
看到這裡,是不是發現你已然更加愛上C#了。
靜態方法和實例方法對於委托的區別
當一個類的實例的方法被賦給一個委托對象時,在上下文中不僅要維護這個方法,還要維護這個方法所在的實例。System.Delegate 類的Target屬性指向的就是這個實例。舉個例子:
class Program { static void Main(string[] args) { X x = new X(); ProgressReporter p = x.InstanceProgress; p(1); Console.WriteLine(p.Target == x); // True Console.WriteLine(p.Method); // Void InstanceProgress(Int32) } static void WriteProgressToConsole(int percentComplete) { Console.WriteLine(percentComplete+"%"); } static void WriteProgressToFile(int percentComplete) { System.IO.File.AppendAllText("progress.txt", percentComplete + "%"); } }class X { public void InstanceProgress(int percentComplete) { // do something } }
但對於靜態方法,System.Delegate 類的Target屬性是Null,所以將靜態方法賦值給委托時性能更優。
泛型委托
如果你知道泛型,那麼就很容易理解泛型委托,說白了就是含有泛型參數的委托,例如:
public delegate T Calculator<T> (T arg);
我們可以把前面的例子改成泛型的例子,如下:
public delegate T Calculator<T>(T arg);class Program { static int Double(int x) { return x * 2; } static void Main(string[] args) { int[] values = { 1, 2, 3, 4 }; Utility.Calculate(values, Double); foreach (int i in values) Console.Write(i + " "); // 2 4 6 8 Console.ReadKey(); } }class Utility { public static void Calculate<T>(T[] values, Calculator<T> c) { for (int i = 0; i < values.Length; i++) values[i] = c(values[i]); } }
Func 和 Action 委托
有了泛型委托,就有了一能適用於任何返回類型和任意參數(類型和合理的個數)的通用委托,Func 和 Action。如下所示(下麵的in表示參數,out表示返回結果):
delegate TResult Func <out TResult> ();delegate TResult Func <in T, out TResult> (T arg);delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2); ... 一直到 T16delegate void Action ();delegate void Action <in T> (T arg);delegate void Action <in T1, in T2> (T1 arg1, T2 arg2); ... 一直到 T16
有了這樣的通用委托,我們上面的Calculator泛型委托就可以刪掉了,示例就可以更簡潔了:
public static void Calculate<T>(T[] values, Func<T,T> c) { for (int i = 0; i < values.Length; i++) values[i] = c(values[i]); }
Func 和 Action 委托,除了ref參數和out參數,基本上能適用於任何泛型委托的場景,非常好用。
委托的相容
1. 委托的類型相容
delegate void D1();delegate void D2(); ... D1 d1 = Method1; D2 d2 = d1;
下麵是被允許的:
D2 d2 = newD2 (d1);
對於具體相同的目標方法的委托是被視為相等的:
delegate void D(); ... D d1 = Method1; D d2 = Method1; Console.WriteLine (d1 == d2); // True
同理,對於多播委托,如果含有相同的方法和相同的順序,也被視為相等。
2. 參數類型相容
在OOP中,任何使用父類的地方均可以用子類代替,這個OOP思想對委托的參數同樣有效。如:
delegate void StringAction(string s);class Program { static void Main() { StringAction sa = new StringAction(ActOnObject); sa("hello"); } static void ActOnObject(object o) { Console.WriteLine(o); // hello } }
3. 返回值類型相容
道理和參數類型相容一樣:
delegate object ObjectRetriever();class Program { static void Main() { ObjectRetriever o = new ObjectRetriever(RetriveString); object result = o(); Console.WriteLine(result); // hello } static string RetriveString() { return "hello"; } }
事件
當我們使用委托場景時,我們很希望有這樣兩個角色出現:廣播者和訂閱者。我們需要這兩個角色來實現訂閱和廣播這種很常見的場景。
廣播者這個角色應該有這樣的功能:包括一個委托欄位,通過調用委托來發出廣播。而訂閱者應該有這樣的功能:可以通過調用 += 和 -= 來決定何時開始或停止訂閱。
事件就是描述這種場景模式的一個詞。事件是委托的一個子集,為了滿足“廣播/訂閱”模式的需求而生。
事件的基本使用
聲明一個事件很簡單,只需在聲明一個委托對象時加上event關鍵字就行。如下:
public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);public class IPhone6 { public event PriceChangedHandler PriceChanged; }
事件的使用和委托完全一樣,只是多了些約束。下麵是一個簡單的事件使用例子:
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);public class IPhone6 { decimal price; public event PriceChangedHandler PriceChanged; public decimal Price { get { return price; } set { if (price == value) return; decimal oldPrice = price; price = value; // 如果調用列表不為空,則觸發。 if (PriceChanged != null) PriceChanged(oldPrice, price); } } }class Program { static void Main() { IPhone6 iphone6 = new IPhone6() { Price = 5288 }; // 訂閱事件 iphone6.PriceChanged += iphone6_PriceChanged; // 調整價格(事件發生) iphone6.Price = 3999; Console.ReadKey(); } static void iphone6_PriceChanged(decimal oldPrice, decimal price) { Console.WriteLine("年終大促銷,iPhone 6 只賣 " + price + " 元, 原價 " + oldPrice + " 元,快來搶!"); } }
運行結果:
有人可能會問,如果把上面的event關鍵字拿掉,結果不是一樣的嗎,到底有何不同?
沒錯可以用事件的地方就一定可以用委托代替。
但 事件有一系列規則和約束用以保證程式的安全可控,事件只有 += 和 -= 操作,這樣訂閱者只能有訂閱或取消訂閱操作,沒有許可權執行其它操作。如果是委托,那麼訂閱者就可以使用 = 來對委托對象重新賦值(其它訂閱者全部被取消訂閱),甚至將其設置為null,甚至訂閱者還可以直接調用委托,這些都是很危險的操作,廣播者就失去了獨享 控制權。
事件保證了程式的安全性和健壯性。
事件的標準模式
.NET 框架為事件編程定義了一個標準模式。設定這個標準是為了讓.NET框架和用戶代碼保持一致。System.EventArgs是標準模式的核心,它是一個沒有任何成員,用於傳遞事件參數的基類。
按照標準模式,我們對於上面的iPhone6示例進行重寫。首先定義EventArgs:
public class PriceChangedEventArgs : EventArgs { public readonly decimal OldPrice; public readonly decimal NewPrice; public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) { OldPrice = oldPrice; NewPrice = newPrice; } }
然後為事件定義委托,必須滿足以下條件:
-
必須是 void 返回類型;
-
必須有兩個參數,且第一個是object類型,第二個是EventArgs類型(的子類);
-
它的名稱必須以EventHandler結尾。
由於考慮到每個事件都要定義自己的委托很麻煩,.NET 框架為我們預定義好一個通用委托System.EventHandler<TEventArgs>:
public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;
如果不使用框架的EventHandler<TEventArgs>,我們需要自己定義一個:
public delegate void PriceChangedEventHandler (object sender, PriceChangedEventArgs e);
如果不需要參數,可以直接使用EventHandler(不需要<TEventArgs>)。有了EventHandler<TEventArgs>,我們就可以這樣定義示例中的事件:
public class IPhone6 { ... public event EventHandler<PriceChangedEventArgs> PriceChanged; ... }
最後,事件標準模式還需要寫一個受保護的虛方法來觸發事件,這個方法必須以On為首碼,加上事件名(PriceChanged),還要接受一個EventArgs參數,如下:
public class IPhone6 { ... public event EventHandler<PriceChangedEventArgs> PriceChanged; protected virtual void OnPriceChanged(PriceChangedEventArgs e) { if (PriceChanged != null) PriceChanged(this, e); } ... }
下麵給出完整示例:
public class PriceChangedEventArgs : System.EventArgs { public readonly decimal OldPrice; public readonly decimal NewPrice; public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) { OldPrice = oldPrice; NewPrice = newPrice; } }public class IPhone6 { decimal price; public event EventHandler<PriceChangedEventArgs> PriceChanged; protected virtual void OnPriceChanged(PriceChangedEventArgs e) { if (PriceChanged != null) PriceChanged(this, e); } public decimal Price { get { return price; } set { if (price == value) return; decimal oldPrice = price; price = value; // 如果調用列表不為空,則觸發。 if (PriceChanged != null) OnPriceChanged(new PriceChangedEventArgs(oldPrice, price)); } } }class Program { static void Main() { IPhone6 iphone6 = new IPhone6() { Price = 5288M }; // 訂閱事件 iphone6.PriceChanged +=iphone6_PriceChanged; // 調整價格(事件發生) iphone6.Price = 3999; Console.ReadKey(); } static void iphone6_PriceChanged(object sender, PriceChangedEventArgs e) { Console.WriteLine("年終大促銷,iPhone 6 只賣 " + e.NewPrice + " 元, 原價 " + e.OldPrice + " 元,快來搶!"); } }
運行結果:
結尾
委托和事件的知識比較多,所以我單獨寫成一篇。由於委托和事件將來會經常用到(尤其是委托),所以建議大家一定要撐握,用到的時候能夠自己寫得出來。
有些人可能會比較急,希望我直接寫ASP.NET MVC的示例。如果是這樣,你永遠成為不了大牛,將來開發的時候你只會Google找答案找例子。
既然我開始寫大牛系列,那麼我就有責任指引那些認真閱讀和理解並加以實踐的人走向大牛之路。你不認真沒能成為大牛是你自己的錯,你認真了直到本系列文章結束也沒能成為大牛那就是我的錯。