02011501 事件
02011501 事件
1. 发布者和订阅者
- 很多程序都有一个共同的需求,即当一个特点的程序事件发生时,程序的其它部分可以得到该事件已经发出的通知。
- 发布者/订阅者模式:订阅者类通过向发布者提供一个方法来“注册”以获取通知,当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
- 由订阅者提供的方法称为回调方法。因为发布者通过执行这些方法来“往回调用订阅者的方法”。回调方法还可以称为事件处理程序,因为它们是为处理事件而调用的代码。
- 发布者(Publicsher):发布某个事件的类或结构,其它类可以在该事件发生时得到通知。
- 订阅者(Subscriber):注册并在事件发生时得到通知的类或结构。
- 事件处理程序(Event Handler):由订阅者注册到事件的方法,在发布者触发事件时执行。
- 事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
- 触发(Raise)事件,是调用(Invoke)或触发(Fire)事件的术语。所有注册到它的方法都会被依次调用。
2. 事件与委托的关系
- 事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。
- 委托和事件的行为之所以相似,是有充分理由的。事件包含了一个私有的委托。

事件有被封装的委托
- 有关事件的私有委托相关重要事项如下。
-
事件提供了对它的私有控制委托的结构化访问。
- 也就是说,你无法直接访问委托。
-
事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。
-
事件被触发时,它调用委托来一次调用列表中的方法。
-
在上图中,只有+=和-=运算符在事件框的左边,这是因为它们是事件唯一允许的操作。
-
3. 具有一个事件的类的结构和术语

- 上图演示了一个叫做Incrementer的类,它按照某种方式进行计数。
- Incrementer定义了一个CountedADozen事件,每次积累到12个项时将会触发该事件。
- 订阅者Dozens和SomeOtherClass各自有一个注册到CountedADozen事件的事件处理程序。
- 每当事件触发时,都会调用这些处理程序。
4. 源代码组件概览

