不像Windows API中使用C语言风格的函数指针这种不安全的方式进行回调。.Net中此功能使用使用更为安全和面向对象的委托(delegate)来完成。委托是一个类型安全的对象,它指向程序中另一个以后会被调用的方法(或多个方法)。 委托类型包含3个重要信息: 它所调用的方法的名称 该方法的参数(可选) 该方法的返回值(可选)
当上述信息被提供后,委托可以在运行时动态调用其指向的方法。很重要的一点:.Net中每个委托都被自动赋予同步或异步访问方法的能力。 定义委托 在C#中使用delegate关键字创建一个委托。我们称这种类为委托类。委托类的实例成为委托对象。从概念上说,委托对象是一种指向一个或多个方法(静态或非静态)的引用。要求是此委托匹配它指向的方法的签名。 如下委托可以指向一任何传入两个整数返回一个整数的方法。 | public delegate int BinaryOp(int x, int y); | 定义委托后,系统生成一个派生自MulticastDelegate类的密封类。此类中有3个方法: Invoke()方法,用来以同步方式调用委托维护的每个方法。(不能在C#中显示调用此方法,Invoke()在后台被调用) BeginInvoke()与EndInvoke()方法在第二个线程上异步调用当前方法。
开发人员创建第二个执行线程的原因调用比较耗时的方法。(相当于委托顺带实现了一些System.Threading命名空间管理的线程问题) 这个委托的密封类大概如下: | 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的委托对象,使用如下方式得到调用函数的信息 | 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# 第二版
|