8. 用户事件
在上一章中,我们讨论了与表单组件相关的事件概念。现在我们将探讨如何在自己的类中创建事件。
8.1. 预定义的委托对象
我们在上一章中已经接触过对象委托的概念,但当时只是略过不谈。在探讨表单组件的事件处理程序如何声明时,我们曾遇到过类似以下的代码:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
其中 buttonAfficher 是一个 [Button] 组件。该类定义了如下 Click 事件:
![]() |
- [1]:[Button] 类
- [2]:其事件
- [3,4]:Click 事件
- [5]:事件 [Control.Click] [4] 的声明。
- EventHandler 是名为“委托”的方法的原型(模型)。
- event 是一个关键字,用于限制委托的功能。EventHandler:作为对象的委托比 event 具有更丰富的功能。
访问委托 EventHandler 的定义如下:
![]() |
Visit 委托的 EventHandler 指定了一个方法模型:
- 其第一个参数为 Object 类型
- 其第二个参数为 EventArgs
- 且不返回任何结果
与 EventHandler 定义的模型对应的方法可能如下所示:
private void buttonAfficher_Click(object sender, EventArgs e);
要创建一个 EventHandler,请按以下步骤操作:
EventHandler evtHandler=new EventHandler(méthode correspondant au prototype défini par le type EventHandler);
因此我们可以这样写:
实际上,委托类型的变量是一组指向该委托所对应方法的引用列表。要将新方法 M 添加到上面的变量 evtHandler 中,我们使用以下语法:
即使 evtHandler 是一个空列表,也可以使用 += 语法。
说明:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
将一个事件处理程序添加到 buttonAfficher.Click 事件方法列表中。当组件 buttonAfficher 发生 Click 事件时,VB 将执行:
其中:
- source 是事件背后的对象
- evt 类型为 EventArgs,且不包含任何信息
所有与 Click 事件关联的 void M(object, EventArgs) 签名方法由:
this.buttonAfficher.Click += new System.EventHandler(M);
都将通过 VB 传递的参数 (source, evt) 被调用。
8.2. 定义委托对象
说明
public delegate int Opération(int n1, int n2);
定义了一个名为 Operation 的类型,它表示一个函数原型,该原型接受两个整数并返回一个整数。正是关键字 delegate 使 Operation 成为一个函数原型定义。
类型为 Operation 的变量 op 将注册一组与原型 Operation 对应的函数:
将方法 fi 注册到变量 op 中,可通过 op=new Operation(fi) 或简写为 op=fi 实现。要将方法 fj 添加到已注册函数列表中,我们写 op+= fj。要移除方法 fk,我们写 op-=fk。如果在本例中,我们写 n=op(n1,n2),则 op 中存储的方法集合将使用参数 n1 和 n2 执行。 最终获得的结果 n 将是最后执行的方法所返回的值。无法获取所有方法产生的结果。因此,若将方法列表存储在委托函数中,这些函数通常会返回 void 类型的结果。
请看以下示例:
using System;
namespace Chap6 {
class Class1 {
// function prototype definition
// accepts 2 integers as parameters and returns an integer
public delegate int Opération(int n1, int n2);
// two instance methods corresponding to the prototype
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//add
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtract
// a static method corresponding to the prototype
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//increase
static void Main(string[] args) {
// define an operation object to store functions
// we register the static function increase
Opération op = Augmenter;
// the delegate is executed
int n = op(4, 7);
Console.WriteLine("n=" + n);
// creation of a c1 object of type class1
Class1 c1 = new Class1();
// we register c1's add method in the delegate
op = c1.Ajouter;
// execution of delegated object
n = op(2, 3);
Console.WriteLine("n=" + n);
// the subtract method of c1 is registered in the delegate
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//registration of two functions in the delegate
op = c1.Ajouter;
op += c1.Soustraire;
// execution of delegated object
op(0, 0);
// remove a function from the delegate
op -= c1.Soustraire;
// the delegate is executed
op(1, 1);
}
}
}
- 第 3 行:定义了一个名为 Class1 的类。
- 第 6 行:定义委托 Operation:一个方法原型,接受两个 int 类型的参数并返回一个 int 类型
- 第 9-12 行:将实例方法 Add 添加到委托 Operation 的签名中。
- 第 14-17 行:将实例方法 subtract 添加到委托 Operation 的签名中。
- 第 20-23 行:将类方法 Ancrease 添加到委托 Operation 的签名中。
- 第 25 行:执行了 Main 方法
- 第 20 行:变量 op 的类型为 delegate Operation。它将包含一组具有 delegate Operation 类型签名的方法。它被赋予了第一个方法引用,即静态方法 Class1.Augmenter 的引用。
- 第 31 行:执行委托 op:op 引用的所有方法都将被执行。它们将使用传递给委托 op 的参数进行执行。在此处,仅会执行静态方法 Class1.Augmenter。
- 第 35 行:创建 Class1 类的实例 c1。
- 第 37 行:将实例方法 c1.Ajouter 赋值给委托 op。Augmenter 是静态方法,而 Add 是实例方法。我们想说明这并不重要。
- 第 39 行:执行委托 op:Add 方法将使用传递给委托 op 的参数进行执行。
- 第 42 行:对实例方法 Subtract 执行相同操作。
- 第 46-47 行:委托 op 中的 Add 和 Subtract 方法。
- 第 49 行:执行委托 op:Add 和 Subtract 都将使用传递给委托 op 的参数被执行。
- 第 51 行:从委托操作中移除 Subtract 方法。
- 第 53 行:执行委托操作:剩余的 Add 方法将被执行。
结果如下:
8.3. 委托还是接口?
委托和接口的概念看似相当相似,我们可能会好奇这两者究竟有何区别。让我们来看一个与我们之前研究过的例子相似的示例:
using System;
namespace Chap6 {
class Program1 {
// function prototype definition
// accepts 2 integers as parameters and returns an integer
public delegate int Opération(int n1, int n2);
// two instance methods corresponding to the prototype
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//add
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//subtract
// Executing a delegate
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// delegate execution Add
Console.WriteLine(Execute(Ajouter, 2, 3));
// delegate execution Subtract
Console.WriteLine(Execute(Soustraire, 2, 3));
// executing a multicast delegate
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// remove a function from the delegate
op -= Soustraire;
// the delegate is executed
Console.WriteLine(Execute(op, 2, 3));
}
}
}
第 20 行,Execute 方法期望接收一个对第 6 行定义的 Operation 委托类型对象的引用。这会切换到 Execute 方法的不同实现(第 26、28、32 和 36 行)。这种多态特性也可以通过 : 来实现:
using System;
namespace Chap6 {
// interface IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// class Add
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// class Subtract
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// test class
public static class Program2 {
// Executing the single method of the IOperation interface
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// delegate execution Add
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// delegate execution Subtract
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- 第 6-8 行:接口 [IOperation] 定义了一个 operation 方法。
- 第 11-16 行和第 19-24 行:[Add] 和 [Subtract] 类实现了 [IOperation] 接口。
- 第 29-31 行:方法 Execute 的第一个参数是接口类型 IOperation。方法 Execute 将依次接收 Add 类的实例和 Subtract 类的实例作为第一个参数。
前一个示例中委托的多态性。同时,这两个示例也展示了这两个概念之间的差异。
当接口仅包含一个方法时,委托和接口可以互换使用
- (前提是接口仅包含一个方法)。实际上,委托是单个方法的封装,而接口可以定义多个方法。
- 如果未使用委托的多播特性。接口中并不存在多播的概念。
如果满足这两个条件,那么我们可以为 Execute 方法选择以下两种签名之一:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
第二种方法使用委托,因此使用起来更加灵活。在第一种签名中,方法的第一个参数必须实现 IOperation 接口。这要求创建一个类来定义将作为 Execute 方法第一个参数传递的方法。而在第二种签名中,任何具有正确签名的现有方法均可使用,无需额外构建。
8.4. 事件管理
对象委托类可用于定义事件。类 C1 可以如下方式定义事件 evt:
- 在 C1 类内部或外部定义一个类型委托:
- 类 C1 定义了一个名为 Evt 的委托:
- 当类 C1 的实例 c1 想要报告一个事件时,它将调用其委托 Evt1,并向其传递委托 Evt 定义的参数。随后,委托 Evt1 中注册的所有方法都将使用这些参数被执行。可以说,这些方法已收到关于事件 Evt1 的通知。
- 如果某个使用类 C1 的对象 c2 希望在对象 c1 上发生事件 Evt1 时收到通知,它将把其方法 c2.M 注册到委托对象 c1.Evt1 中。每当对象 c1 上发生事件 Evt1 时,其 c2.M 方法就会被执行。如果不再希望收到该事件的通知,它们也可以取消订阅。
- 由于委托对象 c1.Evt1 可以注册多个方法,不同的 c2 对象均可向 c1.Evt1 注册,从而接收 c1 上 Evt1 事件的通知。
在此场景中,我们有:
- 一个触发事件的类
- 接收该事件通知的类。这些类被称为订阅了该事件。
- 一个委托类型,用于定义将接收该事件通知的方法的签名
.NET 框架定义了:
- 事件委托的标准签名
- source:报告该事件的对象
- evtInfo:一个 EventArgs 类型或其派生类的对象,用于提供有关该事件的信息
- 委托的名称必须以 EventHandler 结尾
- 在类中声明 MyEventHandler 的标准方法:
Evt1 类型是委托。关键字 event 的作用是限制对其可执行的操作:
- 在 C1 类外部,仅允许执行 += 和 -= 操作。这可防止订阅该事件的方法被删除(例如因开发人员错误导致)。您可以直接订阅(+=)或取消订阅(-=)该事件。
- 只有 C1 类型的实例才能调用 Evt1(source, evtInfo),从而触发订阅了 Evt1 的方法的执行。
.NET 框架提供了一个满足事件委托签名的泛型方法:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- 委托 EventHandler 使用泛型类型 TEventArgs,该类型即其第二个参数的类型
- 类型 TEventArgs 必须继承自 EventArgs(where TEventArgs : EventArgs)
采用这种泛型委托后,类 C 中事件 X 的声明应遵循以下推荐方案:
- 定义一个从 EventArgs 派生的类型 XEventArgs 来封装事件信息 X
- 在类 C 中定义一个 EventHandler<XEventArgs>。
- 在类 C 中定义一个受保护的方法
用于将事件 X “发布”给订阅者。
请看以下示例:
- 类 Transmitter 封装了一个温度值。该温度值会被持续监测。当温度超过某个阈值时,将触发一个事件。我们将该事件命名为 TemperatureTropHaute。关于此事件的信息将被封装在类型 TemperatureTropHauteEventArgs 中。
- 一个名为 Underwriter 的类订阅了上述事件。当接收到事件通知时,它会在控制台上显示一条消息。
- 一个控制台程序创建了一个 Transmitter 和两个订阅者。它从键盘输入温度值并将其存储在 Transmitter 中。如果温度过高,Transmitter 将发布 TemperatureTropHaute 事件。
为了符合推荐的事件管理方法,我们首先定义类型 TemperatureTropHauteEventArgs 来封装事件信息:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// temperature during evt
public decimal Temperature { get; set; }
// manufacturers
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- 第 6 行:由 TemperatureTropHauteEventArgs 封装的信息是触发 TemperatureTropHaute 事件的温度。
Transmitter 类定义如下:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// observed temperature
private decimal temperature;
// name of source
public string Nom { get; set; }
// event reported
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// read/write temperature
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// subscribers are notified of the event
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// reporting an event
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// issue of event TemperatureTropHaute to subscribers
TemperatureTropHaute(this, evt);
}
}
}
- 第 5 行:温度阈值,超过该阈值时将发布 TemperatureTropHaute 事件。
- 第 10 行:发送者有一个用于识别的名称
- 第 12 行:TemperatureTropHaute 事件。
- 第 15-26 行:get 方法用于获取温度,set 方法用于记录温度。当待记录的温度超过第 5 行设定的阈值时,set 方法将发布 TemperatureTropHaute 事件。它通过传递一个记录了超过阈值温度的 TemperatureTropHauteEventArgs 对象,使用第 29 行定义的 OnTemperatureTropHauteHandler 发布该事件。
- 第 29-32 行:发布 TemperatureTropHaute 事件,将事件发送者本身作为第一个参数,并将接收到的 TemperatureTropHauteEventArgs 对象作为参数。
将订阅 TemperatureTropHaute 事件的 Underwriter 类如下所示:
using System;
namespace Chap6 {
public class Souscripteur {
// name
public string Nom { get; set; }
// event manager TemperatureTropHaute
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// operator console display
Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
}
}
}
- 第 6 行:每个订阅者都通过名称进行标识。
- 第 9-12 行:与 TemperatureTropHaute 事件关联的方法。该方法的签名是 EventHandler<TEventArgs> 类型,这是事件管理器必须具备的类型。该方法会在控制台显示:显示该消息的订阅者名称、报告该事件的发送者名称,以及触发该事件的温度。
- Transmitter 对象对 TemperatureTropHaute 事件的订阅并非在 Underwriter 类中完成,而是由一个外部类来处理。
[Program.cs] 程序将所有这些元素整合在一起:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// creation of an evts transmitter
Emetteur e1 = new Emetteur() { Nom = "e" };
// creation of a table of 2 subscribers
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// creation subscriber
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// we subscribe him to e1's TemperatureTropHaute event
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// temperatures are read from the keyboard
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// as long as the line entered is not empty
while (saisie != "") {
// is the input a decimal number?
if (decimal.TryParse(saisie, out temperature)) {
// correct temperature - recorded
e1.Temperature = temperature;
} else {
// on signale l'erreur
Console.WriteLine("Température incorrecte");
}
// new entry
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//while
}
}
}
- 第 6 行:创建发送器
- 第8-14行:创建两个订阅器,用于订阅发布器的 TemperatureTropHaute 事件。
- 第20-32行:键盘温度输入循环
- 第 24 行:如果输入的温度正确,则将其传输至 Transmitter e1 对象;若温度高于 19°C,该对象将触发 TemperatureTropHaute 事件。
结果如下:

