一篇博客彻底弄清楚C#中的事件
链接: 源码
1. 什么是事件
C#中的事件就像一个广播电台,当某个对象(比如按钮)发生了特定的事情(比如被点击),它就会“播出”一条消息。其他对象(比如窗体)如果对这条消息感兴趣,就可以“订阅”这个事件,就像收听某个电台一样。一旦事件发生,所有订阅了该事件的对象都会收到通知,并执行预先准备好的应对动作(比如弹出确认对话框)。这样,对象之间通过事件就能互相沟通,却不直接干涉彼此的内部运作,保持了代码的模块化和独立性。
C# 事件是面向对象编程中的一个重要特性,用于实现对象之间的通信和协作。事件允许一个对象(称为“发布者”或“事件源”)通知其他对象(称为“订阅者”或“事件处理器”)关于特定情况的发生。这种设计模式使得代码更具模块化和松耦合性,因为发布者不需要直接了解订阅者的具体实现细节。
事件是带有约束的委托实例.
2. 委托和事件的关系
事件和委托在C#中密切相关,它们共同构成了事件驱动编程的基础。下面详细阐述它们的联系与区别:
联系
-
基于委托:事件是基于委托的一种实现。每个事件都关联一个特定的委托类型,这个委托定义了事件处理程序的签名(返回类型、方法名、参数列表)。事件处理程序必须与该委托兼容才能被正确订阅。
public delegate void MyDelegate(string message); public class EventPublisher { public event MyDelegate MyEvent; }
在上述例子中,
MyEvent
事件基于MyDelegate
委托类型。 -
共享相同语法:事件的订阅(
+=
)和取消订阅(-=
)操作与委托的组合(+=
)和解除组合(-=
)操作语法相同。// 订阅事件 publisher.MyEvent += HandleMyEvent; // 取消订阅事件 publisher.MyEvent -= HandleMyEvent; // 相同语法用于委托 myDelegate += AnotherMethod; myDelegate -= AnotherMethod;
-
共同实现观察者模式:事件和委托一起实现了观察者模式,其中委托充当了“消息”接口,事件则提供了订阅和发布机制。发布者通过触发事件来通知订阅者,订阅者通过提供符合委托签名的方法作为事件处理程序来响应事件。
区别
-
语义和用途:
- 委托:是一种类型安全的函数指针,用于封装方法引用。它可以单独使用,支持多播(即一个委托实例可以包含多个方法),常用于回调、策略模式、函数式编程等场景,允许在运行时动态地改变行为。
- 事件:是一种特殊的成员,设计用于实现发布/订阅(Observer)模式,提供了一种安全、可控的方式让一个对象通知其他对象其状态的改变。事件通常在类的外部只能订阅和取消订阅,而触发(引发)事件的权限仅限于事件所在的类内部。
-
访问控制:
- 委托:作为一个类型或字段,其访问修饰符可以自由设定(如
public
、private
等),可以在类的内外部被赋值、调用或测试是否为null
。 - 事件:虽然在语法上事件看起来像是一个字段,但实际上编译器会生成专用的访问器方法(
add
和remove
)来控制对事件的订阅和取消订阅。这些方法默认为public
,但事件本身通常声明为public
、protected
或internal
,以控制外部访问。外部代码不能直接访问或触发事件,只能通过+=
和-=
操作符订阅和取消订阅。
- 委托:作为一个类型或字段,其访问修饰符可以自由设定(如
-
触发机制:
- 委托:可以直接调用,无论在类的内部还是外部。
- 事件:只能在类的内部通过
eventVariable?.Invoke(args)
或类似的语法触发。外部代码无法直接触发事件,这是为了确保事件的触发逻辑完全由事件拥有者控制。
-
安全性与封装:
- 委托:更灵活但也更暴露。如果一个公共字段是委托类型,任何代码都可以直接赋值、清空或调用它,可能导致意外的行为或安全问题。
- 事件:提供了更高的封装性和安全性。外部代码只能通过
+=
和-=
操作符添加或移除事件处理程序,不能直接访问或修改事件背后的委托实例,也不能直接触发事件。
-
编译器支持:
- 委托:编译器不会为普通委托生成额外的辅助方法。
- 事件:编译器会为事件生成私有委托字段(存储实际的事件订阅者列表)以及
add
和remove
访问器方法,这些方法负责维护订阅者列表的安全访问。
总结来说,事件和委托都是C#中用于处理方法调用的机制,它们紧密相关且都服务于事件驱动编程。委托提供了方法封装和多播能力,而事件在此基础上增加了专门的访问控制、触发规则和编译器支持,形成了一个更适合实现对象间事件通知的高级抽象。事件确保了发布者与订阅者之间的解耦,以及对事件触发逻辑的严格控制。
3. 如何定义事件
1. 使用 event
关键字结合自定义委托类型
这是最标准的定义事件的方式,先定义一个符合需求的自定义委托类型,然后在类中使用 event
关键字声明一个基于该委托类型的事件。
// 定义一个自定义委托类型
public delegate void CustomEventHandler(string message, bool isImportant);
//public event Action MyEvent;//用系统自带的Action委托也可以
public class MyClass
{
// 使用自定义委托类型定义事件
public event CustomEventHandler ImportantMessageReceived;
}
2. 使用 event
关键字结合预定义的泛型委托
C#提供了几个预定义的泛型委托,如 EventHandler
、EventHandler<TEventArgs>
等,它们非常适合用于定义通用事件。使用这些预定义的委托可以简化事件定义,并遵循.NET框架的标准约定。
// 定义一个事件参数类,继承自 EventArgs
public class CustomEventArgs : EventArgs
{
public string Message {
get; set; }
public bool IsImportant {
get; set; }
}
public class MyClass
{
// 使用预定义的泛型委托EventHandler<TEventArgs>定义事件
public event EventHandler<CustomEventArgs> ImportantMessageReceived;
}
3. 使用 event
关键字结合匿名方法或Lambda表达式(仅限事件订阅者)
虽然不能直接使用匿名方法或Lambda表达式来定义事件本身(因为事件是类的成员,需要一个明确的类型),但可以在订阅事件时使用它们来创建事件处理程序。这样可以在订阅事件时简洁地定义处理逻辑。
public class MyClass
{
public event EventHandler<CustomEventArgs> ImportantMessageReceived;
}
var myObject = new MyClass();
myObject.ImportantMessageReceived += (sender, e) =>
{
Console.WriteLine($"Received message: {
e.Message}, Importance: {
e.IsImportant}");
};
总结起来,定义事件的主要方法包括使用 event
关键字结合自定义委托类型、预定义的泛型委托,以及在订阅事件时使用匿名方法或Lambda表达式来定义处理逻辑。在特殊情况下,还可以使用属性样式的事件定义来实现更复杂的控制逻辑。在大多数常规应用场景中,前两种方法(自定义委托类型或预定义泛型委托)是最常用的选择。
4. 事件的触发(引发)
事件由事件源类在其内部逻辑中通过调用 EventHandler.Invoke()
方法或使用 +=
运算符来触发。通常,事件的触发封装在一个受保护的(protected
)方法内,以便派生类可以访问。这个方法被称为“** raiser**”方法,通常以 On
开头。
public class PublisherClass
{
// ...
protected virtual void OnCustomEvent(MyEventArgs e)
{
CustomEvent?.Invoke(this, e);
}
private void SomeInternalMethod()
{
// 当满足特定条件时,触发事件
var eventArgs = new MyEventArgs(...);
OnCustomEvent(eventArgs);
}
}
5. 事件订阅与取消订阅
所谓事件订阅,简单来说,就是事件绑定某个方法,我们就说该方法订阅了该事件.
订阅者通过将一个符合委托签名的方法(事件处理程序)赋值给事件来注册对事件的监听。这通常使用 +=
运算符完成。同样,使用 -=
运算符可以取消订阅事件。
public class SubscriberClass
{
private PublisherClass _publisher;
public SubscriberClass(PublisherClass publisher)
{
_publisher = publisher;
// 订阅事件
_publisher.CustomEvent += HandleCustomEvent;
}
private void HandleCustomEvent(object sender, MyEventArgs e)
{
// 在这里处理事件逻辑
}
// 取消订阅事件
public void UnsubscribeFromEvent()
{
_publisher.CustomEvent -= HandleCustomEvent;
}
}
6. 事件的使用场景
事件常用于以下场景:
- 用户界面编程:如按钮点击、文本框文本变化、窗口关闭等。
- 系统级通知:如文件系统监控、网络连接状态变更、定时任务完成等。
- 组件间通信:在模块化设计中,不同组件可以通过事件来交换信息而不直接依赖对方的实现细节。
7. 最佳实践
- 避免空引用异常:在触发事件时,通常使用
EventHandler?.Invoke()
的形式,以防止在没有订阅者时引发NullReferenceException
。 - 使用有意义的事件参数:通过自定义
EventArgs
类传递与事件相关的详细信息。 - 保持事件处理程序简洁:避免在事件处理程序中执行耗时操作,尤其是对于UI线程上的事件。考虑使用异步处理或任务调度。
- 遵循约定:如事件名一般采用过去分词形式(如
Clicked
、Changed
), raiser 方法以On
开头等。
8. 综合案例
下面是一个综合案例, 小伙伴对着Program.cs 敲一遍代码,C#中的事件相关的知识应该差不多了. 代码源码链接在文章开头.
public class MyEventClass
{
public event Action MyEvent;//声明事件
protected virtual void OnMyEvent() //触发事件的方法
{
MyEvent?.Invoke(); //如果事件有订阅者(挂载了方法),通知订阅者(执行挂载的方法)
}
public void DoSomething() //模拟事件发布的方法
{
Console.WriteLine("Doing Something...");
OnMyEvent();
}
}
public class MyEventArgs : EventArgs
{
public string CustomData;
public MyEventArgs(string customData)
{
CustomData = customData;
}
}
public class MyEventListener
{
public void HandleMyEvent()
{
Console.WriteLine("MyEvent was raised!");
}
}
public class Subscribe
{
public static void OnSubscribe(object sender, EventArgs e)
{
Console.WriteLine("订阅者收到消息");
Console.WriteLine(sender.GetType().Name);
Console.WriteLine(e.ToString());
}
}
public class MyClass
{
public event EventHandler MyEvent;
public void EventSignal()
{
Console.WriteLine("触发事件的信号");
MyEvent?.Invoke(this, null);
}
public void SubscribeEvent(object? sender, EventArgs e)
{
Console.WriteLine("事件被订阅");
}
}
/// <summary>
/// 自己写委托,自己定义类,委托带参数
/// </summary>
public class MyClass2
{
public delegate void MyDelgate(object? sender, EventArgs e);
public event MyDelgate MyEvent;
public void MyFunc(object? sender, EventArgs e)
{
Console.WriteLine($"发送者是{
sender}");
Console.WriteLine($"发送的参数是{
e}");
}
public void EventSignal()
{
Console.WriteLine("触发事件的信号发射了->");
MyEvent?.Invoke(this, null);
}
}
/// <summary>
/// 自己写委托,自己定义类,委托带不参数
/// </summary>
public class MyClass3
{
public delegate void MyDelgate();
public event MyDelgate MyEvent;
public void MyFunc()
{
Console.WriteLine("我是订阅者中的方法");
}
public void EventSignal()
{
Console.WriteLine("触发事件的信号发射了->");
MyEvent?.Invoke();
}
}
//步骤:
//1.事件声明
//2.事件绑定订阅者
//3.事件触发
//
//注意事项:
//1.事件绑定的方法,需要和事件的委托方法严格一致
//2.通过EventArgs可以携带参数
public class GenericEventHandler
{
public event EventHandler<MyEventArgs> MyEvent;//声明事件
public virtual void OnMyEvent(object sender, MyEventArgs e)
{
//事件处理逻辑
Console.WriteLine("doing something...");
Console.WriteLine(e.CustomData);
Console.WriteLine("done");
MyEvent?.Invoke(this, e);//如果事件有订阅者,则激活事件,将当前对象和事件参数e作为参数传递
}
}
//Program.cs
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using 事件;
{
//用系统自带的委托Action声明事件
MyEventClass eventSource = new MyEventClass();//创建事件源对象
MyEventListener listener = new MyEventListener();//创建事件监听器对象
eventSource.MyEvent += listener.HandleMyEvent; //订阅事件(事件绑定某个方法)
eventSource.DoSomething();
eventSource.MyEvent -= listener.HandleMyEvent;
}
{
//用关键字EventHandler<T>声明泛型事件
GenericEventHandler eventHandlerTest = new GenericEventHandler();
eventHandlerTest.MyEvent += Subscribe.OnSubscribe;
eventHandlerTest.OnMyEvent(null, new MyEventArgs("hello,keson"));
}
{
//用关键字EventHandler声明事件
MyClass myClass = new MyClass();
myClass.MyEvent += myClass.SubscribeEvent;
myClass.EventSignal();
}
{
//通过自定义委托,声明带参数事件
MyClass2 myClass = new MyClass2();
myClass.MyEvent += myClass.MyFunc;
myClass.EventSignal();
}
{
//通过自定义委托,声明不带参数事件
MyClass3 myClass = new MyClass3();
myClass.MyEvent += myClass.MyFunc;
myClass.EventSignal();
}