- 需要在事件中使用的代码有5部分,如下所示。
- 委托类型声明 → 事件和事件处理程序必须有共同的签名和返回类型。
- 它们通过委托类型进行描述。
- 事件处理程序声明 → 订阅者类中会在事件触发时执行的方法声明。
- 它们不一定是显式命名的方法,还可以是匿名方法和Lambda表达式。
- 事件声明 → 发布者类必须声明一个订阅者类可以注册的事件成员。
- 当类声明的事件为public时,称为发布了事件。
- 事件注册 → 订阅者必须注册事件才能在事件被触发时得到通知。
- 这是将事件处理程序与事件相连的代码。
- 触发事件的代码 → 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码。
- 委托类型声明 → 事件和事件处理程序必须有共同的签名和返回类型。
5. 声明事件
- 发布者类必须提供事件对象。创建事件只需要委托类型和名称。
// 事件声明的基础语法
class Incrementer
{
关键字 委托类型 事件名
↓ ↓ ↓
public event EventHanlder CounterADozen;
}
说明:
1. 上述代码中声明了一个叫做CounterADozen的事件。
2. 事件声明在一个类中。
3. 事件需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托的签名和返回类型匹配。
4. 事件声明为public,这样其它类和结构可以在它上面注册事件处理程序。
5. 事件不能使用对象创建表达式(new表达式)来创建对象。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 通过使用“,”分隔的列表在一个声明语句中声明一个以上的事件
publci event EventHanlder MyEvent1, MyEvent2, OtherEvent; // 声明3个事件
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 可以使用static关键字让事件变成静态的
public static event EventHanlder CountedADozen;
6. 事件是成员
- 一个常见的误解是把事件视为类型。事件和方法、属性一样,事件是类或结构的成员。
- 事件是成员,因此引出了如下几点重要内容。
- 由于事件是成员,我们不能在一段可执行代码中声明事件。
- 事件必须声明在类或结构中,和其它成员一样。
- 事件成员被隐式的自动初始化为null。
- 事件声明需要委托类型的名称,我们可以声明一个委托类型或使用已有的委托类型。
- 如果声明一个委托类型,它必须指定将事件注册的方法的签名和返回类型。
- BLC声明了一个叫做EventHandler的委托,专门用于系统事件。后续会介绍。
7. 订阅事件
- 订阅者向事件添加事件处理程序。对于一个要添加到事件的事件处理程序来说,它必须具有与事件的委托相同的返回类型和签名。
- 使用+=运算符来为事件添加事件处理程序,事件处理程序应该位于该运算符的右边。
- 事件处理程序的规范可以是以下任意一种。
- 实例方法的名称。
- 静态方法的名称。
- 匿名方法。
- Lambda表达式。
// 为事件添加方法基础语法
类 事件成员 实例方法
↓ ↓ ↓
incrementer.CounterADozen += IncrementDozensCount; // 实例方法引用形式
incrementer.CounterADozen += ClassB.CounterHandlerB; // 静态方法引用形式
mc.CounterADozen += new EventHandler(cc.CounterHandlerC); // 委托形式
incrementer.CountedADozen += () => DozensCount++; // Lambda表达式
incrementer.CountedADozen += delegate {DozensCount++;}; // 匿名方法
8. 触发事件
- 事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。
if(CounterADozen != null) // 确认有方法可以执行
CountedADozen(source, args); // 触发事件
↑ ↑
事件名称 参数列表
说明:
1. 上述代码触发了CountedADozen事件。
2. 在触发事件之前和null进行比较,从而查看事件是否包含事件处理程序。如果事件是null,则表示没有事件处理程序,不能执行。
3. 触发事件的语法和调用方法一样。
3.1 使用事件名称,后面跟着参数列表。
3.2 参数列表必须与事件的委托类型相匹配。
- 把事件声明和触发事件的代码放在一起便有了发布者类声明。
// 发布者类声明
class Incrementer
{
public event EventHandler CountedADozen; // 声明事件
void DoCount(object source, EventArgs args)
{
for(int i = 1; i < 100; i++)
if(1 % 12 == 0)
if(CountedADozen != null) // 确认有方法可以执行
CountedADozen(source, args); // 触发事件
}
}
9. 声明事件和触发事件整个程序示例
using System;
namespace Demo01
{
delegate void Handler(); // @1 声明委托
class Incrementer // @2 声明发布者类
{
public event Handler CountedADozen; // @3 声明事件并发布
public void DoCount()
{
for (int i = 1; i < 100; i++)
if (i % 12 == 0 && CountedADozen != null)
CountedADozen(); // 每增加12次,计数器触发事件一次
}
}
class Dozens // @4 声明订阅者类
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; // @6 订阅事件
void IncrementDozensCount() // @5 声明事件处理程序
{
DozensCount++;
}
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");
Console.ReadLine();
}
}
}
控制台输出:
Number of dozens = 8
说明:
1. Incrementer是发布者类,Dozens是订阅者类。
2. 在构造函数中,Dozen类订阅事件,将IncrementDozensCount作为事件处理程序。
10 标准事件的用法
10.1 采用系统定义的EventHandler委托
- GUI编程是事件驱动的,也就是说在程序运行时,它可以在任何时候被事件打断。
- 比如按钮点击,按下按键或系统定时器。在这些情况发生时,程序需要处理事件然后继续做其它事情。
- 程序事件的一部处理是使用C#事件的绝佳场景。
- Windows GUI编程如此广泛地使用了事件,以至于对于事件的使用,.NET框架提供了一个标准模式。
- 该标准模式的基础就是System命名空间中声明的EventHanlder类型。
- EventHandler委托类型的声明如以下代码所示。
public delegate void EventHanlder(object sender, EventArgs e);
注意,使用EventHanlder委托的重点事项:
1. 第一个参数用来保存触发事件的对象的引用,由于是object类型的,因此可以匹配任何类型的实例。
2. 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
3. 返回类型是void。
补充说明:
1. EventHandler委托类型的第二个参数是EventArgs类的对象,EventArgs类声明在System命名空间中。
2. 既然第二个参数用来传递数据,EventArgs类的对象应该可以保存某种类型的数据。这个观点是错误的,理由如下:
2.1 EventArgs不能传递任何数据,它用于不需要传递数据的事件处理程序。
2.2 如果你需要传递数据,必须声明一个派生自EventArgs的类,并且使用合适的字段来保存需要传递的数据。
3. 尽管EventArgs类实际上不传递数据,但它是使用EventHandler委托模式的重要组成部分。
3.1 不管参数的实际类型是什么,object和EventArgs类型的参数总是基类。
3.2 这样EventHandler就能提供一个对所有事情和事件处理程序都通过的签名,让所有事件都正好有两个参数,而不是各自都有不同的签名。
10.2 标准事件的用法
using System;
namespace Demo01
{
class Incrementer
{
public event EventHandler CountedADozen; // @1 使用系统定义的EventHandler委托来声明事件
public void DoCount()
{
for (int i = 1; i < 100; i++)
if (i % 12 == 0 && CountedADozen != null)
CountedADozen(this, null); // @2 this在类中表示当前类的对象,null表示忽略第二个形参。
}
}
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; // @4 订阅事件
void IncrementDozensCount(object source, EventArgs e) // @3 声明事件处理程序,必须与系统定义的EventHandler委托签名匹配。
{
DozensCount++;
}
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");
Console.ReadLine();
}
}
}
控制台输出:
Number of dozens = 8
10.3 通过EventArgs的派生类传递数据
// 第1步 → 声明一个派生自EventArgs的类
public class IncrementerEvents : EventArgs // 派生类以基类名称结尾,便于观察
{
public int IterationCount(get; set;) // 定义属性,用于存储数据
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 第2步 → 有了派生的自定义类,可以向事件处理程序的第二个参数传递数据了,所以需要一个使用新自定义类的委托类型,此时可以使用泛型版本的委托EventHandler<>。
public event EventHandler<IncrementerEventArgs> CounterADozen;
↑ ↑
泛型委托 事件名称
说明:
1. 泛型委托将自定义类的名称放到<>内。
2. 在需要使用泛型委托的地方使用整个字符串,如:EventHandler<IncrementerEventArgs>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 通过EventArgs的派生类传递数据示例
using System;
namespace Demo01
{
class IncrementerEventArgs : EventArgs // @1 创建自定义类
{
public int IterationCount { get; set; } // @2 接受数据
}
class Incrementer
{
public event EventHandler<IncrementerEventArgs> CountedADozen; // @3 使用泛型委托来声明事件
public void DoCount()
{
IncrementerEventArgs args = new IncrementerEventArgs(); // @4 创建自定义类对象
for (int i = 1; i < 100; i++)
if (i % 12 == 0 && CountedADozen != null)
{
args.IterationCount = i; // @5 通过自定义类的对象接收数据
CountedADozen(this, args); // @6 this在类中表示当前类的对象,null表示忽略第二个形参。
}
}
}
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; // @8 订阅事件
void IncrementDozensCount(object source, IncrementerEventArgs e) // @7 声明事件处理程序,必须与系统定义的EventHandler委托签名匹配。
{
Console.WriteLine($"展示接受的数据:{e.IterationCount} in {source.ToString()}"); // @9 展示自定义类接受的数据
DozensCount++;
}
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");
Console.ReadLine();
}
}
}
控制台输出:
展示接受的数据:12 in Demo01.Incrementer
展示接受的数据:24 in Demo01.Incrementer
展示接受的数据:36 in Demo01.Incrementer
展示接受的数据:48 in Demo01.Incrementer
展示接受的数据:60 in Demo01.Incrementer
展示接受的数据:72 in Demo01.Incrementer
展示接受的数据:84 in Demo01.Incrementer
展示接受的数据:96 in Demo01.Incrementer
Number of dozens = 8
说明:通过ToString()方法展示了完全限定名,后续章节介绍。
11. 移除事件处理程序
- 在用完事件处理程序之后,可以使用“-=”运算符将事件处理程序从事件中将其移除。
// 移除事件处理程序语法格式
p.SimpleEvent -= s.MethodB;
12. 事件访问器
- 事件只允许使用“+=”和“-=”运算符,这个两个运算符具有定义良好的行为。
- 然而,我们可以修改这两个运算符的行为,在使用它们时让事件执行任何我们希望执行的自定义代码。这是高级主体,本节只是简单介绍。
- 要改变“+=”和“-=”运算符的操作,必须为事件定义事件访问器。
- 有两个访问器,add和remove。
- 声明事件的访问器看上去和声明一个属性差不多。
// 具有访问器的事件声明
publci event EventHandler CountedADozen
{
add
{
... // 执行+=运算符的代码
}
remove
{
... // 执行-=运算符的代码
}
}
说明:
1. add和remove两个事件访问器都有叫做value的隐式值参数,它接受实例或静态方法的引用。
2. 声明了事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。
3. 事件访问器表现为void方法,也就是不能使用返回值的return语句。
结尾
书籍:C#图解教程
著:【美】丹尼尔 · 索利斯;卡尔 · 施罗坦博尔
译:窦衍森;姚琪琳
ISBN:978-7-115-51918-4
版次:第5版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※