第6章 委托和事件

更新时间:2023-12-14 04:56:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

清华大学出版社授权博科购书网www.cdbook.cn独家连载 第6章 委托和事件

回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景,就曾在许多Windows API中使用过回调。VB添加了AddressOf关键字后,开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。这说明,C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了。如本章所述,.NET把委托作为一种类型安全的操作。本章后面将学习.NET如何将委托用作实现事件的方式。

6.1 委托

当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码:

int i = int.Parse(\

我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法,这听起来很令人迷惑,下面用几个示例来说明:

● 启动线程—— 在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为

线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread.Start()方法必须带有一个参数,该参数定义了要由线程调用的方法。

● 通用库类—— 有许多库包含执行各种标准任务的代码。这些库通常可以自我包含。这

样在编写库时,就会知道任务该如何执行。但是有时在任务中还包含子任务,只有使用该库的客户机代码才知道如何执行这些子任务。例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,就无法提前告诉计算机应如何比较对象。处理类中对象数组的客户机代码也必须告诉类如何比较要排序的对象。换言之,客户机代码必须给类传递某个可以进行这种比较的合适方法的细节。

清华大学出版社版权所有,侵权必究

? 154 ? 第Ⅰ部分 C# 语 言 ● 事件—— 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时,

运行库需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。这些将在本章后面讨论。

前面建立了有时把方法的细节作为参数传递给其他方法的规则。下面需要指出如何完成这一过程。最简单的方式是把方法名作为参数传递出去。例如在前面的线程示例中,假定要启动一个新线程,且有一个叫作EntryPoint()的方法,该方法是开始运行线程时的地方。

void EntryPoint() {

// do whatever the new thread needs to do }

也可以用下面的代码开始执行新线程:

Thread NewThread = new Thread();

Thread.Start(EntryPoint); // WRONG

实际上,这是一种很简单的方式,在一些语言如C和C++中使用的就是这种方式(在C和C++中,参数EntryPoint是一个函数指针)。

但这种直接的方法会导致一些问题,例如类型的安全性,在进行面向对象编程时,方法很少是孤立存在的,在调用前,通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的细节。

6.1.1 在C#中声明委托

在C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。

定义委托的语法如下:

delegate void VoidOperation(uint x);

在这个示例中,定义了一个委托VoidOperation,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个uint参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所代表的方法的全部细节。

提示:

理解委托的一种好方式是把委托的作用当作是给方法签名指定名称。

假定要定义一个委托TwoLongsOp ,该委托代表的函数有两个long型参数,返回类型为double。可以编写如下代码:

delegate double TwoLongsOp(long first, long second);

第6章 委托和事件 ? 155 ? 或者定义一个委托,它代表的方法不带参数,返回一个string型的值,则可以编写如下代码:

delegate string GetAString();

其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、 private和 protected等:

public delegate string GetAString();

注意: 实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System. MulticastDelegate的类,System.MulticastDelegate又派生于基类System.Delegate。C#编译器知道这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况,这是C#与基类共同合作,使编程更易完成的另一个示例。

定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。

注意:

此处,在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。您需要从上下文中确定委托的确切含义。

6.1.2 在C#中使用委托

下面的代码段说明了如何使用委托。这是在int上调用ToString()方法的一种相当冗长的方式:

private delegate string GetAString();

static void Main(string[] args) {

int x = 40;

GetAString firstStringMethod = new GetAString(x.ToString); Console.WriteLine(\ // With firstStringMethod initialized to x.ToString(), // the above statement is equivalent to saying // Console.WriteLine(\

在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数、返回一个字符串的方法来初始化firstStringMethod,就会产生一个编译错误。注意,int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确初始化委托。

? 156 ? 第Ⅰ部分 C# 语 言 下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释语句中的代码行。

委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但有趣的是,它们不关心调用该方法的是什么类型的对象,甚至不考虑该方法是静态方法,还是实例方法。

提示:

给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方法—— 只要方法的签名匹配于委托的签名即可。

为了说明这一点,我们扩展上面的代码,让它使用firstStringMethod委托在另一个对象上调用其他两个方法,其中一个方法是实例方法,另一个方法是静态方法。为此,再次使用本章前面定义的Currency结构。

struct Currency {

public uint Dollars; public ushort Cents;

public Currency(uint dollars, ushort cents) {

this.Dollars = dollars; this.Cents = cents; }

public override string ToString() {

return string.Format(\–2:00}\ }

public static explicit operator Currency (float value) {

checked {

uint dollars =(uint)value;

ushort cents =(ushort)((value-dollars)*100); return new Currency(dollars,cents); } }

public static implicit operator float (Currency value) {

return value.Dollars + (value.Cents/100.0f); }

第6章 委托和事件 public static implicit operator Currency (uint value) {

return new Currency(value, 0); }

public static implicit operator uint (Currency value) {

return value.Dollars; } }

? 157 ? Currency结构已经有了自己的ToString()重载方法。为了说明如何使用带有静态方法的委托,再增加一个静态方法,其签名与Currency的签名相同:

struct Currency {

public static string GetCurrencyUnit() {

return \ }

下面就可以使用GetAString 实例,代码如下所示:

private delegate string GetAString();

static void Main(string[] args) {

int x = 40;

GetAString firstStringMethod = new GetAString(x.ToString); Console.WriteLine(\ Currency balance = new Currency(34, 50);

firstStringMethod = new GetAString(balance.ToString); Console.WriteLine(\

firstStringMethod = new GetAString(Currency.GetCurrencyUnit); Console.WriteLine(\

这段代码说明了如何通过委托来调用方法,然后重新给委托指定在类的不同实例上执行的不同方法,甚至可以指定静态方法,或者在类的不同类型的实例上执行的方法,只要每个方法的签名匹配于委托定义即可。

但是,我们还没有说明把一个委托传递给另一个方法的具体过程,也没有给出任何有用的结果。调用int和Currency对象的ToString()的方法要比使用委托直观得多!在真正领会到委托的用途前,需要用一个相当复杂的示例来说明委托的本质。下面就是两个委托的示例。第一个示例仅使用委托来调用两个不同的操作,说明了如何把委托传递给方法,如何使用委托数组,但这仍没有很好地说明没有委托,就不能完成很多工作。第二个示例就复杂得多了,它有一个类BubbleSorter,执行一个方法,按照升序排列一个对象数组,这个类没有委托是很难编写出来的。

? 158 ? 第Ⅰ部分 C# 语 言 6.2 匿名方法

到目前为止,要想使委托工作,方法必须已经存在(即委托是用方法的签名定义的)。但使用委托还有另外一种方式:即通过匿名方法。匿名方法是用作委托参数的一个代码块。

用匿名方法定义委托的语法与前面的定义并没有什么区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:

namespace ConsoleApplication1 {

class Program {

delegate string delegateTest(string val);

static void Main(string[] args) {

string mid = \

delegateTest anonDel = delegate(string param) {

param += mid;

param += \ return param; };

Console.WriteLine(anonDel(\ } } }

委托delegateTest定义为一个类级变量,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:

{

param += mid;

param += \ return param; };

可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并添加到要传送的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串传送为参数,将返回的字符串输出到控制台上。

匿名方法的优点是减少了系统开销。方法仅在由委托使用时才定义。在为事件定义委托时,这是非常显然的。(本章后面探讨事件。)这有助于降低代码的复杂性,尤其是定义了好几个方

第6章 委托和事件 ? 159 ? 法时,代码会显得比较简单。

在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。

6.2.1 简单的委托示例

在这个示例中,定义一个类MathsOperations,它有两个静态方法,对double类型的值执行两个操作,然后使用该委托调用这些方法。这个数学类如下所示:

class MathsOperations {

public static double MultiplyByTwo(double value) {

return value*2; }

public static double Square(double value) {

return value*value; } }

下面调用这些方法:

using System;

namespace SimpleDelegate {

delegate double DoubleOp(double x);

class MainEntryPoint {

static void Main() {

DoubleOp [] operations = {

new DoubleOp(MathsOperations.MultiplyByTwo), new DoubleOp(MathsOperations.Square) };

for (int i=0 ; i

Console.WriteLine(\

? 160 ? 第Ⅰ部分 C# 语 言 ProcessAndDisplayNumber(operations[i], 2.0); ProcessAndDisplayNumber(operations[i], 7.94); ProcessAndDisplayNumber(operations[i], 1.414); Console.WriteLine(); } }

static void ProcessAndDisplayNumber(DoubleOp action, double value) {

double result = action(value); Console.WriteLine(

\ }

在这段代码中,实例化了一个委托数组DoubleOp (记住,一旦定义了委托类,就可以实例化它的实例,就像处理一般的类那样—— 所以把一些委托的实例放在数组中是可以的)。该数组的每个元素都初始化为由MathsOperations类执行的不同操作。然后循环这个数组,把每个操作应用到3个不同的值上。这说明了使用委托的一种方式—— 把方法组合到一个数组中,这样就可以在循环中调用不同的方法了。

这段代码的关键一行是把委托传递给ProcessAndDisplayNumber()方法,例如:

ProcessAndDisplayNumber(operations[i], 2.0);

其中传递了委托名,但不带任何参数,假定operations[i]是一个委托,其语法是: ● operations[i]表示“这个委托”。换言之,就是委托代表的方法。 ● operations[i](2.0)表示“调用这个方法,参数放在括号中”。 ProcessAndDisplayNumber()方法定义为把一个委托作为其第一个参数:

static void ProcessAndDisplayNumber(DoubleOp action, double value)

在这个方法中,调用:

double result = action(value);

这实际上是调用action委托实例封装的方法,其返回结果存储在result中。 运行这个示例,得到如下所示的结果:

SimpleDelegate Using operations[0]:

Value is 2, result of operation is 4 Value is 7.94, result of operation is 15.88 Value is 1.414, result of operation is 2.828

Using operations[1]:

Value is 2, result of operation is 4

Value is 7.94, result of operation is 63.0436

第6章 委托和事件 Value is 1.414, result of operation is 1.999396

? 161 ? 如果在这个例子中使用匿名方法,就可以删除第一个类MathOperations。Main方法应如下所示:

static void Main() {

DoubleOp multByTwo = delegate(double val) {return val * 2;} DoubleOp square = delegate(double val) {return val * val;}

DoubleOp [] operations = {multByTwo, square};

for (int i=0 ; i

Console.WriteLine(\ ProcessAndDisplayNumber(operations[i], 2.0); ProcessAndDisplayNumber(operations[i], 7.94); ProcessAndDisplayNumber(operations[i], 1.414); Console.WriteLine(); } }

运行这个版本,结果与前面的例子相同。其优点是删除了一个类。

6.2.2 BubbleSorter示例

下面的示例将说明委托的用途。我们要编写一个类BubbleSorter,它执行一个静态方法Sort(),这个方法的第一个参数是一个对象数组,把该数组按照升序重新排列。换言之,假定传递的是int数组:{0, 5, 6, 2, 1},则返回的结果应是{0, 1, 2, 5, 6}。

冒泡排序算法非常著名,是一种排序的简单方法。它适合于一小组数字,因为对于大量的数字(超过10个),还有更高效的算法。冒泡排序算法重复遍历数组,比较每一对数字,按照需要交换它们的位置,把最大的数字逐步移动到数组的最后。对于给int排序,进行冒泡排序的方法如下所示:

for (int i = 0; i < sortArray.Length; i++) {

for (int j = i + 1; j < sortArray.Length; j++) {

if (sortArray[j] < sortArray[i]) // problem with this test {

int temp = sortArray[i]; // swap ith and jth entries sortArray[i] = sortArray[j]; sortArray[j] = temp; } } }

? 162 ? 第Ⅰ部分 C# 语 言 它非常适合于int,但我们希望Sort()方法能给任何对象排序。换言之,如果某段客户机代码包含Currency结构数组或其他类和结构,就需要对该数组排序。这样,上面代码中的if(sortArray[j] < sortArray[i])就有问题了,因为它需要比较数组中的两个对象,看看哪一个更大。可以对int进行这样的比较,但如何对直到运行期间才知道或确定的新类进行比较?答案是客户机代码知道类在委托中传递的是什么方法,封装这个方法就可以进行比较。

定义如下的委托:

delegate bool CompareOp(object lhs, object rhs);

给Sort方法指定下述签名:

static public void Sort(object [] sortArray, CompareOp gtMethod)

这个方法的文档说明强调,gtMethod必须表示一个静态方法,该方法带有两个参数,如果第二个参数的值“大于”第一个参数(换言之,它应放在数组中靠后的位置),就返回true。

注意:

这里使用的是委托,但也可以使用接口来解决这个问题。.NET提供的IComparer接口就用于此目的。但是这里使用委托是因为这种问题本身要求使用委托。

设置完毕后,下面定义类BubbleSorter:

class BubbleSorter {

static public void Sort(object [] sortArray, CompareOp gtMethod) {

for (int i=0 ; i

for (int j=i+1 ; j

if (gtMethod(sortArray[j], sortArray[i])) {

object temp = sortArray[i]; sortArray[i] = sortArray[j]; sortArray[j] = temp; } } } } }

为了使用这个类,需要定义一些其他类,建立要排序的数组。在本例中,假定Mortimer Phones移动电话公司有一个员工列表,要对照他们的薪水进行排序。每个员工分别由类Employee的一个实例表示,如下所示:

class Employee

第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中定义的事件都使用它。如前所述,委托要求添加到委托列表中的所有方法都必须

? 168 ? 第Ⅰ部分 C# 语 言 有相同的签名。显然事件委托也有这个要求。下面是Button_Click方法的定义:

Private void Button_Click(object sender, Eventargs e) { }

这个方法有几个重要的地方。首先,它总是没有返回值。事件处理程序不能有返回值。其次是参数。只要使用EventHandler委托,参数就应是object和EventArgs。第一个参数是引发事件的对象,在这个例子中是btnOne或btnTwo,这取决于被单击的按钮。把一个引用发送给引发事件的对象,就可以把同一个的事件处理程序赋予多个对象。例如,可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮。

第二个参数EventArgs是包含有关事件的其他有用信息的对象。这个参数可以是任意类型,只要它派生于EventArgs即可。MouseDown事件使用MouseDownEventArgs,它包含所使用按钮的属性、指针的X和Y坐标,以及与事件相关的其他信息。注意,其命名模式是在类型的后面加上EventArgs。本章的后面将介绍如何创建和使用基于EventArgs的定制对象。

方法的命名也应注意。按照约定,事件处理程序应遵循“object_event.object”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。

本例最后在处理程序中添加了一些代码,以完成一些工作。记住有两个按钮使用同一个处理程序。所以首先必须确定是哪个按钮引发了事件,接着调用应执行的操作。在本例中,只是在窗体的一个标签控件上输出一些文本。把一个标签控件从工具箱拖放到窗体上,并将其命名为lblInfo,然后在Button_Click方法中编写如下代码:

if(((Button)sender).Name == \ lblInfo.Text = \else

lblInfo.Text = \

注意,由于sender参数作为对象发送,所以必须把它的数据类型转换为引发事件的对象类型,在本例中就是Button。本例使用Name属性确定是哪个按钮引发了对象,也可以使用其他属性。例如Tag属性就可以处理这种情形,因为它可以包含任何内容。为了了解事件委托的多播功能,给btnTwo的Click事件添加另一个方法,使用默认的方法名。窗体的构造函数如下所示:

btnOne.Click += new EventHandler(Button_Click); btnTwo.Click += new EventHandler(Button_Click); btnTwo.Click += new EventHandler(btnTwo_Click);

如果让Visual Studio创建存根(stub),就会在源文件的末尾得到如下方法。但是,必须添加对MessageBox函数的调用:

Private void btnTwo_Click(object sender, EventArgs e) {

第6章 委托和事件 MessageBox.Show(\}

? 169 ? 如果使用匿名方法,就不需要Button_Click方法和btnTwo_Click方法了。事件的代码如下:

btnOne.Click += new EventHandler(lblInfo.Text = \btnTwo.Click += new EventHandler(lblInfo.Text = \btnTwo.Click += new EventHandler(MessageBox.Show

(\

在运行这个例子时,单击btnOne会改变标签上的文本。单击btnTwo不仅会改变文本,还会显示消息框。注意,不能保证标签文本在消息框显示之前改变,所以不要在处理程序中编写具有依赖性的代码。

我们已经学习了许多概念,但要在接收器中编写的代码量是很小的。记住,编写事件接收器常常比编写事件发送器要频繁得多。至少在Windows用户界面上,Microsoft已经编写了所有需要的事件发送器(它们都在.NET基类中,在Windows.Forms命名空间中)。

6.3.2 生成事件

接收事件并响应它们仅是事件的一个方面。为了使事件真正发挥作用,还需要在代码中生成和引发事件。下面的例子将介绍如何创建、引发、接收和取消事件。

这个例子包含一个窗体,它会引发另一个类正在监听的事件。在引发事件后,接收对象就确定是否执行一个过程,如果该过程未能继续,就取消事件。本例的目标是确定当前时间的秒数是大于30还是小于30。如果秒数小于30,就用一个表示当前时间的字符串设置一个属性;如果秒数大于30,就取消事件,把时间字符串设置为空。

用于生成事件的窗体包含一个按钮和一个标签。下载的示例代码把按钮命名为btnRaise,标签命名为lblInfo,您也可以给标签使用其他名称。在创建窗体,添加两个控件后,就可以创建事件和相应的委托了。在窗体类的类声明部分,添加如下代码:

public delegate void ActionEventHandler(object sender, ActionCancel EventArgs ev); public static event ActionEventHandler Action;

这两行代码的作用是什么?首先,我们声明了一种新的委托类型ActionEventHandler。必须创建一种新委托,而不使用.NET Framework预定义的委托,其原因是后面要使用定制的EventArgs类。方法签名必须与委托匹配。有了一个要使用的委托后,下一行代码就定义事件。在本例中定义了Action事件,定义事件的语法要求指定与事件相关的委托。还可以使用在.NET Framework中定义的委托。从EventArgs类中派生出了近100个类,应该可以找到一个自己能使用的类。但本例使用的是定制的EventArgs类,所以必须创建一个与之匹配的新委托类型。

基于EventArgs的新类ActionCancelEventHandler实际上派生于CancelEventArgs,而CancelEventArgs派生于EventArgs。CancelEventArgs添加了Cancel属性,该属性是一个布尔值,它通知sender对象,接收器希望取消或停止事件的处理。在ActionEventHandler类中,还添加了Message属性,这是一个字符串属性,包含事件处理状态的文本信息。下面是ActionCancel-

? 170 ? 第Ⅰ部分 C# 语 言 EventHandler类的代码:

public class ActionCancelEventHandler : System.ComponentModel. CancelEventArgs {

string _msg = \

public ActionCancelEventArgs() : base() {}

public ActionCancelEventArgs(bool cancel) : base(cancel) {}

public ActionCancelEventArgs(bool cancel, string message) : base(cancel) {

_msg = message; }

public string Message {

get {return _msg;} set {_msg = value;} } }

可以看出,所有基于EventArgs的类都负责在发送器和接收器之间来回传送事件的信息。在大多数情况下,EventArgs类中使用的信息都被事件处理程序中的接收器对象使用。但是,有时事件处理程序可以把信息添加到EventArgs类中,使之可用于发送器。这就是本例使用EventArgs类的方式。注意在EventArgs类中有两个可用的构造函数。这种额外的灵活性增加了该类的可用性。

目前声明了一个事件,定义了一个委托,并创建了EventArgs类。下一步需要引发事件。真正需要做的是用正确的参数调用事件,如本例所示:

ActionCancelEventArgs ev = new CancelEventArgs(); Action(this, ev);

这非常简单。创建新的ActionCancelEventArgs类,并把它作为一个参数传递给事件。但是,这有一个小问题。如果事件不会在任何地方使用,该怎么办?如果还没有为事件定义处理程序,该怎么办?Action事件实际上是空的。如果试图引发该事件,就会得到一个空引用异常。如果要派生一个新的窗体类,并使用该窗体,把Action事件定义为基事件,则只要引发了Action事件,就必须执行其他一些操作。目前,我们必须在派生的窗体中激活另一个事件处理程序,这样才能访问它。为了使这个过程容易一些,并捕获空引用错误,就必须创建一个方法OnEvent Name,其中EventName是事件名。在这个例子中,有一个OnAction方法,下面是OnAction方法的完整代码:

protected void OnAction(object sender, ActionCancelEventArgs ev) {

if(Action != null) Action(sender, ev); }

第6章 委托和事件 ? 171 ? 代码并不多,但完成了需要的工作。把该方法声明为protected,就只有派生类可以访问它。事件在引发之前还会进行空引用测试。如果派生一个包含该方法和事件的新类,就必须重写OnAction方法,然后连接事件。为此,必须在重写代码中调用base.OnAction()。否则就不会引发该事件。在整个.NET Framework中都用这个命名约定,并在.NET SDK文档中对这一命名规则进行了说明。

注意传送给OnAction方法的两个参数。它们看起来很熟悉,因为它们与需要传送给事件的参数相同。如果事件需要从另一个对象中引发,而不是从定义方法的对象中引发,就需要把访问修饰符设置为internal或public,而不能设置为protected。有时让类只包含事件声明和从其他类中调用的事件是有意义的。仍可以创建OnEventName方法,但它们是静态方法。

目前,我们已经引发了事件,还需要一些代码来处理它。在项目中创建一个新类,在这个例子中把该类称为BusEntity。本项目的目的是检查当前时间的秒数,如果它小于30,就把一个字符串值设置为时间;如果它大于30,就把字符串设置为::,并取消事件。下面是代码:

using System; using System.IO;

using System.ComponentModel;

namespace SimpleEvent {

public class BusEntity {

string _time =\ public BusEntity() {

Form1.Action += new Form1.ActionEventHandler(Form1_Action); }

private void Form1_Action(object sender, ActionCancelEventArgs ev) {

ev.Cancel = !DoAction(); if(ev.Cancel)

ev.Message =\’t the right time.\ }

private bool DoAction() {

bool retVal = false;

DateTime tm = DateTime.Now;

if(tm.second) < 30) {

_time =\ retVal = true;

? 172 ? } else

_time = \

return retVal; }

public string TimeString {

get {return _time;} } } }

第Ⅰ部分 C# 语 言 在构造函数中声明了Form1.Action事件的处理程序。注意其语法非常类似于前面Click事件的语法。由于声明事件使用的模式都是相同的,所以语法也应保持一致。还要注意如何获取Action事件的引用,而无需在BusEntity类中有对Form1的引用。在Form1类中,将Action事件声明为静态,这并不是必需的,但这样更易于创建处理程序。我们可以把事件声明为public,但接着需要引用Form1的一个实例。

在构造函数中编写事件时,调用添加到委托列表中的方法Form1_Action,并遵循命名标准。在处理程序中,需要确定是否取消事件。DoActions方法根据前面描述的时间条件返回一个布尔值,并把_time字符串设置为正确的值。

在DoActions返回值后,就把该值赋给ActionCancelEventArgs的Cancel属性。EventArgs类一般仅在事件发送器和接收器之间来回传递值。如果取消了事件(ev.Cancel = true),Message属性就设置为一个字符串值,以说明事件为什么被取消。

如果再次查看btnRaise_Click事件处理程序的代码,就可以看出Cancel属性的使用方式:

private void btnRaise_Click(object sender, EventArgs e) {

ActionCancelEventArgs cancelEvent = new ActionCancelEventArgs(); OnAction(this, cancelEvent); If(cancelEvent.Cancel)

lblInfo.Text = cancelEvent.Message; else

lblInfo.Text = _busEntity.TimeString; }

注意创建了ActionCancelEventArgs对象。接着引发了事件Action,并传递了新建的ActionCancelEventArgs对象。在调用OnAction方法,引发事件时,BusEntity对象中Action事件处理程序的代码就会执行。如果还有其他对象注册了事件Action,它们也会执行。记住,如果其他对象也处理这个事件,它们就会看到同一个ActionCancelEventArgs对象。如果需要确定是哪个对象取消了事件,而且如果有多个对象取消了事件,就需要在ActionCancelEventArgs类

第6章 委托和事件 ? 173 ? 中包含某种基于列表的数据结构。

在与事件委托一起注册的处理程序执行完毕后,就可以查询ActionCancelEventArgs对象,确定它是否被取消了。如果是,lblInfo就包含Message属性值;如果事件没有被取消,lblInfo就会显示当前时间。

本节这基本上说明了如何利用事件和事件中基于EventArgs的对象,在应用程序中传递信息。

6.4 小结

本章介绍了委托和事件的基本知识,解释了如何声明委托,如何给委托列表添加方法,并讨论了声明事件处理程序来响应事件的过程,以及如何创建定制事件,使用引发事件的模式。

.NET开发人员将大量使用委托和事件,特别是开发Windows Forms应用程序。事件是.NET开发人员监视应用程序执行时出现的各种Windows消息的方式,否则就必须监视WndProc,捕获WM_MOUSEDOWN消息,而不是获取按钮的鼠标Click事件。

在设计大型应用程序时,使用委托和事件可以减少依赖性和层的关联,并能开发出具有更高复用性的组件。

第6章 委托和事件 ? 173 ? 中包含某种基于列表的数据结构。

在与事件委托一起注册的处理程序执行完毕后,就可以查询ActionCancelEventArgs对象,确定它是否被取消了。如果是,lblInfo就包含Message属性值;如果事件没有被取消,lblInfo就会显示当前时间。

本节这基本上说明了如何利用事件和事件中基于EventArgs的对象,在应用程序中传递信息。

6.4 小结

本章介绍了委托和事件的基本知识,解释了如何声明委托,如何给委托列表添加方法,并讨论了声明事件处理程序来响应事件的过程,以及如何创建定制事件,使用引发事件的模式。

.NET开发人员将大量使用委托和事件,特别是开发Windows Forms应用程序。事件是.NET开发人员监视应用程序执行时出现的各种Windows消息的方式,否则就必须监视WndProc,捕获WM_MOUSEDOWN消息,而不是获取按钮的鼠标Click事件。

在设计大型应用程序时,使用委托和事件可以减少依赖性和层的关联,并能开发出具有更高复用性的组件。

本文来源:https://www.bwwdw.com/article/vx45.html

Top