不像Windows API中使用C語言風格的函式指標這種不安全的方式進行回撥。.Net中此功能使用使用更為安全和麵向物件的委託(delegate)來完成。委託是一個型別安全的物件,它指向程式中另一個以後會被呼叫的方法(或多個方法)。
委託型別包含3個重要資訊:
-
它所呼叫的方法的名稱
-
該方法的引數(可選)
-
該方法的返回值(可選)
當上述資訊被提供後,委託可以在執行時動態呼叫其指向的方法。很重要的一點:.Net中每個委託都被自動賦予同步或非同步訪問方法的能力。
定義委託
在C#中使用delegate關鍵字建立一個委託。我們稱這種類為委託類。委託類的例項成為委託物件。從概念上說,委託物件是一種指向一個或多個方法(靜態或非靜態)的引用。要求是此委託匹配它指向的方法的簽名。
如下委託可以指向一任何傳入兩個整數返回一個整數的方法。
1 |
public delegate int BinaryOp(int x, int y); |
-
定義委託後,系統生成一個派生自MulticastDelegate類的密封類。此類中有3個方法:
-
Invoke()方法,用來以同步方式呼叫委託維護的每個方法。(不能在C#中顯示呼叫此方法,Invoke()在後臺被呼叫)
-
BeginInvoke()與EndInvoke()方法在第二個執行緒上非同步呼叫當前方法。
開發人員建立第二個執行執行緒的原因呼叫比較耗時的方法。(相當於委託順帶實現了一些System.Threading名稱空間管理的執行緒問題)
這個委託的密封類大概如下:
1 2 3 4 5 6 7 |
sealed class BinaryOp : System.MulticastDelegate { public BinaryOp(object target, uint functionAddress); public int Invoke(int x, int y); public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state); public int EndInvoke(IAsyncResult result); } |
下面給一個簡單的委託示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 這個委託指向任何一個傳入兩個整數並返回一個整數的方法 public delegate int BinaryOp(int x, int y); #region SimpleMath class public class SimpleMath { public int Add(int x, int y) { return x + y; } public int Subtract(int x, int y) { return x - y; } public static int SquareNumber(int a) { return a * a; } } #endregion class Program { static void Main(string[] args) { Console.WriteLine("***** Simple Delegate Example *****n"); // 建立一個指向SimpleMath.Add()方法的BinaryOp物件 SimpleMath m = new SimpleMath(); BinaryOp b = new BinaryOp(m.Add); // 使用委託呼叫呼叫Add()方法 // 此處也是Invoke()被呼叫的位置 Console.WriteLine("n10 + 10 is {0}", b(10, 10)); Console.ReadLine(); } } |
委託型別安全的體現
如果傳入一個與委託宣告不匹配的方法,將在編譯時報錯。如上例中如果傳入int SquareNumber(int),將會導致一個編譯時錯誤。
獲取委託中呼叫函式列表的方法,示例:
假設有名為delObj的委託物件,使用如下方式得到呼叫函式的資訊
1 2 3 |
Delegate d in delObj.GetInvocationList() Console.WriteLine("Method Name: {0}", d.Method); Console.WriteLine("Target Name: {0}", d.Target); |
Method屬性表示呼叫的函式的簽名,Target表示呼叫的函式所在的物件的型別名,所以如果委託呼叫的是一個靜態方法則Target不會有任何顯示,只有當委託呼叫的是一個例項方法時,Target屬性才有值。
更完整的委託應用(示例來自C#與.Net3.0高階程式設計),程式碼:
汽車類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
using System; using System.Collections.Generic; using System.Text; namespace CarDelegate { public class Car { // 定義委託型別 public delegate void AboutToBlow(string msg); public delegate void Exploded (string msg); // 定義各自委託型別的物件 private AboutToBlow almostDeadList; private Exploded explodedList; // 將成員新增到呼叫列表 public void OnAboutToBlow(AboutToBlow clientMethod) { almostDeadList += clientMethod; } public void OnExploded(Exploded clientMethod) { explodedList += clientMethod; } // 由呼叫列表移除方法 public void RemoveAboutToBlow(AboutToBlow clientMethod) { almostDeadList -= clientMethod; } public void RemoveExploded(Exploded clientMethod) { explodedList -= clientMethod; } // 內部狀態成員 private int currSpeed; private int maxSpeed; private string petName; // 汽車壞了嗎? bool carIsDead; public Car() { maxSpeed = 100; } public Car(string name, int max, int curr) { currSpeed = curr; maxSpeed = max; petName = name; } public void SpeedUp(int delta) { // 如果汽車壞了,觸發Exploded事件 if (carIsDead) { if (explodedList != null) explodedList("Sorry, this car is dead"); } else { currSpeed += delta; // 幾乎要壞了? if (10 == maxSpeed - currSpeed && almostDeadList != null) { almostDeadList("Careful buddy! Gonna blow!"); } // 還好! if (currSpeed >= maxSpeed) carIsDead = true; else Console.WriteLine("->CurrSpeed = {0}", currSpeed); } } } } |
主函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
namespace CarDelegate { class Program { static void Main(string[] args) { // 製造一輛車 Car c1 = new Car("SlugBug", 100, 10); // 註冊事件處理函式 Car.Exploded d = new Car.Exploded(CarExploded); c1.OnAboutToBlow(new Car.AboutToBlow(CarIsAlmostDoomed)); c1.OnAboutToBlow(new Car.AboutToBlow(CarAboutToBlow)); c1.OnExploded(d); // 加速 (這將觸發事件) Console.WriteLine("n***** 加速 *****"); for (int i = 0; i < 6; i++) c1.SpeedUp(20); // 由呼叫列表移除CarExploded方法 c1.RemoveExploded(d); Console.WriteLine("n***** 加速 *****"); for (int i = 0; i < 6; i++) c1.SpeedUp(20); Console.ReadLine(); } public static void CarAboutToBlow(string msg) { Console.WriteLine(msg); } public static void CarIsAlmostDoomed(string msg) { Console.WriteLine("Critical Message from Car: {0}", msg); } public static void CarExploded(string msg) { Console.WriteLine(msg); } } } |
對多路廣播的支援
.Net委託內建多路,即一個委託可以維護一個可呼叫方法的列表而不只是單獨一個方法,使用過載過的+=運算子可以向一個委託物件新增多個方法。關於對對路廣播的支援可以參考上述示例。
在多路廣播的支援中有一個需要注意的問題,一個委託呼叫的多個方法需要無引數且無返回值,因為在呼叫委託時,即使傳入了引數也不知道具體應該傳給哪一個方法,即使這些方法有返回值也不知道該接受那個函式的返回值。所以說直接不要呼叫有引數及返回值的方法,這點與事件關聯多個事件處理方法時對處理方法簽名的要求相同(可以參見本系列介紹事件的文章)。
注意:我們可以用呼叫方法的語法”呼叫”委託物件。這樣會呼叫委託物件所引用的方法。(事件的觸發與委託的呼叫相同,本來事件就是一個委託型別的物件)。這些方法的呼叫是在呼叫委託的方法所在的執行緒中完成的。這種呼叫稱同步呼叫。
C#2.0編譯器的委託類推測功能
C#編譯器引入了在建立委託變數時可以推測其型別的能力。這樣就可以將一個方法賦給隱式建立的委託物件。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Program { delegate void Deleg1(); delegate string Deleg2( string s ); static void f1() { System.Console.WriteLine("f1() called."); } static string f2(string s) { string _s=string.Format( "f2() called with the param "{0}"." , s ); System.Console.WriteLine( _s ); return _s; } public static void Main() { Deleg1 d1 = f1; // 代替 Deleg1 d1 = new Deleg1( f1 ); d1(); Deleg2 d2 = f2; // 代替 Deleg2 d2 = new Deleg2( f2 ); string s = d2("hello"); } } |
委託協變(covariance)
允許建立一個委託,其返回的物件的型別是繼承關係的,示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// 簡單的整合關係的兩個類 class Car { public override string ToString() { return "A stateless car"; } } class SportsCar : Car { public override string ToString() { return "A stateless sports car"; } } class Program { // 定義一個返回Car或SportsCar的委託 public delegate Car ObtainVehicalDelegate(); // 委託指向目標 public static Car GetBasicCar() { return new Car(); } public static SportsCar GetSportsCar() { return new SportsCar(); } static void Main(string[] args) { ObtainVehicalDelegate targetA = new ObtainVehicalDelegate(GetBasicCar); Car c = targetA(); Console.WriteLine(c); // 協變允許指定這樣的目標方法 ObtainVehicalDelegate targetB = new ObtainVehicalDelegate(GetSportsCar); SportsCar sc = (SportsCar)targetB(); Console.WriteLine(sc); Console.ReadLine(); } } |
委託逆變,其中引數具有整合關係,委託簽名的引數型別(派生型別)比方法具有的引數型別(基型別)更具體。定義一個引數型別是派生型別的委託,這個委託可以接收具有基型別引數的方法,因為派生型別隱式轉換成了基型別。注意此方法必須接收與委託簽名相同的引數型別(派生型別),雖然方法的簽名中引數是基型別。
接下來說一下委託在多執行緒程式中的應用,主角有兩個:ThreadStart和ParameterizedThreadStart,它們都定義與System.Threading名稱空間下。
使用這兩個委託,你可以以程式設計方式建立此執行緒來分擔一些任務,步驟如下:
-
建立一個方法作為新執行緒的入口點。
-
建立一個ParameterizedThreadStart(或ThreadStart)委託,並把之前所定義的方法傳給委託的建構函式。
-
建立一個Thread物件,並把ParameterizedThreadStart或ThreadStart委託作為建構函式的引數。
-
建立任意初始化執行緒的特性(名稱、優先順序等)。
-
呼叫Thread.Start()方法。
完成上述步驟,在第2步建立的委託所指向的方法將線上程中儘快開始執行。
ThreadStart委託指向一個沒有引數、無返回值的方法,它在呼叫一個被設計用來僅僅在後臺執行、而沒有更多的互動時非常有用。它的侷限在於無法給這個函式出入引數,所以在.Net2.0中出現了ParameterizedThreadStart了方法,它可以接受一個包含了任意個數的引數(傳給它要呼叫的方法的)的Object型別物件做引數(即允許使用者為新執行緒要執行的方法傳入一個物件作為引數)。但注意這兩種委託指向的函式的返回值都必須是void。
看看示例程式碼,首先是ThreadStart委託的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Printer { public void PrintNumbers() { //具體實現省略 } } class Program { static void Main(string[] args) { Printer p = new Printer(); Thread bgroundThread = new Thread(new ThreadStart(p.PrintNumbers)); // 控制此執行緒是否在後臺執行? bgroundThread.IsBackground = true; bgroundThread.Start(); } } |
接下來的程式碼示例了ParameterizedThreadStart委託的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//包裝引數的類 class AddParams { public int a; public int b; public AddParams(int numb1, int numb2) { a = numb1; b = numb2; } } class Program { static void Main(string[] args) { Console.WriteLine("主執行緒ID:{0}", Thread.CurrentThread.GetHashCode()); //生成要傳入的引數 AddParams ap = new AddParams(10, 10); Thread t = new Thread(new ParameterizedThreadStart(Add)); t.Start(ap); } //ParameterizedThreadStart委託呼叫的方法 //使用AddParams類物件做引數 public static void Add(object data) { if (data is AddParams) { Console.WriteLine("後臺執行緒ID: {0}", Thread.CurrentThread.GetHashCode()); AddParams ap = (AddParams)data; Console.WriteLine("{0} + {1} is {2}", ap.a, ap.b, ap.a + ap.b); } } } |
詳細通過上面兩段簡單的程式碼示例,你已經對ThreadStart和ParameterizedThreadStart的使用有了全面的瞭解。
另外有一點需要說的,有些情況下可以省略這個委託物件的構造,即構造Thread物件時,直接向Thread的建構函式傳入一個方法的名稱,而不用先構造一個委託的物件。傳入的方法既可以是靜態方法也可以是例項方法。
另外委託在非同步程式設計中的作用見非同步程式設計的文章
參考資料:
C#與.Net3.0高階程式設計
C#與.Net2.0實戰
CLR via C# 第二版