說實話現在已經是c#5.0的時代,c#6很快也要出來了,委托作為一個c#1就有的性質,已經早就被更高級的工具例如泛型委托,lambda表達式包裝起來了,基本上已經很少有人會在程序中聲明一個delegate。不過,了解一下基礎也是很好的,
基本概念
委托是一個特殊的類(密封類),可以被視為函數指針,其代表一類和委托簽名的輸入輸出變量類型和個數相同的方法。委托本身可以作為變量傳入方法。
借用經典的greetPeople例子,在實際工作中,總會遇到類似的情況,即通過switch來對不同的輸入執行不同的結果。但我們看到,其實每個switch執行的方法都很類似,方法的簽名還完全相同。此時我們很容易想到的就是當再加入一個新的switch case的時候,我們除了要加一個新方法之外,還要對現成的GreetPeople方法進行修改,這違反了開閉原則(對修改關閉)。有沒有一種方法,可以在不修改GreetPeople方法的前提下對程序進行擴展呢?
public class Program { public static void Main() { GreetPeople("Alex", "Chinese"); GreetPeople("Beta", "English"); GreetPeople("Clara", "France"); Console.ReadKey(); } public static void GreetPeople(string name, string lang) { switch (lang) { case "English": EnglishGreeting(name); break; case "Chinese": ChineseGreeting(name); break; case "France": FrenchGreeting(name); break; } } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
首先,我們要放棄使用switch,否則我們終究避免不了修改GreetPeople方法的命運。之後,我們自然而然的會想,假設我們在主函數裡面傳入的第二變量不是字符串,而是方法名,那麼似乎我們就不需要那個switch了。因為我們會直接去到對應的方法,不用switch再分派過去。那麼這件事該怎麼實現呢?傳入方法名到底意味著什麼呢?這些方法的簽名全都一樣,我是否可以用某種手法將他們封裝起來呢?
於是,委托就出現了,它可以解決上面我們所有的問題。委托代表了一類具有相同簽名的方法,可以變身為其中任何一個。委托也可以作為變量傳入方法,其行為和其他類型例如int,string完全一樣。很多人覺得委托很不好理解,是因為委托代表的是方法,而普通類型代表的都是值或者對象。比如string,其可以代表任何的字符串,int也是可以代表在某個取值范圍中任何的整數一樣。委托則代表著某一類方法(視其定義而定),當某個函數的其中一個變量是委托時,意味著我們將要傳入一個可以被該委托所代表的方法名。委托是方法的指針,可以指向不同的方法,類比一下,如同string可以指向堆上的字符串,int可以指向棧上的整數一樣。
public class Program { //現在這個委托代表了一類輸入一個字符串,沒有輸出的方法 public delegate void GreetPeopleDelegate(string name); public static void Main() { //利用委托,傳入不同的方法會得到不同的結果 GreetPeople("Alex", ChineseGreeting); GreetPeople("Beta", EnglishGreeting); GreetPeople("Clara", FrenchGreeting); Console.ReadKey(); } //委托可以作為方法的變量,從而代替switch public static void GreetPeople(string name, GreetPeopleDelegate aGreetPeopleDelegate) { aGreetPeopleDelegate(name); } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
委托的方法和屬性
1. MulticastDelegate(委托自己所在的密封類)
小寫的delegate是你用來聲明委托的關鍵字,當你聲明完之後,編譯器創建一個新的密封類,該類的類型是MulticastDelegate(繼承自System.MultipleDelegate,其再繼承自System.Delegate)這就是大寫的和小寫d的delegate關鍵字的區別。
這個新的密封類定義了三個方法,invoke, begininvoke和endinvoke。invoke是當你調用委托所代表的方法時隱式執行的,例如aGreetPeopleDelegate(name)實際上和aGreetPeopleDelegate.Invoke(name)沒有區別。所以Invoke的方法簽名永遠和委托本身相同,即如果某委托簽名為int a(int x, int y)則它的invoke簽名一定是public int Invoke(int x, int y)。
後兩者則賦予委托異步的能力。這兩個方法放到多線程系列中進行分析。
2. System.MultipleDelegate和委托的調用列表(方法鏈)
System.MultipleDelegate中重要的方法GetInvocationList()獲得當前委托所代表的方法的各種信息。注意這個方法返回的是一個數組,這也就是說,委托可以同時代表多個方法(此時,invoke委托會將該組方法順序一個一個執行),這也叫做委托的多路廣播。通過+=和-=,我們可以為委托增加和減少方法。我們無需深入研究方法鏈是如何實現的,但以下幾個事情需要知道:
1. 可以重復增加相同的方法,此時該方法將執行兩次
2. 可以刪除委托所有的方法,即委托可以暫時不代表方法,此時invoke委托將什麼都不發生
3. 即使不小心多刪除了方法一次,也不會出現異常(如增加了一個方法然後誤刪除了兩次),此時委托暫時不代表任何方法
4. +=和-=是操作符的重載,本質是調用System.Delegate中的Combine和Remove方法
System.MultipleDelegate還重載了==和!=,判斷兩個委托是否相等僅僅看它們代表的方法鏈是否相等(即都是指向相同對象上的相同方法)。
3. System.Delegate
System.Delegate中有兩個重要的公共成員target和method。其中method代表方法的信息,而如果Method代表一個靜態成員,則Target為null,否則,target代表方法所在的對象。通過GetInvocationList()我們可以查看當前委托中方法鏈的信息。另外這個類還有Combine和Remove方法,其已經被子類重載故不需要直接調用他們。
public class Program { public delegate void GreetPeopleDelegate(string name); public static void Main() { //實例化委托一定要為其指派一個符合要求的方法 GreetPeopleDelegate aGreetPeopleDelegate = new GreetPeopleDelegate(ChineseGreeting); PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); //增加一個方法 aGreetPeopleDelegate += EnglishGreeting; PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); anotherClass a = new anotherClass(); //增加一個非靜態方法 aGreetPeopleDelegate += a.NonStaticGreeting; PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); Console.ReadKey(); } //觀看當前委托中代表的方法鏈 public static void PrintInvocationList(Delegate[] aList) { foreach (var delegateMethod in aList) { //Method代表當前維護的方法的詳細信息 //如果Method代表一個靜態成員,則Target為null,否則,target代表方法所在的對象 Console.WriteLine(string.Format("Method name: {0}, value: {1}", delegateMethod.Method, delegateMethod.Target)); } Console.WriteLine("------------------------------------"); } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } } public class anotherClass { public void NonStaticGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
動態維護委托的調用列表
上面說了委托都是有一個調用列表的,我們可以動態的操作他,為他添加或者刪除成員。如果我們創建一個公共的委托成員列表,則可以很容易的實現多路廣播。下面例子來自精通c#第六版。其中調用列表
public CarEngineHandler methodList;
是公共的,並且外部方法main會創建一個新的實例作為訂閱者,在適當情形下,調用委托然後執行委托列表中的方法。
public class Program { public static void Main() { //創建了一個新的訂閱者 var c = new Car("Mycar", 0, 100); //該訂閱者(消費者)訂閱了方法OnCarEvent1 c.methodList += OnCarEvent1; //取消注釋實現多路廣播,此時將會執行兩個方法 //c.methodList += OnCarEvent2; for (int i = 0; i < 10; i++) { c.Accel(20); } Console.ReadKey(); } public static void OnCarEvent1(string msg) { Console.WriteLine("***** message from car *****"); Console.WriteLine("=> " + msg); Console.WriteLine("****************************"); } public static void OnCarEvent2(string msg) { Console.WriteLine("=> " + msg.ToUpper()); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } public delegate void CarEngineHandler(string message); public CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡時執行訂閱列表中的方法 if (isDead) { if (methodList != null) methodList("Sorry, car is broken"); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } }
從委托到事件
上個例子中的委托有一個問題,就是其不夠安全。調用者可以直接訪問委托對象CarEngineHandler,並且還能對其調用列表:
1 invoke,即可以隨時使用委托
2 +=或者-=,甚至直接賦值(=)也可以
有時候,我們並不希望用戶可以更改委托的成員。而且,我們希望委托不能被用戶Invoke,而是在特定的時候被委托的訂閱者調用。也就是說我們希望下面兩句代碼都不通過編譯:
//為委托賦以一個全新的對象(我們不希望其他代碼可以改變委托指向) c.methodList = OnCarEvent1; //直接調用委托(我們不希望其他代碼可以直接調用,除非經過許可) c.methodList.Invoke("test");
此時,一個自然的想法就是將委托本身定義為private,但如果這樣做,外部的所有類都無法使用該委托。所以我們還要搞若干公共的方法,作為外部類使用內部私有委托的橋梁。下面代碼中,methodList是私有的所以我們不能直接對他操作,我們要通過Car類的兩個公共方法操作他。(無關的代碼已省略)
public class Program { public static void Main() { //創建了一個新的訂閱者 var c = new Car("Mycar", 0, 100);
c.Addmethod(OnCarEvent1); c.Invoke("test"); } public class Car {public delegate void CarEngineHandler(string message); private CarEngineHandler methodList; public CarEngineHandler Addmethod(CarEngineHandler aMethod) { methodList += aMethod; return methodList; } public void Invoke(string msg) { methodList.Invoke(msg); } }
但問題就來了,那對於所有的委托,如果我們要追求安全,豈不是都要弄這些方法,而且方法還比較多,有添加方法,刪除方法,方法的同步和異步的調用等。這看上去非常麻煩,要打很多的代碼。相信這時候你也想到了,又有一個強大的東西要出場了,它可以解決上面所有的問題,它就是事件。