十一、事件(Event)
CLR #事件
✅ 第11章事件
🌟 一、事件的本质与设计目标
🧠 本质:事件是对“委托”的一种语法封装,用于实现发布-订阅模式
- 事件 = 安全的委托公开接口(只能
+=/-=,不能直接调用)
📌 核心设计目的:
- 发布者控制对事件的访问(不能外部直接调用)
- 支持多播回调
- 提高封装性
🔁 二、发布者如何设计一个事件
🧩 典型写法:
public class MailManager
{
public event EventHandler<NewMailEventArgs> NewMail;
protected virtual void OnNewMail(NewMailEventArgs e)
{
NewMail?.Invoke(this, e); // null 条件调用 + sender 传 this
}
public void SimulateNewMail(string from, string to, string subject)
{
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
OnNewMail(e);
}
}
public class NewMailEventArgs : EventArgs
{
public string From, To, Subject;
public NewMailEventArgs(string from, string to, string subject)
{
From = from; To = to; Subject = subject;
}
}
🧠 重点说明:
event封装了+=/-=的访问器- 回调方法签名:
void Handler(object sender, TEventArgs e) - 利用
?.Invoke()实现线程安全(简洁写法)
🧑💼 三、订阅者如何响应事件
public class Fax
{
public Fax(MailManager mm)
{
mm.NewMail += FaxMsg;
}
void FaxMsg(object sender, NewMailEventArgs e)
{
Console.WriteLine($"Fax received mail from {e.From} to {e.To}");
}
}
🧠 小技巧:
- 最好在
Dispose或析构中取消订阅,防止内存泄露:
mm.NewMail -= FaxMsg;
🔍 四、编译器背后的实现机制(图示)
🌐 事件编译结构(Mermaid 图):
classDiagram
class MailManager {
- EventHandler<NewMailEventArgs> NewMail
+ add_NewMail(handler)
+ remove_NewMail(handler)
# OnNewMail()
}
编译器将
event生成私有字段 + add/remove 方法,封装委托访问。
💡 五、性能优化:显式事件实现(EventSet)
适用于:大量事件但实际只触发少量者(如 WinForms)
class MyControl {
private Dictionary<string, Delegate> _events = new();
public event EventHandler Click {
add { AddHandler("Click", value); }
remove { RemoveHandler("Click", value); }
}
private void Raise(string name, EventArgs e)
{
if (_events.TryGetValue(name, out var d))
((EventHandler)d)?.Invoke(this, e);
}
}
你的问题非常好,也非常关键。我们来深入解释显式事件(EventSet)为何能优化性能和节省内存,并举例说明它适合的场景。
🌟 5.1、传统事件实现的开销问题
在默认的事件实现中,每个 event 字段其实背后是一个 多播委托字段(MulticastDelegate):
public class Control
{
public event EventHandler Click;
public event EventHandler MouseMove;
public event EventHandler Resize;
public event EventHandler Focus;
// 每个事件都会在对象中额外分配一个字段(4 个字段)
}
🧨 问题:
即使这些事件从未被订阅,每个对象依然会额外占用内存去保存这些委托字段(初始为 null),而当对象数量非常多时(如 10,000 个 UI 控件),这些“未使用”的事件字段就造成大规模内存浪费。
🧠 5.2、显式事件实现(EventSet)的优化点
显式事件使用一个共享的字典(或类似结构)来存储事件委托,仅当事件被订阅时才真正分配空间。
public class Control
{
private Dictionary<string, Delegate> _events = new();
public event EventHandler Click
{
add => AddHandler("Click", value);
remove => RemoveHandler("Click", value);
}
void AddHandler(string key, Delegate handler)
{
if (_events.TryGetValue(key, out var existing))
_events[key] = Delegate.Combine(existing, handler);
else
_events[key] = handler;
}
void RemoveHandler(string key, Delegate handler)
{
if (_events.TryGetValue(key, out var existing))
{
var newDelegate = Delegate.Remove(existing, handler);
if (newDelegate == null)
_events.Remove(key);
else
_events[key] = newDelegate;
}
}
}
✅ 优势:
| 点 | 优化说明 |
|---|---|
| 🧠 延迟分配 | 只有事件第一次被订阅时才创建委托 |
| 🧹 节省内存 | 1 个字典代替 N 个字段(未被使用的事件不占内存) |
| 🧵 更易扩展 | 可动态添加任意数量事件,适合事件种类多但大部分不使用的系统 |
🔧 5.3、适用场景
非显式事件适合:
- 少量事件
- 每个对象实例都可能使用所有事件(比如简单游戏脚本)
显式事件适合:
- 事件种类多(上百个),但每个对象通常只用少量(如 WinForms 控件、UI 系统组件)
- 控件类库、可扩展插件框架
🎯 总结一句话
显式事件通过只在“实际使用时”分配资源,避免了每个对象预留所有事件字段所造成的内存浪费,从而在大规模对象场景下显著优化内存使用。
🏁 六、总结
| 内容 | 描述 | 示例 |
|---|---|---|
event |
封装委托,防止外部调用 | public event EventHandler<MyArgs> Foo; |
| 事件参数类 | 派生自 EventArgs,携带数据 |
class MyArgs : EventArgs {} |
| 触发方法 | 使用 ?.Invoke(this, e) |
OnFoo(new MyArgs(...)) |
| 订阅 / 取消 | += / -= 绑定回调方法 |
publisher.Foo += Handler; |
| 显式事件 | 节省内存,只注册被用事件 | 使用 Dictionary 管理 |
点赞鼓励下,(づ ̄3 ̄)づ╭❤~
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号