委托
委托是一种类型安全的函数指针,它定义了方法的签名(参数列表和返回值类型),可以指向任何与该签名匹配的方法(包括静态方法和实例方法)。
MulticastDelegate
类是 .NET 中表示多播委托的抽象基类,它允许一个委托实例调用多个方法。
但我们不需要直接实现它。相反,我们应该:
- 使用内置委托类型:如
Action
、Func
系列委托 - 定义自定义委托:使用
delegate
关键字 - 利用事件机制:这是最常用的多播委托应用场景
- 使用委托操作符:
+
、+=
、-、-=
来组合和移除委托
方法
• Equals(object? obj) (override)
• 用途:判断当前多播委托与指定对象是否相等。
• 返回值:如果两个委托具有相同的调用列表,则返回 true;否则返回 false。
• 特点:该方法被密封(sealed),不能被进一步重写。• GetHashCode() (override)
• 用途:获取当前实例的哈希码。
• 返回值:32位有符号整数形式的哈希码。
• 特点:该方法也被密封。• GetInvocationList() (override)
• 用途:按照调用顺序返回此多播委托的调用列表。
• 返回值:一个 Delegate[] 数组,其中包含组成当前委托的所有方法。
• 特点:该方法同样被密封。• GetObjectData(SerializationInfo info, StreamingContext context) (override)
• 用途:将序列化所需的数据填充到 SerializationInfo 对象中。
• 参数:
• info:保存序列化/反序列化数据的对象。
• context:存储和检索序列化数据的位置(保留字段)。
• 异常:当 info 为null时抛出 ArgumentNullException;发生序列化错误时抛出 SerializationException。
• 备注:已被标记为过时(obsolete),不建议在应用代码中使用。• CombineImpl(Delegate? follow) (override)
• 用途:将当前委托与另一个委托合并形成新的委托。
• 参数:
• follow:要与此委托组合的委托。
• 返回值:代表新调用列表根节点的新委托。
• 异常:若 follow 与当前实例不是相同类型则抛出 ArgumentException。
• 特点:受保护且密封的方法。• GetMethodImpl() (override)
• 用途:返回由当前 MulticastDelegate 表示的方法信息。
• 返回值:一个 MethodInfo 对象。• RemoveImpl(Delegate value) (override)
• 用途:从当前委托的调用列表中移除与给定委托相等的第一个条目。
• 参数:
• value:要在调用列表中查找并移除的委托。
• 返回值:如果找到并成功移除了 value,则返回一个新的不含该委托的委托;否则返回原委托本身。
• 特点:受保护且密封的方法。运算符
• operator ==
• 用途:比较两个 MulticastDelegate 是否具有相同的调用列表。
• 返回值:如果两者具有相同的调用列表则返回 true,否则返回 false。• operator !=
• 用途:检查两个 MulticastDelegate 的调用列表是否不同。
• 返回值:如果不具有相同的调用列表则返回 true,否则返回 false。
简单委托
using System;// 1. 定义委托类型(声明方法的签名:无参数,无返回值)
public delegate void PrintDelegate();class Program
{// 2. 定义几个与委托签名匹配的方法static void PrintHello(){Console.WriteLine("Hello, Delegate!");}static void PrintWelcome(){Console.WriteLine("Welcome to C#!");}static void Main(string[] args){// 3. 创建委托实例,关联到具体方法PrintDelegate print1 = new PrintDelegate(PrintHello);PrintDelegate print2 = PrintWelcome; // 简化写法// 4. 调用委托(实际执行关联的方法)print1(); // 输出: Hello, Delegate!print2(); // 输出: Welcome to C#!// 5. 多播委托:组合多个方法PrintDelegate multiPrint = print1 + print2;multiPrint(); // 依次执行两个方法}
}
下面是一些常用的委托场景
举例1:异步操作回调
// 定义委托(回调签名)
public delegate void TaskCompletedCallback(string result);// 执行异步任务的类
public class AsyncTask
{// 接收回调方法作为参数public void Execute(TaskCompletedCallback callback){// 模拟耗时操作Thread.Sleep(1000);string result = "任务完成";// 执行回调callback?.Invoke(result);}
}// 使用场景
class Program
{static void Main(){AsyncTask task = new AsyncTask();// 将回调方法通过委托传递task.Execute(OnTaskCompleted);}// 回调方法(与委托签名匹配)static void OnTaskCompleted(string result){Console.WriteLine("收到结果:" + result);}
}
举例2:多方法组合执行(多播委托)
当需要动态组合多个方法并批量执行(如批量校验、批量通知)时,必须用委托的多播特性。
// 定义委托(校验方法签名)
public delegate bool ValidateDelegate(string input);class Validator
{private ValidateDelegate _validators;// 添加校验规则(组合方法)public void AddRule(ValidateDelegate rule){_validators += rule;}// 执行所有校验public bool Validate(string input){if (_validators == null) return true;// 依次执行所有组合的校验方法foreach (ValidateDelegate rule in _validators.GetInvocationList()) //GetInvocationList 获取委托列表{if (!rule(input)) return false;}return true;}
}// 使用
class Program
{static void Main(){Validator validator = new Validator();// 组合多个校验规则validator.AddRule(IsNotNull); // 非空校验validator.AddRule(MinLength); // 长度校验bool result = validator.Validate("test");Console.WriteLine("校验结果:" + result); // 输出:True}static bool IsNotNull(string input) => !string.IsNullOrEmpty(input);static bool MinLength(string input) => input.Length >= 3;
}
举例3:跨线程 UI 更新(WinForm/WPF)
UI 控件通常只能由创建它的线程(主线程)访问,后台线程需通过委托(Invoke
方法)切换到主线程更新 UI。
// WinForm 窗体类
public partial class MainForm : Form
{public MainForm(){InitializeComponent();}private void StartButton_Click(object sender, EventArgs e){// 启动后台线程Thread thread = new Thread(BackgroundWork);thread.Start();}// 后台线程方法private void BackgroundWork(){// 模拟耗时操作Thread.Sleep(2000);string message = "后台任务完成";// 使用委托通过UI线程更新Labelthis.Invoke(new Action(() => {resultLabel.Text = message; // 此代码在主线程执行}));}
}
为什么必须用委托:Invoke
方法需要接收一个委托来指定 “要在主线程执行的逻辑”,否则无法安全跨线程操作 UI。
事件
要理解 C# 中的事件,核心是抓住它的本质 ——基于委托的 “发布 - 订阅模式” 封装,目的是让对象间能安全、解耦地传递 “状态变化通知”。下面从本质、结构、用法、特性四个维度详细拆解,结合代码示例让逻辑更清晰。
一、事件的本质:委托的 “安全包装器”
事件并非独立于委托的新特性,而是对委托的访问权限控制和使用场景约束。
- 没有委托,事件无法存在(事件必须基于某个委托类型定义);
- 没有事件,委托的访问权限过宽(外部可直接调用、赋值,导致逻辑混乱)。
简单说:事件 = 委托 + 访问控制,它只开放 “订阅(+=
)” 和 “取消订阅(-=
)” 两种操作,禁止外部直接调用事件或覆盖已有的订阅方法,确保通知逻辑完全由事件的 “发布者” 控制。
二、事件的核心结构:3 个关键角色
实现一个完整的事件功能,需要明确 “发布者”“订阅者”“事件参数” 三个角色,三者协作完成 “通知 - 响应” 流程。
角色 | 职责 |
---|---|
发布者(Publisher) | 定义事件,在特定条件下(如状态变化)触发事件,是 “通知的发起者”。 |
订阅者(Subscriber) | 订阅事件,提供 “事件处理方法”,是 “通知的接收者”(可多个)。 |
事件参数(EventArgs) | 传递事件相关的数据(如 “点击位置”“温度值”),需继承自 System.EventArgs 。 |
三、事件的完整实现步骤
步骤 1:定义事件参数(传递事件数据)
// 事件参数:包含亮度信息
public class BrightnessChangedEventArgs : EventArgs
{public int Brightness { get; } // 0-100的亮度值public BrightnessChangedEventArgs(int brightness) => Brightness = brightness;
}
步骤 2:定义发布者(发起事件通知)
// 发布者:灯泡
public class LightBulb
{private int _brightness;// 声明事件:亮度变化时触发public event EventHandler<BrightnessChangedEventArgs> BrightnessChanged;// 触发事件的方法protected virtual void OnBrightnessChanged(int brightness){BrightnessChanged?.Invoke(this, new BrightnessChangedEventArgs(brightness));}// 调整亮度(会触发事件)public void SetBrightness(int brightness){if (brightness < 0 || brightness > 100)throw new ArgumentException("亮度必须在0-100之间");if (_brightness != brightness){_brightness = brightness;OnBrightnessChanged(brightness); // 亮度变化,触发事件}}
}
步骤 3:定义订阅者(响应事件通知)
// 订阅者1:调光开关(显示亮度)
public class DimmerSwitch
{public void Subscribe(LightBulb bulb){bulb.BrightnessChanged += (sender, e) => Console.WriteLine($"开关显示:当前亮度 {e.Brightness}%");}
}// 订阅者2:安防系统(过暗时报警)
public class SecuritySystem
{public void Subscribe(LightBulb bulb){bulb.BrightnessChanged += (sender, e) => {if (e.Brightness < 30)Console.WriteLine("安防报警:亮度不足,可能有人闯入!");};}
}
步骤 4:关联发布者与订阅者(运行逻辑)
// 使用示例
class Program
{static void Main(){LightBulb bulb = new LightBulb();new DimmerSwitch().Subscribe(bulb);new SecuritySystem().Subscribe(bulb);bulb.SetBrightness(80); // 亮度变化,触发事件bulb.SetBrightness(20); // 亮度降低,触发事件}
}
四、事件的核心特性(为什么必须用事件?)
从上述示例能看出,事件相比纯委托有 3 个关键特性,这也是它在 “通知场景” 中不可替代的原因:
1. 访问权限控制:仅允许订阅 / 取消订阅
外部代码对事件的操作被严格限制:
- 允许:
+=
(添加订阅)、-=
(取消订阅); - 禁止:直接调用事件(如
order.OrderPaid.Invoke(...)
)、直接赋值(如order.OrderPaid = inventory.OnOrderPaid
,会覆盖所有已有订阅)。
作用:确保事件的触发权完全由发布者控制(如只有订单真正支付成功,才会触发 OrderPaid
事件),避免外部伪造通知。
2. 多订阅者支持:一对多通信
一个事件可以绑定多个订阅者的处理方法(如示例中 OrderPaid
绑定了 3 个服务),事件触发时,所有订阅的方法会按绑定顺序依次执行。
作用:满足 “一个状态变化需要多个组件协同响应” 的场景(如订单支付需同步更新库存、短信、日志),且发布者无需知道订阅者的存在,解耦性极高。
3. 遵循.NET 标准:统一的事件模型
.NET 推荐事件使用 EventHandler
或 EventHandler<TEventArgs>
委托类型,参数遵循 (object sender, TEventArgs e)
格式:
sender
:事件源(发布者实例),方便订阅者获取发布者信息;e
:事件参数,传递事件相关数据。
作用:让代码风格统一,降低团队协作成本(所有开发者都能快速理解事件逻辑)。
常用事件场景
1. UI 控件交互(如按钮点击、文本变化)
所有 UI 框架(WinForm、WPF、Blazor)的控件交互必须用事件,确保外部只能响应交互而不能伪造交互。
示例:WinForm 按钮点击
// 按钮点击事件(系统自带)
button1.Click += Button1_Click;private void Button1_Click(object sender, EventArgs e)
{MessageBox.Show("按钮被点击了");
}
为什么必须用事件:如果用委托,外部可以直接调用button1.Click.Invoke()
伪造点击,导致逻辑混乱。
2. 状态变化通知(如设备状态、数据更新)
当对象状态变化需要通知多个外部组件,且不允许外部伪造状态变化时,必须用事件。
示例:温度传感器通知
public class TemperatureSensor
{public event EventHandler<TemperatureEventArgs> TemperatureChanged;// 内部更新温度时触发事件private void UpdateSensor(){int newTemp = ReadHardwareTemperature(); // 从硬件读取真实温度TemperatureChanged?.Invoke(this, new TemperatureEventArgs(newTemp));}
}
为什么必须用事件:防止外部直接调用TemperatureChanged.Invoke(100)
伪造高温,确保状态变化的真实性。
3. 消息 / 事件总线(解耦系统组件)
大型系统中,事件总线用于组件间通信,必须用事件确保发布者和订阅者完全解耦。
示例:简单事件总线
public static class EventBus
{public static event Action<string> MessagePublished;public static void Publish(string message){MessagePublished?.Invoke(message); // 发布消息}
}// 模块A发布消息
EventBus.Publish("订单已创建");// 模块B订阅消息(无需知道模块A存在)
EventBus.MessagePublished += msg => Console.WriteLine("模块B收到:" + msg);
为什么必须用事件:事件总线无需维护订阅者列表,通过事件自动管理,实现组件解耦。
4. 生命周期钩子(如对象初始化、销毁)
对象生命周期的关键节点(创建、销毁、加载完成)需要通知外部时,必须用事件。
示例:页面加载完成事件
public class WebPage
{public event EventHandler LoadCompleted;public void Load(){// 加载页面资源...LoadCompleted?.Invoke(this, EventArgs.Empty); // 加载完成后通知}
}// 使用
var page = new WebPage();
page.LoadCompleted += (s, e) => Console.WriteLine("页面加载完成,可以交互了");
page.Load();
为什么必须用事件:页面无法预知外部需要在加载完成后执行什么操作(如统计、渲染),事件允许灵活扩展。
5. 多订阅者协作(如日志、监控同时响应)
当一个事件需要多个独立组件同时响应(如操作日志、性能监控、安全审计),必须用事件支持多订阅。
示例:用户操作审计
public class UserService
{public event EventHandler<UserEventArgs> UserLoggedIn;public void Login(string username){// 登录逻辑...UserLoggedIn?.Invoke(this, new UserEventArgs(username));}
}// 订阅者1:记录日志
service.UserLoggedIn += (s, e) => LogHelper.Write($"用户 {e.Username} 登录");// 订阅者2:更新在线状态
service.UserLoggedIn += (s, e) => OnlineMonitor.AddUser(e.Username);// 订阅者3:安全检查
service.UserLoggedIn += (s, e) => SecurityChecker.Check(e.Username);