8. 用户事件
上一章我们讨论了与表单组件相关的事件概念。现在我们将了解如何在自己的类中创建事件。
8.1. 预定义的委托对象
上一章曾提及 delegate 对象的概念,但当时仅进行了简要介绍。在探讨表单组件事件处理程序的声明方式时,我们曾遇到过类似以下代码:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
其中 buttonAfficher 是 [Button] 类型的组件。该类有一个名为 Click 的字段,定义如下:
![]() |
- [1]:类 [Button]
- [2]:其事件
- [3,4]:事件 Click
- [5]:事件 [Control.Click] [4] 的声明。
- EventHandler 是一个称为委托的方法原型(模板)。
- event 是一个关键字,用于限制 delegate 的功能。EventHandler: delegate 对象的功能比 event 对象更丰富。
delegate EventHandler 的定义如下:
![]() |
delegate 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);
因此可以编写如下代码:
类型为 delegate 的变量实际上是一组对 delegate 模型中相应方法的引用列表。 若要在上述变量 evtHandler 中添加新方法 M,请使用以下语法:
即使 evtHandler 是一个空列表,也可以使用 += 语法。
语句:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
将类型为 EventHandler 的方法添加到事件 buttonAfficher.Click 的方法列表中。 当组件 buttonAfficher 上的事件 Click 发生时,VB 执行以下语句:
其中:
- source 是类型为 object 的组件,该组件触发了事件
- 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);
定义了一个名为 Opération 的类型,该类型表示一个接受两个整数并返回一个整数的函数原型。正是关键字 delegate 使 Opération 成为函数原型的定义。
一个类型为 Opération 的变量 op 将用于存储与原型 Opération 对应的函数列表:
将方法 fi 存储在变量 op 中,可通过以下方式实现:op=new 操作(fi) 或者更简单地写成 op=fi.若要将方法 fj 添加到已存储的函数列表中,应写为 op+= fj。若要删除已存储的方法 fk,应写为 op-=fk。 如果在本例中输入 n=op(n1,n2),则变量 op 中记录的所有方法都将使用参数 n1 和 n2 执行。 最终获取的结果 n 将是最后一个执行的方法的结果。无法获取所有方法生成的结果。因此,如果将一组方法存储在委托函数中,这些方法通常会返回类型为 void 的结果。
请看以下示例:
using System;
namespace Chap6 {
class Class1 {
// 定义函数原型
// 接受 2 个整数作为参数,并返回一个整数
public delegate int Opération(int n1, int n2);
// 两个与原型对应的实例方法
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//添加
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//减法
// 一个与原型对应的静态方法
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//增加
static void Main(string[] args) {
// 定义一个操作类型的对象,用于存储函数
// 注册静态函数“增加”
Opération op = Augmenter;
// 执行委托
int n = op(4, 7);
Console.WriteLine("n=" + n);
// 创建一个类型为 class1 的对象 c1
Class1 c1 = new Class1();
// 将 c1 的 add 方法注册到委托中
op = c1.Ajouter;
// 执行委托对象
n = op(2, 3);
Console.WriteLine("n=" + n);
// 将 c1 的减法方法注册到委托中
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//将两个函数注册到委托中
op = c1.Ajouter;
op += c1.Soustraire;
// 执行委托对象
op(0, 0);
// 从委托中移除一个函数
op -= c1.Soustraire;
// 执行委托
op(1, 1);
}
}
}
- 第 3 行:定义类 Class1。
- 第 6 行:定义 delegateOpération:一个方法原型,接受两个 int 类型的参数,并返回 int 类型的结果
- 第 9-12 行:实例方法 Ajouter 具有委托 Opération 的签名。
- 第 14-17 行:实例方法 Soustraire 具有 Opération 委托的签名。
- 第 20-23 行:类方法 Augmenter 的签名与 Opération 委托的签名一致。
- 第 25 行:方法 Main 已执行
- 第20行:变量op的类型为Opération委托。它将包含一组具有Opération委托签名的方法列表。此时为其赋值了第一个方法引用,即静态方法Class1.Augmenter的引用。
- 第 31 行:执行 delegate op:op 引用的所有方法都将被执行。 它们将使用传递给 delegate 和 op. 的参数进行执行。在此,仅会执行静态方法 Class1.Augmenter。
- 第 35 行:创建了类 Class1 的实例 c1。
- 第 37 行:实例方法 c1.Ajouter 被分配给委托 op。Augmenter 是静态方法,Ajouter 是实例方法。此处旨在说明这并不重要。
- 第 39 行: 执行 delegate op:方法 Ajouter 将使用传递给 delegate op 的参数进行执行。
- 第 42 行:对实例方法 Soustraire 重复上述操作。
- 第 46-47 行:将方法 Ajouter 和 Soustraire 放入 delegate op 中。
- 第49行:执行delegate和op: 方法 Ajouter 和 Soustraire 将使用传递给 delegate op 的参数进行执行。
- 第 51 行:方法 Soustraire 从 delegate op 中移除。
- 第 53 行:执行 delegate op:剩余的方法 Ajouter 将被执行。
执行结果如下:
8.3. 委托还是接口?
delegates与接口的概念看似相当接近,人们可能会质疑这两者之间究竟有何区别。让我们以一个与先前研究过的示例相似的例子来说明:
using System;
namespace Chap6 {
class Program1 {
// 定义函数原型
// 接受 2 个整数作为参数并返回一个整数
public delegate int Opération(int n1, int n2);
// 两个与原型对应的实例方法
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//添加
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//减法
// 执行委托
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// 执行“加法”委托
Console.WriteLine(Execute(Ajouter, 2, 3));
// 执行减法委托
Console.WriteLine(Execute(Soustraire, 2, 3));
// 执行多播委托
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// 从代理中移除一个函数
op -= Soustraire;
// 执行委托
Console.WriteLine(Execute(op, 2, 3));
}
}
}
第 20 行,方法 Execute 期望接收一个指向第 6 行定义的 Opération 委托类型对象的引用。这使得该方法能够调用 Execute 中的不同方法(第 26、28、32 和 36 行)。 这种多态特性也可以通过接口实现:
using System;
namespace Chap6 {
// 接口 IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// Add类
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// Subtract 类
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// 测试类
public static class Program2 {
// 执行接口的唯一方法 IOperation
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// 执行“加法”委托
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// 执行减法委托
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- 第 6-8 行:接口 [IOperation] 定义了方法 operation。
- 第 11-16 行和第 19-24 行:类 [Ajouter] 和 [Soustraire] 实现了接口 [IOperation]。
- 第29-31行:方法Execute,其第一个参数的类型为接口IOperation。 方法 Execute 将依次接收 Ajouter 类的实例和 Soustraire 类的实例作为第一个参数。
这确实体现了前一个示例中 delegate 类型参数所具有的多态性。这两个示例同时也展示了这两个概念之间的差异。
类型 delegate 和 interface 是可互换的
- 。实际上,类型 delegate 是针对单一方法的封装,而接口本身可以定义多个方法。
- 如果未使用 delegate 的多播特性。事实上,接口中并不存在多播这一概念。
如果满足上述两个条件,则方法 Execute 可在以下两种签名中进行选择:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
使用 delegate 的第二种签名可能更灵活。因为在第一种签名中,方法的第一个参数必须实现 IOperation 接口。 这迫使开发者必须创建一个类,并在其中定义将作为 Execute 方法第一个参数传递的方法。而在第二种签名中,任何具有正确签名的现有方法均可适用。无需进行额外的构建工作。
8.4. 事件管理
delegate 对象可用于定义事件。C1 类可以如下方式定义 evt 事件:
- 在 C1 类内部或外部定义一个 delegate 类型:
- 类 C1 定义了一个类型为 delegate 的字段 Evt:
- 当类 C1 的实例 c1 需要触发事件时,它将调用其委托 Evt1,并向其传递由委托 Evt 定义的参数。 此时,delegate Evt1中注册的所有方法都将使用这些参数被执行。可以说,它们已收到Evt1事件的通知。
- 如果一个使用 c1 对象的 c2 对象希望收到 c1 对象上 Evt1 事件发生的通知, 它将把其方法 c2.M 之一注册到对象 c1. 的委托对象 c1.Evt1 中。因此,每当当事件 Evt1 在对象 c1 上发生时,其方法 c2.M 就会被执行。当它不再希望收到该事件的通知时,也可以取消注册。
- 由于代理对象 c1.Evt1 可以注册多个方法, 不同的 ci 对象可以向代理 c1.Evt1 注册,以便接收关于 c1 上 Evt1 事件的通知。
在此场景中,我们有:
- 一个发布事件的类
- 若干接收该事件通知的类。通常称其为“订阅该事件”或“订阅该事件”。
- 一个类型 delegate,用于定义将收到事件通知的方法的签名
.NET框架定义了:
- delegate事件的标准签名
- source:触发该事件的对象
- evtInfo:一个 EventArgs 类型或其派生对象,提供有关该事件的信息
- delegate 的名称必须以 EventHandler 结尾
- 在类中声明 MyEventHandler 类型事件的标准方式:
字段 Evt1 的类型为 delegate。关键字 event 的作用是限制对其可执行的操作:
- 在 C1 类外部,仅允许执行 += 和 -= 操作。这可防止(例如因开发人员失误)误删订阅该事件的方法。用户仅能对事件进行订阅(+=)或取消订阅(-=)。
- 只有 C1 类型的实例才能调用 Evt1(source,evtInfo),从而触发订阅了 Evt1. 事件的方法的执行
框架 .NET 提供了一个符合事件 delegate 签名要求的通用方法:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- delegate EventHandler 使用泛型类型 TEventArgs,该类型是其第二个参数的类型
- 类型 TEventArgs 必须继承自类型 EventsArgs(其中 TEventArgs 继承自 EventArgs)
对于这个通用的 delegate 类型,在 C 类中声明 X 事件时,应遵循以下建议的模式:
- 定义一个从 EventArgs 派生的 XEventArgs 类型,用于封装 X 事件的信息
- 在类 C 中定义类型为 EventHandler<XEventArgs> 的事件。
- 在类 C 中定义一个受保护的方法
,用于向订阅者“发布”事件 X。
考虑以下示例:
- 类 Emetteur 封装了一个温度值。该温度值被持续监测。当温度超过某个阈值时,应触发一个事件。我们将该事件命名为 TemperatureTropHaute。该事件的相关信息将被封装在类型 TemperatureTropHauteEventArgs 中。
- 类 Souscripteur 订阅了上述事件。当接收到事件通知时,它会在控制台显示一条消息。
- 一个控制台程序创建了一个发布者和两个订阅者。它通过键盘输入温度,并将温度记录到 Emetteur 实例中。如果温度过高,Emetteur 实例将发布事件 TemperatureTropHaute。
为了遵循推荐的事件管理方法,我们首先定义类型 TemperatureTropHauteEventArgs 来封装事件信息:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// 事件发生时的温度
public decimal Temperature { get; set; }
// 制造商
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- 第 6 行:由类 TemperatureTropHauteEventArgs 封装的信息是引发事件 TemperatureTropHaute 的温度。
类 Emetteur 如下所示:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// 观测温度
private decimal temperature;
// 数据源名称
public string Nom { get; set; }
// 事件报告
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// 温度读写
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// 向订阅者报告事件
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// 事件报告
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// 向订阅者发送事件 TemperatureTropHaute
TemperatureTropHaute(this, evt);
}
}
}
- 第5行:超过该温度阈值时,将发布事件TemperatureTropHaute。
- 第10行:发送者有一个用于识别的名称
- 第12行:事件 TemperatureTropHaute。
- 第15-26行:返回温度的get方法,以及记录温度的set方法。 当待记录的温度超过第5行的阈值时,set方法会触发发布TemperatureTropHaute事件。 它通过第29行的OnTemperatureTropHauteHandler方法发布该事件,并向其传递一个TemperatureTropHauteEventArgs对象作为参数,该对象中记录了超过阈值的温度。
- 第29-32行:发布事件TemperatureTropHaute,其第一个参数为发布者本身,第二个参数为作为参数接收到的TemperatureTropHauteEventArgs对象。
将订阅事件 TemperatureTropHaute 的类 Souscripteur 如下:
using System;
namespace Chap6 {
public class Souscripteur {
// 名称
public string Nom { get; set; }
// 事件 TemperatureTropHaute 的管理员
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// 操作员控制台显示
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 关联的方法。 该方法具有 delegate EventHandler<TEventArgs> 类型的签名,这是事件处理程序必须具备的。 该方法在控制台上显示:显示该消息的订阅者名称、报告该事件的发布者名称,以及触发该事件的温度。
- 对象 Emetteur 并未在类 Souscripteur 中订阅事件 TemperatureTropHaute。该订阅将由一个外部类完成。
程序 [Program.cs] 将所有这些元素相互关联:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// 创建事件发送器
Emetteur e1 = new Emetteur() { Nom = "e" };
// 创建包含 2 个订阅者的数组
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// 创建订阅者
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// 将其订阅来自 e1 的事件 TemperatureTropHaute
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// 通过键盘读取温度
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// 只要输入的行不为空
while (saisie != "") {
// 输入内容是否为十进制数?
if (decimal.TryParse(saisie, out temperature)) {
// 温度正确 - 将其记录
e1.Temperature = temperature;
} else {
// 报告错误
Console.WriteLine("Température incorrecte");
}
// 重新输入
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//while
}
}
}
- 第 6 行:创建发布者
- 第 8-14 行:创建两个订阅者,并将其订阅发布者 TemperatureTropHaute 的事件。
- 第 20-32 行:通过键盘输入温度的循环
- 第 24 行:如果输入的温度正确,则将其传输至对象 Emetteur e1;若温度高于 19 °C,该对象将触发事件 TemperatureTropHaute。
执行结果如下:

