一、委托:方法的 “容器”,实现代码的解耦
委托(Delegate)可以理解为方法的 “类型安全指针”,它定义了方法的签名(返回值 + 参数列表),可以用来封装、传递具有相同签名的方法,让方法调用和具体实现解耦。
1. 委托的核心定义
- 对具有相同返回类型和参数列表的方法进行封装,是一种 “引用方法” 的数据类型。
- 核心价值:实现代码的松耦合设计,让调用者不需要关心方法的具体实现,只需要遵循委托的签名即可。
- 支持多播:一个委托实例可以绑定多个方法,调用时会按顺序执行所有绑定的方法。
2. 委托的语法与调用
(1)定义委托
语法格式非常固定:
// 定义委托:public delegate 返回值类型 委托名(参数列表); public delegate void MyDelegate(string message);这行代码就定义了一个委托,它只能接收 “无返回值、参数为 string” 的方法。
(2)调用委托
委托有两种调用方式,对应同步和异步场景:
- 同步调用:两种写法等价
- 直接调用委托实例:
myDelegate("Hello"); - 调用
Invoke()方法:myDelegate.Invoke("Hello");
- 直接调用委托实例:
- 异步调用:两种实现方式
- 传统模式:
BeginInvoke()启动异步调用 +EndInvoke()等待结果 - 现代推荐:使用
Task封装异步委托,更简洁易读。
- 传统模式:
(3)多播委托:绑定多个方法
多播委托支持通过+=绑定多个方法,-=解绑方法,调用时会按绑定顺序依次执行:
// 绑定多个方法 MyDelegate del = Method1; del += Method2; del += Method3; // 调用时会依次执行Method1、Method2、Method3 del("多播委托测试"); // 解绑方法 del -= Method2;3. 委托的核心作用
委托的本质就是解耦方法调用与实现:举个例子,你写了一个日志工具类,它不需要关心谁来记录日志,只需要接收一个符合签名的委托方法即可,业务层可以自由实现 “写文件日志”“写数据库日志”“控制台打印日志”,而日志工具类完全不用修改代码,完美实现了开闭原则。
二、事件:基于委托的 “发布 - 订阅” 模型
事件(Event)是在委托的基础上,封装了一层 “发布 - 订阅” 的安全壳,专门用于实现通知机制,比如 WinForm 里的按钮点击、文本框变更,本质都是事件驱动。
1. 事件的核心定义
事件是一种受限的委托,它遵循 “发布者 - 订阅者” 模式:
- 发布者:触发事件的对象,只负责 “通知发生了”,不关心谁订阅了事件。
- 订阅者:响应事件的对象,只需要注册事件处理方法,不需要关心发布者的内部实现。这种模式天然实现了模块之间的解耦,是 UI 交互、状态通知的核心。
2. 事件的语法实现
(1)定义事件:三种常用方式
- 直接使用
event关键字封装委托// 先定义委托 public delegate void ClickHandler(object sender, EventArgs e); // 定义事件 public event ClickHandler Click; - 使用预定义的
EventHandler委托(通用场景)// 系统预定义的无参数事件委托,直接使用 public event EventHandler Click; - 自定义事件参数:使用
EventHandler<TEventArgs>当需要传递自定义数据时,先定义EventArgs的子类,再使用泛型委托:// 自定义事件参数,继承EventArgs public class ButtonClickEventArgs : EventArgs { public string ButtonName { get; set; } } // 定义事件 public event EventHandler<ButtonClickEventArgs> Click;
(2)触发事件
发布者在满足条件时触发事件,需要先判断事件是否为null(即是否有订阅者):
protected virtual void OnClick(ButtonClickEventArgs e) { // 触发事件,sender是发布者自身,e是事件参数 Click?.Invoke(this, e); }(3)订阅事件:多种实现方式
- 最常用:直接用
+=绑定方法// 订阅按钮点击事件 button.Click += Button_Click; private void Button_Click(object sender, EventArgs e) { Console.WriteLine("按钮被点击了!"); } - 其他场景:
- 基类中的事件:子类可以直接重写触发方法(如 WinForm 的
OnClick) - 接口中定义事件:让不同的类实现统一的事件通知规范
- 自定义方法访问器:用
add/remove关键字自定义事件的订阅和解绑逻辑
- 基类中的事件:子类可以直接重写触发方法(如 WinForm 的
3. 事件的典型使用场景
- WinForm/WPF UI 交互:按钮点击、文本变更、窗口加载等,是事件最经典的应用场景。
- 模块解耦:比如业务逻辑层触发数据变更事件,UI 层订阅并更新界面,二者完全解耦。
- 日志 / 监控 / 状态通知:比如服务状态变更时,触发事件通知日志模块、监控模块执行对应操作。
三、事件与委托的异同:别再傻傻分不清了
很多人学完委托和事件,还是搞不清二者的区别,其实它们是 “基础与封装” 的关系,核心异同点整理如下:
表格
| 维度 | 委托 | 事件 |
|---|---|---|
| 相同点 | 1. 底层基于委托机制实现2. 都支持多播(+=/-=绑定方法)3. 核心用途都是回调、通知、解耦 | 同左 |
| 不同点:访问控制 | 可以被外部直接调用(任何持有委托对象的代码都能调用) | 只能由定义事件的类内部触发,外部只能通过+=/-=订阅 / 解绑,不能直接调用,更安全 |
| 不同点:封装性 | 只是一个方法指针类型,没有额外的封装限制 | 隐式生成私有委托字段,外部无法直接访问底层委托,避免被恶意调用或清空 |
| 不同点:接口支持 | 不能在接口中定义委托 | 可以在接口中定义事件,比如IButton接口定义Click事件,不同按钮类实现接口即可 |
| 不同点:使用场景 | 更通用,适合需要传递方法、异步调用、简单回调的场景 | 专门用于发布 - 订阅的通知场景,比如 UI 事件、状态变更通知 |
简单总结一句话:事件是 “安全版的委托”,它限制了委托的调用权限,只允许发布者内部触发,外部只能订阅,更适合事件驱动的场景;而委托更灵活,适合需要直接传递和调用方法的场景。
四、实战小例子:用事件实现一个简单的按钮点击
结合上面的知识点,写一个极简的按钮事件 Demo,直观感受事件的使用:
using System; // 自定义按钮类 public class MyButton { // 定义事件,使用泛型EventHandler public event EventHandler<string> Click; // 模拟按钮点击,触发事件 public void SimulateClick() { Console.WriteLine("按钮被点击,准备触发事件..."); // 触发事件,传递按钮名称 Click?.Invoke(this, "提交按钮"); } } // 订阅者:日志模块 public class LogModule { public void OnButtonClick(object sender, string buttonName) { Console.WriteLine($"日志记录:{buttonName} 被点击了"); } } // 订阅者:业务处理模块 public class BusinessModule { public void OnButtonClick(object sender, string buttonName) { Console.WriteLine($"业务处理:{buttonName} 点击,准备提交数据"); } } class Program { static void Main(string[] args) { // 发布者 MyButton button = new MyButton(); // 订阅者 LogModule log = new LogModule(); BusinessModule business = new BusinessModule(); // 订阅事件 button.Click += log.OnButtonClick; button.Click += business.OnButtonClick; // 模拟点击,触发所有订阅的方法 button.SimulateClick(); Console.ReadLine(); } }运行结果:
按钮被点击,准备触发事件... 日志记录:提交按钮 被点击了 业务处理:提交按钮 点击,准备提交数据可以看到,按钮类完全不用关心有多少订阅者,只需要触发事件即可,订阅者也只需要实现自己的处理逻辑,二者完全解耦,这就是事件的魅力。
五、总结
委托和事件是 C# 中实现面向对象设计、事件驱动架构的核心,理解它们不仅能帮你写出更解耦、更易维护的代码,也是学习异步编程、框架底层(比如ASP.NET Core 的管道、WPF 的依赖属性)的基础。
记住两个核心要点:
- 委托是 “方法的容器”,负责封装和传递方法,实现松耦合。
- 事件是 “安全的委托封装”,专门用于发布 - 订阅的通知场景,限制了调用权限,更安全。
掌握了这两个概念,你会发现很多以前看不懂的框架代码,瞬间就通了!