《无私分享(C#高级编程第6版doc)第07章 委托和事件.doc》由会员分享,可在线阅读,更多相关《无私分享(C#高级编程第6版doc)第07章 委托和事件.doc(23页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、目 录第7章 委托和事件27.1 委托27.1.1 在C#中声明委托27.1.2 在C#中使用委托37.1.3 简单的委托示例67.1.4 BubbleSorter示例77.1.5 多播委托107.1.6 匿名方法137.1.7 l表达式147.1.8 协变和抗变167.2 事件177.2.1 从接收器的角度讨论事件177.2.2 生成事件197.3 小结23第7章 委托和事件回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C+编程背景,应该就曾在许多Windows API中使用过回调。Visual Basic添加了AddressOf关键字后,开发人员就可以利用以
2、前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。这说明,C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了。如本章所述,.NET把委托作为一种类型安全的操作。本章后面将学习.NET如何将委托用作实现事件的方式。本章的主要内容如下:委托匿名方法 表达式事件7.1 委托当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码:int i = int.Parse(
3、99);我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法,这听起来很令人迷惑,下面用几个示例来说明:启动线程- 在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该
4、序列。必须为计算机提供开始执行的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了要由线程调用的方法。通用库类- 有许多库包含执行各种标准任务的代码。这些库通常可以自我包含。这样在编写库时,就会知道任务该如何执行。但是有时在任务中还包含子任务,只有使用该库的客户机代码才知道如何执行这些子任务。例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,就无法提前告诉计算机应如何比较对象。处理类中对象数组的客户机代码也必须告诉类如何比较要排序的对象。换言之,
5、客户机代码必须给类传递某个可以进行这种比较的合适方法的细节。事件- 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。这些将在本章后面讨论。在C和C+中,只能提取函数的地址,并传送为一个参数。C是没有类型安全性的。可以把任何函数传送给需要函数指针的方法。这种直接的方法会导致一些问题,例如类型的安全性,在进行面向对象编程时,方法很少是孤立存在的,在调用前,通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方
6、法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的地址。7.1.1 在C#中声明委托在C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。定义委托的语法如下:delegate void IntMethodInvoker(int x
7、);在这个示例中,定义了一个委托IntMethodInvoker,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个int参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所代表的方法签名和返回类型等全部细节。提示:理解委托的一种好方式是把委托当作给方法签名和返回类型指定名称。假定要定义一个委托TwoLongsOp,该委托代表的方法有两个long型参数,返回类型为double。可以编写如下代码:delegate double TwoLongsOp(long first, long second);或者定义一个委托,它代表的方法不带参数,返回一个s
8、tring型的值,则可以编写如下代码:delegate string GetAString();其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、private、protected等:public delegate string GetAString();注意:实际上,定义一个委托是指定义一个新类。委托实现为派生自基类System. M
9、ulticast Delegate的类,System.MulticastDelegate又派生自基类System.Delegate。C#编译器知道这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况,这是C#与基类共同合作,使编程更易完成的另一个示例。定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。注意:此处,在术语方面有一个问题。类有两个不同的术语:类表示较广义的定义,对象表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。必须从上下文中确定委托的确切含义。7.1.2 在C#中使用委托下面的代码段说明了如何使用委托。这是在int上调用T
10、oString()方法的一种相当冗长的方式:private delegate string GetAString();static void Main()int x = 40;GetAString firstStringMethod = new GetAString(x.ToString);Console.WriteLine(String is 0 + firstStringMethod();/ With firstStringMethod initialized to x.ToString(), / the above statement is equivalent to saying /
11、Console.WriteLine(String is 0 + x.ToString(); 在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数、返回一个字符串的方法来初始化firstStringMethod变量,就会产生一个编译错误。注意,int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确初始化委托。下一行代码使用这个委托来显示字
12、符串。在任何代码中,都应提供委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释语句中的代码行。实际上,给委托实例提供括号与调用委托类的Invoke()方法完全相同。firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()。firstStringMethod();firstStringMethod. Invoke();C# 2.0使用委托推断扩展了委托的语法。为了减少输入量,只要需要委托实例,就
13、可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。下面的示例用GetAString委托的一个新实例初始化了GetAString类型的变量firstStringMethod:GetAString firstStringMethod = new GetAString(x.ToString);只要用变量x把方法名传送给变量firstStringMethod,就可以编写出作用相同的代码:GetAString firstStringMethod = x.ToString;C#编译器创建的代码是一样的。编译器会用firstStringMethod检测需
14、要的委托类型,因此创建GetAString委托类型的一个实例,用对象x把方法的地址传送给构造函数。注意:不能调用x.ToString()方法,把它传送给委托变量。调用x.ToString()方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但有趣的是,它们不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法,还是实例方法。提示:给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方
15、法- 只要方法的签名匹配于委托的签名即可。为了说明这一点,我们扩展上面的代码,让它使用firstStringMethod委托在另一个对象上调用其他两个方法,其中一个是实例方法,另一个是静态方法。为此,再次使用本章前面定义的Currency结构。Currency结构有自己的ToString()重载方法和一个与GetCurrencyUnit()的签名相同的静态方法,这样,就可以用同一个委托变量调用这些方法了: struct Currencypublic uint Dollars;public ushort Cents; public Currency(uint dollars, ushort ce
16、nts)this.Dollars = dollars;this.Cents = cents;public override string ToString()return string.Format($0.1,-2:00, Dollars,Cents);public static string GetCurrencyUnit()return Dollar;public static explicit operator Currency (float value)checkeduint dollars =(uint)value;ushort cents =(ushort)(value-dolla
17、rs)*100);return new Currency(dollars,cents);public static implicit operator float (Currency value)return value.Dollars + (value.Cents/100.0f);public static implicit operator Currency (uint value)return new Currency(value, 0);public static implicit operator uint (Currency value)return value.Dollars;下
18、面就可以使用GetAString 实例,代码如下所示: private delegate string GetAString(); static void Main()int x = 40;GetAString firstStringMethod = x.ToString;Console.WriteLine(String is 0 + firstStringMethod(); Currency balance = new Currency(34, 50);/ firstStringMethod references an instance methodfirstStringMethod = b
19、alance.ToString;Console.WriteLine(String is 0 + firstStringMethod();/ firstStringMethod references a static methodfirstStringMethod = new GetAString(Currency.GetCurrencyUnit);Console.WriteLine(String is 0 + firstStringMethod();这段代码说明了如何通过委托来调用方法,然后重新给委托指定在类的不同实例上执行的不同方法,甚至可以指定静态方法,或者在类的不同类型的实例上执行的方法
20、,只要每个方法的签名匹配委托定义即可。运行应用程序,会得到委托引用的不同方法的结果:String is 40String is $34.50String is Dollar但是,我们还没有说明把一个委托传递给另一个方法的具体过程,也没有给出任何有用的结果。调用int和Currency对象的ToString()的方法要比使用委托直观得多!在真正领会到委托的用处前,需要用一个相当复杂的示例来说明委托的本质。下面就是两个委托的示例。第一个示例仅使用委托来调用两个不同的操作,说明了如何把委托传递给方法,如何使用委托数组,但这仍没有很好地说明:没有委托,就不能完成很多工作。第二个示例就复杂得多了,它有一
21、个类BubbleSorter,执行一个方法,按照升序排列一个对象数组,这个类没有委托是很难编写出来的。7.1.3 简单的委托示例在这个示例中,定义一个类MathsOperations,它有两个静态方法,对double类型的值执行两个操作,然后使用该委托调用这些方法。这个数学类如下所示: class MathsOperationspublic static double MultiplyByTwo(double value)return value*2; public static double Square(double value)return value*value;下面调用这些方法:us
22、ing System;namespace Wrox.ProCSharp.Delegatesdelegate double DoubleOp(double x); class Programstatic void Main()DoubleOp operations = MathsOperations.MultiplyByTwo,MathsOperations.Square; for (int i=0 ; ioperations.Length ; i+)Console.WriteLine(Using operations0:, i);ProcessAndDisplayNumber(operatio
23、nsi, 2.0);ProcessAndDisplayNumber(operationsi, 7.94);ProcessAndDisplayNumber(operationsi, 1.414);Console.WriteLine(); static void ProcessAndDisplayNumber(DoubleOp action, double value)double result = action(value);Console.WriteLine(Value is 0, result of operation is 1, value, result);在这段代码中,实例化了一个委托
24、数组DoubleOp (记住,一旦定义了委托类,就可以实例化它的实例,就像处理一般的类那样- 所以把一些委托的实例放在数组中是可以的)。该数组的每个元素都初始化为由MathsOperations类执行的不同操作。然后循环这个数组,把每个操作应用到3个不同的值上。这说明了使用委托的一种方式- 把方法组合到一个数组中,这样就可以在循环中调用不同的方法了。这段代码的关键一行是把委托传递给ProcessAndDisplayNumber()方法,例如: ProcessAndDisplayNumber(operationsi, 2.0);其中传递了委托名,但不带任何参数,假定operationsi是一个委
25、托,其语法是:operationsi表示这个委托。换言之,就是委托代表的方法。operationsi(2.0)表示调用这个方法,参数放在括号中。ProcessAndDisplayNumber()方法定义为把一个委托作为其第一个参数: static void ProcessAndDisplayNumber(DoubleOp action, double value)在这个方法中,调用: double result = action(value);这实际上是调用action委托实例封装的方法,其返回结果存储在result中。运行这个示例,得到如下所示的结果:SimpleDelegateUsing
26、operations0:Value is 2, result of operation is 4Value is 7.94, result of operation is 15.88Value is 1.414, result of operation is 2.828Using operations1:Value is 2, result of operation is 4Value is 7.94, result of operation is 63.0436Value is 1.414, result of operation is 1.9993967.1.4 BubbleSorter示
27、例下面的示例将说明委托的用途。我们要编写一个类BubbleSorter,它执行一个静态方法Sort(),这个方法的第一个参数是一个对象数组,把该数组按照升序重新排列。换言之,假定传递的是int数组:0, 5, 6, 2, 1,则返回的结果应是0, 1, 2, 5, 6。冒泡排序算法非常著名,是一种排序的简单方法。它适合于一小组数字,因为对于大量的数字(超过10个),还有更高效的算法。冒泡排序算法重复遍历数组,比较每一对数字,按照需要交换它们的位置,把最大的数字逐步移动到数组的最后。对于给int排序,进行冒泡排序的方法如下所示: for (int i = 0; i sortArray.Lengt
28、h; i+)for (int j = i + 1; j sortArray.Length; j+)if (sortArrayj sortArrayi) / problem with this testint temp = sortArrayi; / swap ith and jth entriessortArrayi = sortArrayj;sortArrayj = temp;它非常适合于int,但我们希望Sort()方法能给任何对象排序。换言之,如果某段客户机代码包含Currency结构数组或其他类和结构,就需要对该数组排序。这样,上面代码中的if(sortArrayj sortArray
29、i)就有问题了,因为它需要比较数组中的两个对象,看看哪一个更大。可以对int进行这样的比较,但如何对直到运行才知道或确定的新类进行比较?答案是客户机代码知道类在委托中传递的是什么方法,封装这个方法就可以进行比较。定义如下的委托: delegate bool Comparison(object x, object y);给Sort方法指定下述签名: static public void Sort(object sortArray, Comparison comparison)这个方法的文档说明强调,comparison必须表示一个静态方法,该方法带有两个参数,如果第二个参数的值大于第一个参数(换
30、言之,它应放在数组中靠后的位置),就返回true。设置完毕后,下面定义类BubbleSorter: class BubbleSorterstatic public void Sort(object sortArray, Comparison comparison)for (int i=0 ; isortArray.Length ; i+)for (int j=i+1 ; jsortArray.Length ; j+)if (comparison(sortArrayj, sortArrayi)object temp = sortArrayi;sortArrayi = sortArrayj;sor
31、tArrayj = temp;为了使用这个类,需要定义另一个类,建立要排序的数组。在本例中,假定Mortimer Phones移动电话公司有一个员工列表,要对照他们的薪水进行排序。每个员工分别由类Employee的一个实例表示,如下所示: class Employeeprivate string name;private decimal salary; public Employee(string name, decimal salary)this.name = name;this.salary = salary; public override string ToString()return
32、 string.Format(0, 1:C, name, salary); public static bool CompareSalary(object x, object y)Employee e1 = (Employee) x;Employee e2 = (Employee) y;return (e1.salary e2.salary);注意,为了匹配Comparison委托的签名,在这个类中必须定义CompareSalary,它的参数是两个对象引用,而不是Employee引用。必须把这些参数的数据类型强制转换为Employee引用,才能进行比较。注意:这里除了把对象用作参数之外,还可以
33、使用强类型化的泛型。第9章介绍了泛型和泛型委托。下面编写一些客户端代码,完成排序:using System;namespace Wrox.ProCSharp.Delegatesdelegate bool Comparison(object x, object y);class Programstatic void Main()Employee employees =new Employee(Bugs Bunny, 20000), new Employee(Elmer Fudd , 10000),new Employee(Daffy Duck, 25000),new Employee(Wiley
34、 Coyote, (decimal)1000000.38),new Employee(Foghorn Leghorn, 23000),new Employee(RoadRunner, 50000);BubbleSorter.Sort(employees, Employee. CompareSalary);foreach (var employee in employees) Console.WriteLine(employee); 运行这段代码,正确显示按照薪水排列的Employee,如下所示:BubbleSorterElmer Fudd, $10,000.00Bugs Bunny, $20,
35、000.00Foghorn Leghorn, $23,000.00Daffy Duck, $25,000.00RoadRunner, $50,000.00Wiley Coyote, $1,000,000.387.1.5 多播委托前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最后一个方法的结果。下面的代码取自于SimpleDelegate示例。尽管其语法与以前相同,
36、但实际上它实例化了一个多播委托Operations: delegate void DoubleOp(double value);/ delegate double DoubleOp(double value); / cant do this now class MainEntryPointstatic void Main()DoubleOp operations = MathOperations.MultiplyByTwo;operations += MathOperations.Square;在前面的示例中,要存储对两个方法的引用,所以实例化了一个委托数组。而这里只是在一个多播委托中添加两个
37、操作。多播委托可以识别运算符+和+=。还可以扩展上述代码中的最后两行,它们具有相同的效果: DoubleOp operation1 = MathOperations.MultiplyByTwo;DoubleOp operation2 = MathOperations.Square;DoubleOp operations = operation1 + operation2;多播委托还识别运算符-和-=,以从委托中删除方法调用。注意:根据后面的内容,多播委托是一个派生于System.MulticastDelegate的类,System. MulticastDelegate又派生于基类System.
38、Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起,成为一个列表。为了说明多播委托的用法,下面把SimpleDelegate示例改写为一个新示例MulticastDelegate。现在需要把委托表示为返回void的方法,就应重写MathOperations类中的方法,让它们显示其结果,而不是返回它们: class MathOperationspublic static void MultiplyByTwo(double value)double result = value*2;Console.WriteLine(Multiplying b
39、y 2: 0 gives 1, value, result); public static void Square(double value)double result = value*value;Console.WriteLine(Squaring: 0 gives 1, value, result);为了适应这个改变,也必须重写ProcessAndDisplayNumber:static void ProcessAndDisplayNumber(DoubleOp action, double valueToProcess)Console.WriteLine();Console.WriteL
40、ine(ProcessAndDisplayNumber called with value = 0 valueToProcess);action(valueToProcess);下面测试多播委托,其代码如下: static void Main()DoubleOp operations = MathOperations.MultiplyByTwo;operations += MathOperations.Square; ProcessAndDisplayNumber(operations, 2.0);ProcessAndDisplayNumber(operations, 7.94);Proces
41、sAndDisplayNumber(operations, 1.414);Console.WriteLine();现在,每次调用ProcessAndDisplayNumber时,都会显示一个信息,说明它已经被调用。然后,下面的语句会按顺序调用action委托实例中的每个方法: action(value);运行这段代码,得到如下所示的结果:MulticastDelegateProcessAndDisplayNumber called with value = 2Multiplying by 2: 2 gives 4Squaring: 2 gives 4ProcessAndDisplayNumbe
42、r called with value = 7.94Multiplying by 2: 7.94 gives 15.88Squaring: 7.94 gives 63.0436ProcessAndDisplayNumber called with value = 1.414Multiplying by 2: 1.414 gives 2.828Squaring: 1.414 gives 1.999396 如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,因此应避免编写依赖于以特定顺序调用方法的代码。通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果
43、通过委托调用的一个方法抛出了异常,整个迭代就会停止。下面是MulticastIteration示例。其中定义了一个简单的委托DemoDelegate,它没有参数,返回void。这个委托调用方法One()和Two(),这两个方法满足委托的参数和返回类型要求。注意方法One()抛出了一个异常:using System;namespace Wrox.ProCSharp.Delegatespublic delegate void DemoDelegate();class Programstatic void One() Console.WriteLine(One);throw new Exceptio
44、n(Error in one);static void Two()Console.WriteLine(Two);在Main()方法中,创建了委托d1,它引用方法One(),接着把Two()方法的地址添加到同一个委托中。调用d1委托,就可以调用这两个方法。异常在try/catch块中捕获:static void Main()DemoDelegate d1 = One;d1 += Two;tryd1();catch (Exception)Console.WriteLine(Exception caught);委托只调用了第一个方法。第一个方法抛出了异常,所以委托的迭代会停止,不再调用Two()方法
45、。当调用方法的顺序没有指定时,结果会有所不同。OneException Caught注意:错误和异常详见第14章。在这种情况下,为了避免这个问题,应手动迭代方法列表。Delegate类定义了方法GetInvocationList(),它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。static void Main()DemoDelegate d1 = One;d1 += Two;Delegate delegates = d1.GetInvocationList();foreach (DemoDelegate d in delegates)tryd();catch (Exception)Console.WriteLine(Exception caught);修改了代码后运行应用程序,会看到在捕获了异常后,将继续迭代下一个方法。On