第6章 委托和事件 {
private string name; private decimal salary;
public Employee(string name, decimal salary) {
this.name = name; this.salary = salary; }
public override string ToString() {
return string.Format(name + \ }
public static bool RhsIsGreater(object lhs, object rhs) {
Employee empLhs = (Employee) lhs; Employee empRhs = (Employee) rhs;
return (empRhs.salary > empLhs.salary) ? true : false; } }
? 163 ? 注意,为了匹配CompareOp委托的签名,在这个类中必须定义RhsIsGreater,它的参数是两个对象引用,而不是Employee引用。必须把这些参数的数据类型转换为Employee引用,才能进行比较。
下面编写一些客户机代码,完成排序:
using System;
namespace Wrox.ProCSharp.AdvancedCSharp {
delegate bool CompareOp(object lhs, object rhs);
class MainEntryPoint {
static void Main() {
Employee [] employees = {
new Employee(\ new Employee(\ new Employee(\
new Employee(\ new Employee(\
? 164 ? 第Ⅰ部分 C# 语 言 new Employee(\
CompareOp employeeCompareOp = new CompareOp(Employee.RhsIsGreater); BubbleSorter.Sort(employees, employeeCompareOp);
for (int i=0 ; i 运行这段代码,正确显示按照薪水排列的Employee,如下所示: BubbleSorter Elmer Fudd, $10,000.00 Bugs Bunny, $20,000.00 Foghorn Leghorn, $23,000.00 Daffy Duck, $25,000.00 RoadRunner, $50,000.00 Wiley Coyote, $1,000,000.38 6.2.3 多播委托 前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void(否则,返回值应送到何处?)。实际上,如果编译器发现某个委托返回void,就会自动假定这是一个多播委托。下面的代码取自于SimpleDelegate示例,尽管其语法与以前相同,但实际上它实例化了一个多播委托Operations: delegate void DoubleOp(double value); // delegate double DoubleOp(double value); // can't do this now class MainEntryPoint { static void Main() { DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo); operations += new DoubleOp(MathOperations.Square); 在前面的示例中,要存储对两个方法的引用,所以实例化了一个委托数组。而这里只是在一个多播委托中添加两个操作。多播委托可以识别运算符+和+=。还可以扩展上述代码中的最后两行,它们具有相同的效果: DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo); DoubleOp operation2 = new DoubleOp(MathOperations.Square); DoubleOp operations = operation1 + operation2; 第6章 委托和事件 ? 165 ? 多播委托还识别运算符–和–=,以从委托中删除方法调用。 注意: 根据后面的内容,多播委托是一个派生于System.MulticastDelegate的类,System.Multicast- Delegate又派生于基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起,成为一个列表。 为了说明多播委托的用法,下面把SimpleDelegate示例改写为一个新示例MulticastDelegate。现在需要把委托表示为返回void的方法,就应重写MathOperations类中的方法,让它们显示其结果,而不是返回它们: class MathOperations { public static void MultiplyByTwo(double value) { double result = value*2; Console.WriteLine( \ } public static void Square(double value) { double result = value*value; Console.WriteLine(\ } } 为了适应这个改变,也必须重写ProcessAndDisplayNumber: static void ProcessAndDisplayNumber(DoubleOp action, double value) { Console.WriteLine(\ valueToProcess); action(valueToProcess); } 下面测试多播委托,其代码如下: static void Main() { DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo); operations += new DoubleOp(MathOperations.Square); ProcessAndDisplayNumber(operations, 2.0); ProcessAndDisplayNumber(operations, 7.94); ProcessAndDisplayNumber(operations, 1.414); ? 166 ? Console.WriteLine(); } 第Ⅰ部分 C# 语 言 现在,每次调用ProcessAndDisplayNumber时,都会显示一个信息,说明它已经被调用。然后,下面的语句会按顺序调用action委托实例中的每个方法: action(value); 运行这段代码,得到如下所示的结果: MulticastDelegate ProcessAndDisplayNumber called with value = 2 Multiplying by 2: 2 gives 4 Squaring: 2 gives 4 ProcessAndDisplayNumber called with value = 7.94 Multiplying by 2: 7.94 gives 15.88 Squaring: 7.94 gives 63.0436 ProcessAndDisplayNumber called with value = 1.414 Multiplying by 2: 1.414 gives 2.828 Squaring: 1.414 gives 1.999396 如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,因此应避免编写依赖于以任意特定顺序调用方法的代码。 6.3 事件 基于Windows的应用程序也是基于消息的。这说明,应用程序是通过Windows来通信的,Windows又是使用预定义的消息与应用程序通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。在MFC等库或VB等开发环境推出之前,开发人员必须处理Windows发送给应用程序的消息。VB和今天的.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。一个常见的例子是用户单击了窗体中的按钮后,Windows就会给按钮消息处理程序(有时称为Windows过程或WndProc)发送一个WM_MOUSECLICK消息。对于.NET开发人员来说,这就是按钮的OnClick事件。 在开发基于对象的应用程序时,需要使用另一种对象通信方式。在一个对象中发生了有趣的事情时,就需要通知其他对象发生了什么变化。这里又要用到事件。就像.NET Framework把Windows消息封装在事件中那样,也可以把事件用作对象之间的通信介质。 委托就用作应用程序接收到消息时封装事件的方式。 在上一节介绍委托时,仅讨论了理解事件如何工作所需要的内容。但Microsoft设计C#事件的目的是为了让用户无需理解底层的委托,就可以使用它们。所以下面开始从客户软件的角度讨论事件,主要考虑的是需要编写什么代码来接收事件通知,而无需担心后台上究竟发生了 第6章 委托和事件 ? 167 ? 什么,从中可以看出事件的处理十分简单。之后,编写一个生成事件的示例,介绍事件和委托之间的关系。 本节的内容对C++开发人员最有用,因为C++没有与事件类似的概念。另一方面,C#事件与VB事件非常类似,但C#中的语法和底层的实现有所不同。 注意: 这里的术语“事件”有两种不同的含义。第一,表示发生了某个有趣的事情;第二,表示C#语言中已定义的一个对象,即处理通知过程的对象。在使用第二个含义时,我们常常把事件表示为C#事件,或者在其含义很容易从上下文中看出时,它就是一个事件。 6.3.1 从客户的角度讨论事件 事件接收器是指在发生某些事情时被通知的任何应用程序、对象或组件。当然,有事件接收器,就有事件发送器。发送器的作用是引发事件。发送器可以是应用程序中的另一个对象或程序集,在系统事件中,例如鼠标单击或键盘按键,发送器就是.NET运行库。注意,事件的发送器并不知道接收器是谁。这就使事件非常有用。 现在,在事件接收器的某个地方有一个方法,它负责处理事件。在每次发生已注册的事件时,就执行这个事件处理程序。此时就要使用委托了。由于发送器对接收器一无所知,所以无法设置两者之间的引用类型,而是使用委托作为中介。发送器定义接收器要使用的委托,接收器将事件处理程序注册到事件中。连接事件处理程序的过程称为封装事件。封装Click事件的简单例子有助于说明这个过程。 首先创建一个简单的Windows窗体应用程序,把一个按钮控件从工具箱拖放到窗体上。在属性窗口中把按钮重命名为btnOne。在代码编辑器中把下面的代码添加到Form1构造函数中: btnOne.Click += new EventHandler(Button_Click); 在Visual Studio中,注意在输入+=运算符之后,就只需按下Tab键两次,编辑器就会完成剩余的输入工作。在大多数情况下这很不错。但在这个例子中,不使用默认的处理程序名,所以应自己输入文本。 这将告诉运行库,在引发btnOne的Click事件时,应执行Button_Click方法。EventHandler是事件用于把处理程序(Button_Click)赋予事件(Click)的委托。注意使用+=运算符把这个新方法添加到委托列表中。这类似于本章前面介绍的多播示例。也就是说,可以为事件添加多个事件处理程序。由于这是一个多播委托,所以要遵循添加多个方法的所有规则,但是不能保证调用方法的顺序。下面在窗体上再添加一个按钮,把它重命名为btnTwo。把btnTwo的Click事件也连接到同一个Button_Click方法上,如下所示: btnOne.Click += new EventHandler(Button_Click); btnTwo.Click += new EventHandler(Button_Click); EventHandler委托已在.NET Framework中定义了。它位于System命名空间,所有在.NET Framework中定义的事件都使用它。如前所述,委托要求添加到委托列表中的所有方法都必须