1、9.1 委托委托9.2 事件事件9.3 小结小结9.1 委托在在C、C+和和Pascal中,如果把函数的指针(地址)作中,如果把函数的指针(地址)作为参数传递给另一个函数,当该指针被用于调用它所指为参数传递给另一个函数,当该指针被用于调用它所指向的函数时,我们就可以称之为回调函数。向的函数时,我们就可以称之为回调函数。回调函数是一种功能强大的编程特性,窗口过程、异步回调函数是一种功能强大的编程特性,窗口过程、异步过程调用等都需要使用回调函数。过程调用等都需要使用回调函数。但是,函数指针只是一个内存地址,该地址不带任何额但是,函数指针只是一个内存地址,该地址不带任何额外信息,例如函数期望收到的参
2、数个数、参数类型、函外信息,例如函数期望收到的参数个数、参数类型、函数的返回值类型以及函数的调用约定等,所以函数指针数的返回值类型以及函数的调用约定等,所以函数指针是非类型安全的。因此,为了保证程序的安全性,是非类型安全的。因此,为了保证程序的安全性,Java不提供任何具有指针函数功能的结构,但是不提供任何具有指针函数功能的结构,但是C#提提供这种结构,这就是类型安全的委托。供这种结构,这就是类型安全的委托。9.1.1 委托概述在在C#中,委托是一种特殊的对象类型(即一种特殊中,委托是一种特殊的对象类型(即一种特殊的类),其特殊之处在于之前定义的对象类型都可的类),其特殊之处在于之前定义的对象
3、类型都可以包含数据,而委托包含的只是方法的地址。也可以包含数据,而委托包含的只是方法的地址。也可以说,委托是对一类方法(参数和返回值类型相似以说,委托是对一类方法(参数和返回值类型相似的方法)的类型。的方法)的类型。和类在使用前要进行实例化一样,委托在使用前也和类在使用前要进行实例化一样,委托在使用前也要进行实例化。但需要注意的是,在术语方面,类要进行实例化。但需要注意的是,在术语方面,类有两个不同的术语,有两个不同的术语,“类类”表示较广义的定义,表示较广义的定义,“对象对象”表示类的实例。但委托只有一个术语,在表示类的实例。但委托只有一个术语,在创建委托的实例时,所创建的委托的实例也称为委
4、创建委托的实例时,所创建的委托的实例也称为委托。托。9.1.1 委托概述(1)确定将要引用方法的签名,声明一个委托类型。)确定将要引用方法的签名,声明一个委托类型。一般语法形式如下一般语法形式如下:访问修饰符访问修饰符 delegate 返回值类型返回值类型 委托类名委托类名 (形参列表形参列表);例如下列代码例如下列代码:public delegate void Del(string message);委托声明在形式上类似于方法的定义,但是没有方法体,在委托声明在形式上类似于方法的定义,但是没有方法体,在返回值类型前需要添加返回值类型前需要添加delegate关键字。关键字。委托声明中的返回
5、值类型、形参类型和形参个数要与被引用委托声明中的返回值类型、形参类型和形参个数要与被引用的方法匹配(不必完全匹配,委托支持协变与逆变,后面章的方法匹配(不必完全匹配,委托支持协变与逆变,后面章节将进行详细介绍)。节将进行详细介绍)。委托的声明实际就是一个类的定义(该类实际定义的是一类委托的声明实际就是一个类的定义(该类实际定义的是一类方法,而且只有方法,没有数据),所以可以在定义类的任方法,而且只有方法,没有数据),所以可以在定义类的任何位置定义委托。何位置定义委托。9.1.1 委托概述(2)实例化一个委托。)实例化一个委托。一般语法形式如下一般语法形式如下:委托类名委托类名 委托名委托名=n
6、ew 委托类名委托类名(被引用的方法名被引用的方法名);或者如下形式或者如下形式:委托类名委托类名 委托名委托名=被引用的方法名;被引用的方法名;例如下列代码。例如下列代码。Del handler =new Del(DelegateMethod);或者如下形式。或者如下形式。Del handler =DelegateMethod;第二种语法是第一种语法的缩写,是第二种语法是第一种语法的缩写,是C#2.0以上版本才支持的语法形式。因为以上版本才支持的语法形式。因为C#2.0支持委托推断,即允许直接为委托实例指派方法名,而不需要先使用委支持委托推断,即允许直接为委托实例指派方法名,而不需要先使用委
7、托对象对其进行包装。托对象对其进行包装。示例代码中的示例代码中的DelegateMehod是一个被引用的具体方法名,不带是一个被引用的具体方法名,不带“()”。因。因为为handler实际是一个对象,对其进行赋值只是将实际是一个对象,对其进行赋值只是将DelegateMethod的内存单的内存单元地址传入元地址传入handler中。有关委托的本质可参见后续章节。中。有关委托的本质可参见后续章节。9.1.1 委托概述(2)实例化一个委托。)实例化一个委托。一般语法形式如下一般语法形式如下:委托类名委托类名 委托名委托名=new 委托类名委托类名(被引用的方法名被引用的方法名);或者如下形式或者如
8、下形式:委托类名委托类名 委托名委托名=被引用的方法名;被引用的方法名;例如下列代码。例如下列代码。Del handler =new Del(DelegateMethod);或者如下形式。或者如下形式。Del handler =DelegateMethod;第二种语法是第一种语法的缩写,是第二种语法是第一种语法的缩写,是C#2.0以上版本才支持的语法形式。因为以上版本才支持的语法形式。因为C#2.0支持委托推断,即允许直接为委托实例指派方法名,而不需要先使用委支持委托推断,即允许直接为委托实例指派方法名,而不需要先使用委托对象对其进行包装。托对象对其进行包装。示例代码中的示例代码中的Deleg
9、ateMehod是一个被引用的具体方法名,不带是一个被引用的具体方法名,不带“()”。因。因为为handler实际是一个对象,对其进行赋值只是将实际是一个对象,对其进行赋值只是将DelegateMethod的内存单的内存单元地址传入元地址传入handler中。有关委托的本质可参见后续章节。中。有关委托的本质可参见后续章节。9.1.1 委托概述(3)委托调用。委托调用的目的就是调用委托所封装)委托调用。委托调用的目的就是调用委托所封装的方法。委托最主要的用途是实现函数回调的方法。委托最主要的用途是实现函数回调.【例例9-1】委托实现函数回调委托实现函数回调 public delegate voi
10、d Del(string message);class Program public static void DelegateMethod(string message)System.Console.WriteLine(message);static void Main(string args)Del handler=DelegateMethod;handler(Hello World);/传入参数传入参数Hello World 9.1.1 委托概述【例例9-2】委托实现函数传递委托实现函数传递 public delegate void Del(string message);class Pr
11、ogram public static void DelegateMethod(string message)System.Console.WriteLine(message);public static void MethodWithCallback(int param1,int param2,Del callback)/Del类委托做参数类委托做参数 callback(The number is:+(param1+param2).ToString();static void Main(string args)Del handler=DelegateMethod;MethodWithCall
12、back(1,2,handler);/委托实现函数传递委托实现函数传递 9.1.1 委托概述【例例9-3】委托引用对象方法委托引用对象方法 public delegate void Del(string message);public class MethodClass public void Method(string message)/非静态方法非静态方法Method System.Console.WriteLine(message);9.1.1 委托概述class Program public static void MethodWithCallback(int param1,int p
13、aram2,Del callback)callback(The number is:+(param1+param2).ToString();static void Main(string args)MethodClass obj=new MethodClass();/MethodClass类的实例类的实例obj Del point=obj.Method;/引用方法时一定要加上对象名引用方法时一定要加上对象名 point(Hello World);MethodWithCallback(1,2,point);9.1.3 匿名方法在在2.0之前的之前的C#版本中,声明委托的惟一方法就是版本中,声明委
14、托的惟一方法就是使用命名方法,即要使用委托传递和回调一个方法,使用命名方法,即要使用委托传递和回调一个方法,该方法必须是已经存在的。该方法必须是已经存在的。C#2.0引入了匿名方法,即允许委托可以只传递代引入了匿名方法,即允许委托可以只传递代码块,使代码块成为委托参数。码块,使代码块成为委托参数。【例例9-4】使用匿名方法。使用匿名方法。class program delegate void Message();delegate int AddOne(int v);static void Main(string args)int y=10;AddOne ao=delegate(int val)
15、val+;return val;9.1.3 匿名方法Console.WriteLine(y=0,y+=1,y,ao(y);Message ms=delegate()Console.WriteLine(Anonymous Method);ms();Console.ReadLine();9.1.3 匿名方法 匿名方法可以带参数,也可以不带参数。当定义带有参数的匿名方法时,匿名方法可以带参数,也可以不带参数。当定义带有参数的匿名方法时,应在关键字应在关键字delegage后面定义参数类型和名称。如同一个常规方法,方后面定义参数类型和名称。如同一个常规方法,方法签名必须与它指派的委托的定义相匹配。当调
16、用委托时,可以传递参法签名必须与它指派的委托的定义相匹配。当调用委托时,可以传递参数的值,与正常的委托调用完全相同。当定义不带参数的匿名方法时,数的值,与正常的委托调用完全相同。当定义不带参数的匿名方法时,可以在关键字可以在关键字deletgate后面添加一对空括号。后面添加一对空括号。如果委托类型的返回值类型为如果委托类型的返回值类型为void,则匿名方法不能有返回值;否则,则匿名方法不能有返回值;否则,匿名方法的返回值必须和委托类型的返回值兼容。对于上述示例代码中匿名方法的返回值必须和委托类型的返回值兼容。对于上述示例代码中定义的两个匿名方法,返回值的类型分别为定义的两个匿名方法,返回值的
17、类型分别为void和和string。与普通的方法定义不同,匿名方法定义结束时,不能省略与普通的方法定义不同,匿名方法定义结束时,不能省略“;”。9.1.3 匿名方法 编译器生成的匿名方法在类中是编译器生成的匿名方法在类中是private可见性的,这会禁止未在类型内可见性的,这会禁止未在类型内部定义的任何访问该方法。部定义的任何访问该方法。因为匿名方法是私有的,所以匿名方法中不能使用跳转语句(因为匿名方法是私有的,所以匿名方法中不能使用跳转语句(goto、break或者或者continue)从代码块内跳转到代码块外部。同样,代码块外)从代码块内跳转到代码块外部。同样,代码块外部的跳转语句也不能跳
18、转到代码块内部。部的跳转语句也不能跳转到代码块内部。匿名方法内部不能访问不安全的代码。匿名方法内部不能访问不安全的代码。不能访问在匿名方法外部的不能访问在匿名方法外部的ref参数和参数和out参数,但是可以使用匿名方法参数,但是可以使用匿名方法访问外部定义的其他变量。访问外部定义的其他变量。is运算符的左侧不允许使用匿名方法。运算符的左侧不允许使用匿名方法。9.1.4 创建多播委托前面介绍的委托,包括匿名方法,都是单链的,即一前面介绍的委托,包括匿名方法,都是单链的,即一个委托对象只能对应一个回调方法。若要通过委托同个委托对象只能对应一个回调方法。若要通过委托同时调用多个方法,即使这些方法的签
19、名都相同,也需时调用多个方法,即使这些方法的签名都相同,也需要分别定义对应的委托对象,然后显式调用这些委托要分别定义对应的委托对象,然后显式调用这些委托对象。事实上,对象。事实上,C#提供了更简捷的方式实现多个方提供了更简捷的方式实现多个方法的同时调用,这种委托就是多播委托(也称作多路法的同时调用,这种委托就是多播委托(也称作多路委托、多路广播或者委托链)。委托、多路广播或者委托链)。我们可以使用加法运算符(我们可以使用加法运算符(+)或者加法赋值运算符)或者加法赋值运算符(+=)向委托的方法列表中添加额外的方法,也可以)向委托的方法列表中添加额外的方法,也可以使用减法运算符(使用减法运算符(
20、-)或者减法赋值运算符()或者减法赋值运算符(-=)从)从委托的方法链表中减少方法。委托的方法链表中减少方法。【例例9-5】多播委托多播委托 class MyClass public void WordHello(string s)System.Console.WriteLine(Hello,0,s);public void WordGoodmorning(string s)System.Console.WriteLine(GoodMorning,0,s);public void WordGoodafternoon(string s)System.Console.WriteLine(Gooda
21、fternoon,0!,s);9.1.4 创建多播委托class Test public delegate void MyDelegate(string s);static void Main(String args)MyClass MyObj=new MyClass();MyDelegate a,b,c,d,e;/定义了定义了5个个MyDelegate类型的委托对象类型的委托对象 a=MyObj.WordHello;b=MyObj.WordGoodmorning;c=MyObj.WordGoodafternoon;d=a+b;/将将a、b合并成合并成d c+=d;/将将d、c合并成新的合并成
22、新的c d(A);c(B);e=c-a;/将将a从从c中删除,中删除,e将只包含将只包含b和和d e(C);Console.ReadLine();9.1.4 创建多播委托关于多播委托,需要注意如下事项。关于多播委托,需要注意如下事项。多播委托的签名需要统一返回多播委托的签名需要统一返回void类型,否则只能类型,否则只能得到委托调用的最后一个方法的结果。得到委托调用的最后一个方法的结果。多播委托的调用方法链顺序并未被正式定义,因此多播委托的调用方法链顺序并未被正式定义,因此需要避免编写依赖于特定顺序调用方法的代码。需要避免编写依赖于特定顺序调用方法的代码。多播委托包含一个逐个调用的委托集合,如
23、果调用多播委托包含一个逐个调用的委托集合,如果调用的方法抛出了异常,整个迭代就会停止。的方法抛出了异常,整个迭代就会停止。9.1.4 创建多播委托9.1.6 手工迭代 前面已经介绍了多播委托的创建和原理。通过创建委托链,前面已经介绍了多播委托的创建和原理。通过创建委托链,能够实现对多个方法的调用。因为委托类型中的能够实现对多个方法的调用。因为委托类型中的Invoke方方法可以实现对法可以实现对_invocationList指向的数组的遍历。但是,指向的数组的遍历。但是,通过分析通过分析Invoke的伪代码可以发现,它使用的是一个非常的伪代码可以发现,它使用的是一个非常简单的算法,尽管该算法足以
24、应付大部分情况,但是它也简单的算法,尽管该算法足以应付大部分情况,但是它也有很大的局限性,表现在如下两个方面。有很大的局限性,表现在如下两个方面。只能支持只能支持void返回值类型签名,否则只能返回最后一个被调用方返回值类型签名,否则只能返回最后一个被调用方法的返回值。法的返回值。如果某个被调用的方法抛出异常,后面的其他方法就不会再被调如果某个被调用的方法抛出异常,后面的其他方法就不会再被调用。用。显然,这两点便显现出了多播委托的不可靠性。为此,显然,这两点便显现出了多播委托的不可靠性。为此,MultcastDelegate类提供了一个实例方法类提供了一个实例方法GetInvocationLi
25、st,它用于显式调用链中的每一个委托,它用于显式调用链中的每一个委托,并使用符合需求的任意算法。并使用符合需求的任意算法。【例例9-6】手工迭代。手工迭代。internal sealed class Light public String SwitchPosition()return The Light is off;internal sealed class Fan public String Speed()throw new InvalidOperationException(The fan broke due to overheating);internal sealed class S
26、peaker public String Volume()return The volume is loud;9.1.6 手工迭代public sealed class program private delegate String GetStatus();/声明一个委托声明一个委托 public static void Main()GetStatus getStatus=null;getStatus+=new GetStatus(new Light().SwitchPosition);getStatus+=new GetStatus(new Fan().Speed);getStatus+=n
27、ew GetStatus(new Speaker().Volume);Console.WriteLine(GetComponentStatusReport(getStatus);private static String GetComponentStatusReport(GetStatus status)if(status=null)return null;StringBuilder report=new StringBuilder();Delegate arrayOfDelegates=status.GetInvocationList();foreach(GetStatus getStatu
28、s in arrayOfDelegates)try report.AppendFormat(011,getStatus(),Environment.NewLine);catch(Exception e)object component=getStatus.Target;report.AppendFormat(Failed to get status from 120Error:300,Environment.NewLine,(component=null)?:component.GetType()+.),getStatus.Method.Name,e.Message);return repor
29、t.ToString();9.1.6 手工迭代 1返回类型协变返回类型协变public class DelegateReturn public class DelegateReturn2:DelegateReturn public delegate DelegateReturn MyDelegate();class Program static void Main()MyDelegate myDelegate=Method;myDelegate();static DelegateReturn2 Method()DelegateReturn2 result=new DelegateReturn2
30、();return result;9.1.7 协变和抗变 2参数类型抗变参数类型抗变public class DelegateParam public class DelegateParam2:DelegateParam public delegate void MyDelegate(DelegateParam2 param2);class Program static void Main()MyDelegate myDelegate=Method;DelegateParam2 param2=new DelegateParam2();myDelegate(param2);static void
31、 Method(DelegateParam param)9.1.7 协变和抗变9.2 事件 事件是事件是C#中一个比较高级的概念,定义了事件成员的类型,允许类型中一个比较高级的概念,定义了事件成员的类型,允许类型(或者类型的实例)在某些特定事情发生时通知其他对象。(或者类型的实例)在某些特定事情发生时通知其他对象。在整个在整个.NET中所定义的事件都是基于委托的。某种意义上讲,事件就是中所定义的事件都是基于委托的。某种意义上讲,事件就是委托的一种特殊形式。委托的一种特殊形式。事件通常包括引发事件的发送方以及捕获事件并做出响应的接收方。在事件通常包括引发事件的发送方以及捕获事件并做出响应的接收方
32、。在事件通信中,事件发送方无法确定哪个对象或方法将接收到(处理)它事件通信中,事件发送方无法确定哪个对象或方法将接收到(处理)它引发的事件,所需要的就是在发送方和接收方之间用一个纽带来联系。引发的事件,所需要的就是在发送方和接收方之间用一个纽带来联系。在在C#中,使用委托作为该纽带。事件的应用极为广泛,尤其是在窗体应中,使用委托作为该纽带。事件的应用极为广泛,尤其是在窗体应用程序中,几乎所有的交互都是通过事件来实现的,例如在登陆窗口中用程序中,几乎所有的交互都是通过事件来实现的,例如在登陆窗口中单击登陆按钮,就是通过在按钮的事件中编写校验用户名和密码的语句单击登陆按钮,就是通过在按钮的事件中编
33、写校验用户名和密码的语句来实现的。来实现的。9.3 小结 C#的委托类似于函数指针,但是比函数指针的安全性更的委托类似于函数指针,但是比函数指针的安全性更高,且函数指针只能引用静态方法,而委托既能引用静高,且函数指针只能引用静态方法,而委托既能引用静态方法,也能引用实例方法。通过使用多播委托或者手态方法,也能引用实例方法。通过使用多播委托或者手工迭代,还可以实现多个方法的回调。工迭代,还可以实现多个方法的回调。事件是事件是C#中的另一个重要概念,事件与处理方法之间的中的另一个重要概念,事件与处理方法之间的桥梁就是委托,事件发生了,委托就会知道,然后将事桥梁就是委托,事件发生了,委托就会知道,然后将事件传递给接收方,接收方通过处理方法进行相应的处理。件传递给接收方,接收方通过处理方法进行相应的处理。委托和事件的联系非常紧密,主要针对高级应用,在理委托和事件的联系非常紧密,主要针对高级应用,在理解上可能会有一定的难度。读者可以先了解它的作用和解上可能会有一定的难度。读者可以先了解它的作用和使用方法,其次研究其实现原理。使用方法,其次研究其实现原理。