十一、事件(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 管理
posted @ 2025-08-26 10:06  世纪末の魔术师  阅读(13)  评论(0)    收藏  举报