前言:
通过上文(C#委托)的学习我们了解了委托的基本用法,而事件是建立在委托的基础之上的。
在了解事件之前我们要先知道一个设计模式:发布者订阅者模式
发布者订阅者模式
比如说,你订阅了某个作者,那么订阅的作者发布的文章,会广播给每一个订阅者,此时订阅的作者就是发布者(Publisher),你和每个订阅者就是订阅者(Subscriber),你们收到的文章叫做Message。
发布/订阅模式在设计模式中属于行为型模式(Behavioral Patterns)
发布者不会将消息直接发送给特定的接收者,而是通过消息通道广播出去,让订阅过消息的订阅者收到。
这种模式松耦合,也就是发布者和订阅者各尽其职,而不是使他们连接在一起,但是由于中间需要代理,增加了系统的复杂度,发布者不知道发布的消息或文章是否被订阅者接收到,增加了不稳定性。
发布/订阅模式的应用场景:
1、程序需要向大量的消费者广播
2、应用程序可以向消费者发送信息,而不需要实时响应
3、应用程序需要与一个或多个独立开发的应用程序或服务通信,而这些应用程序或服务可能使用不同的平台、编程语言和通信协议。
4、被集成的系统被设计为支持其数据的最终一致性模型(关于最终一致性:https://www.cnblogs.com/siyuanwai/p/14302444.html)
5、应用程序需要将消息传递给多个消费者,这些消费者可能具有与发送者不同的可用性要求或正常运行时间计划。例如消息在早上发布出去,消费者计划在下午才去处理这些信息。
在了解完基本的设计模式之后,我们了解一些事件的重要概念:
发布者(publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
订阅者(subscriber):注册并在事件发生时得到通知的类或结构
事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。其定义在事件所在的类或结构中,也可以定义在不同类或结构中。
触发事件(raise):调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。
声明事件
class Incrementer
{
//在类中声明,需要委托类型的名称,
//声明为public,这样就可以让其他类和结构在其上面注册事件处理程序
//不能使用new关键字创建它的对象。
public event EventHandler Counted;
//EventHandler是事件委托,底层也是delegate,多被声明为event
//声明多个事件
public event EventHandler MyEvent1, MyEvent2, MyEvent3;
//同时事件也可以声明为静态
public static event EventHandler StaticEvent;
}
事件和方法、属性一样,是类或结构的成员,同时注意:
不能在一段可执行代码中声明事件,必须声明在类或结构中。
订阅事件:
订阅者向事件添加事件处理程序。
使用+=来进行添加,事件处理程序位于+=的右面
事件处理程序的形态:实例方法的名称、静态方法的名称、匿名方法、Lambda
例如:
class Incrementer
{
public event EventHandler Counted;
//触发事件函数
public void DoCount()
{
Counted(null,null);
}
}
class Program
{
static void staticCount(object? sender, EventArgs e)
{
Console.WriteLine("静态方法被调用");
}
static void Main(string[] args)
{
Incrementer incrementer = new Incrementer();
//添加静态方法
incrementer.Counted += staticCount;
//委托类型
incrementer.Counted += new EventHandler(staticCount);
//lambda
incrementer.Counted += (object? sender, EventArgs e) => Console.WriteLine("lambda被调用");
//匿名方法
incrementer.Counted += delegate { Console.WriteLine("匿名方法被调用"); };
//触发事件
incrementer.DoCount();
}
}
输出
静态方法被调用
静态方法被调用
lambda被调用
匿名方法被调用
触发事件
事件成员本身只是保存了需要被调用的时间处理程序,如果时间没有被触发,什么都不会发生。
在发布者类中定义触发事件的代码:
if(DoCount!=null){
DoCount(source,args);
}
DoCount:事件名
source,args:参数列表
在触发事件事前建议和null进行比较,从而查看是否包含事件处理程序,如果不包含(为null),则不执行
其中触发事件 的参数列表中必须与事件的委托类型相匹配。
举个简单的例子,我们先创建一个发布者类(Incrementer)和一个订阅者类(Dozens),
(这里回顾一下,订阅者负责订阅事件和声明事件处理程序,发布者创建事件并发布)
我们通过main函数创建发布者对象publisher,然后创建一个订阅者,声明事件处理程序并且订阅事件,然后触发事件,得到触发事件后的效果。
delegate void Handler();//声明的委托
class Incrementer
{//发布者
//创建事件并发布
public event Handler CountedDozen;
//触发事件(调用事件)
public void DoCount()
{
for (int i = 0; i < 10; i++)
{//设置触发10次
if (CountedDozen != null)
{//如果事件不为空则触发事件一次
CountedDozen();
}
}
}
}
class Dozens
{//订阅者
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
//订阅事件
incrementer.CountedDozen += AddDozensCount;
}
//声明事件处理程序
void AddDozensCount()
{
this.DozensCount++;
}
}
class Class1
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozens = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"DozensCount Value is {dozens.DozensCount}");
}
}
输出:
DozensCount Value is 10
可以看到发布者在事件发生时触发了10次事件,所以自增10次,得到的结果是10.
标准事件
GUI编程是事件驱动的,在程序运行时,其可以在任何时候被事件打断,比如你按下按钮的时候,按下某个按键的时候。在这些情况发生时,程序需要处理完事件之后才能继续做其他事。
程序事件的异步处理是使用C#事件的最佳场景,对于事件,.NET提供了一个标准事件。在System命名空间下的EventHandler委托类型。
namespace System
{
//
// 摘要:
// Represents the method that will handle an event that has no event data.
// 表示将处理没有事件数据的事件的方法。
// 参数:
// sender:
// The source of the event.事件的来源
//
// e:
// An object that contains no event data.不包含事件数据的对象
public delegate void EventHandler(object? sender, EventArgs e);
}
其中
第一个参数用来保存触发事件的对象的引用。
第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
EvenArgs表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。
EventArgs参数用于不需要传递数据的事件处理程序,如果需要传递数据,需要声明一个派生自EventArgs的类,使用合适的字段来保存需要传递的数据。
我们可以修改上面的例子代码,使其使用EventHandler类型的委托(一定要注意类型匹配):
class Incrementer
{//发布者
//创建事件并发布
public event EventHandler CountedDozen;
//触发事件(调用事件)
public void DoCount()
{
for (int i = 0; i < 10; i++)
{//设置触发10次
if (CountedDozen != null)
{//如果事件不为空则触发事件一次
CountedDozen(this,null);
}
}
}
}
class Dozens
{//订阅者
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
//订阅事件
incrementer.CountedDozen += AddDozensCount;
}
//声明事件处理程序
void AddDozensCount(object? sender, EventArgs e)
{
this.DozensCount++;
}
}
class Class1
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozens = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"DozensCount Value is {dozens.DozensCount}");
}
}
一共改了四处,第一处删除了之前声明的委托类型,第二处修改了声明事件处理程序的参数,第三处和第四处修改了发布者中的创建时间类型,以及触发事件传入的参数。
EventArgs传参
自定义类:
public class IncrementerEventArgs : EventArgs
{
public int IterationCount { set; get; }//存储整数
}
IncrementerEventArgs 是我们自定义的类
EventArgs是我们的基类
继承了EventArgs之后我们就可以对事件处理程序的第二个参数传递数据,我们需要用到泛型的委托(EventArgs<>)来获得该类:
public event EventHandler<IncrementerEventArgs> CountedADozen;
CountedADozen是我们声明的事件名称
EventHandler代表泛型委托使用我们自定义的类(IncrementerEventArgs)
我们在上面的代码中加入我们自定义的类型试试:
public class IncrementerEventArgs : EventArgs
{
public int IterationCount { set; get; }//存储整数
}
class Incrementer
{//发布者
//创建事件并发布
//使用自定义类的泛型委托
public event EventHandler<IncrementerEventArgs> CountedDozen;
//触发事件(调用事件)
public void DoCount()
{
//自定义类的对象
IncrementerEventArgs args = new IncrementerEventArgs();
for (int i = 0; i < 10; i++)
{//设置触发10次
if (CountedDozen != null)
{//如果事件不为空则触发事件一次
args.IterationCount = i;
//触发时传递我们自定义类对象的参数
CountedDozen(this,args);
}
}
}
}
class Dozens
{//订阅者
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
//订阅事件
incrementer.CountedDozen += AddDozensCount;
}
//声明事件处理程序
void AddDozensCount(object? sender, IncrementerEventArgs e)
{
Console.WriteLine($"this is {e.IterationCount} in {sender.ToString()}");
this.DozensCount++;
}
}
class Class1
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozens = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine($"DozensCount Value is {dozens.DozensCount}");
}
}
输出:
this is 0 in EventStu.Incrementer
this is 1 in EventStu.Incrementer
this is 2 in EventStu.Incrementer
this is 3 in EventStu.Incrementer
this is 4 in EventStu.Incrementer
this is 5 in EventStu.Incrementer
this is 6 in EventStu.Incrementer
this is 7 in EventStu.Incrementer
this is 8 in EventStu.Incrementer
this is 9 in EventStu.Incrementer
DozensCount Value is 10
移除事件处理程序
这个没啥说的,就是在用完了事件处理程序之后,把其从事件中移除,我们可以使用-=把其从事件中移除。
同时与委托相同,如果一个处理程序向事件注册了多次,那么执行移除处理程序时,只移除该处理程序中的最后一个实例
事件访问器
+= 和-=是事件中允许 的唯一运算符,但是这些运算符可以进行预定义行为,也就是说我们可以修改这些运算符的行为,使用他们时可以让事件执行任何我们希望执行的自定义代码。
改变这两个运算符的操作就需要事件定义事件访问器
其中有两个访问器:add和remove,声明访问器的方式与声明属性类似。
public event EventHandler CountedADozen
{
add
{
//执行+=运算符的代码
}
remove
{
//执行-=运算符的代码
}
}
声明事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。
注意:事件访问器中为void方法
两个访问器都含有名称为value的隐式值参数,可以接收实例或静态方法的引用,
就是像文档中这样直接使用value隐式值参数:
event EventHandler IDrawingObject.OnDraw
{
add
{
lock (objectLock)
{
PreDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
PreDrawEvent -= value;
}
}
}