当前位置: 首页 > news >正文

C#委托和事件深入

委托

委托是一种类型安全的函数指针,它定义了方法的签名(参数列表和返回值类型),可以指向任何与该签名匹配的方法(包括静态方法和实例方法)。

MulticastDelegate 类是 .NET 中表示多播委托的抽象基类,它允许一个委托实例调用多个方法。

但我们不需要直接实现它。相反,我们应该:

  1. 使用内置委托类型:如 ActionFunc 系列委托
  2. 定义自定义委托:使用 delegate 关键字
  3. 利用事件机制:这是最常用的多播委托应用场景
  4. 使用委托操作符:++=、-、-= 来组合和移除委托
方法
•	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 推荐事件使用 EventHandlerEventHandler<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);
http://www.hskmm.com/?act=detail&tid=32823

相关文章:

  • 海康摄像头4G/5G或有线网都可以配置ISUP接入到LiveNVR实现互联网网页直播和回放。同时支持配置接收摄像头的报警信息,并抓图和紧急录像取证
  • RTP推流测试
  • 2025 年板材厂家最新推荐排行榜:涵盖环保、密度、净化、零醛添加等类型,胖胖熊等优质品牌详细解析
  • 压缩 PDF 文件大小(3 大实用的 Python 库) - E
  • 2025 年搅拌器厂家最新推荐榜:聚焦国内优质厂商,精选实力品牌助力企业采购决策侧入式/立式/脱硫/桨式/水处理搅拌器厂家推荐
  • 2025年轴承厂家权威推荐榜单:电机轴承,单向轴承,含油轴承,自润滑轴承源头厂家综合实力与创新技术全景解析
  • 请输入标题
  • 2025 年修补剂厂家最新推荐排行榜:金属 / 陶瓷 / 橡胶等多材质适配品牌深度解析,助力企业精准选型
  • 2025 工业电子胶粘剂厂家最新推荐榜单发布:国产实力品牌深度解析,选购指南全攻略高端工业/进口国产工业/工业电子胶粘剂胶水厂家推荐
  • 国产项目管理工具Gitee的崛起:数字化转型浪潮下的本土化突围
  • Elasticsearch安装和Kibana安装
  • 2025 试验机厂家最新推荐权威榜单:弹簧 / 环境 / 材料检测设备领军企业深度解析
  • [note] slope trick
  • three自带的框选工具SelectionBox、SelectionHelper
  • 2025年精密磨床/CNC机械加工厂家推荐榜单:涵盖铣床/车床/磨削/多轴/复合加工,铝/不锈钢/钛合金/模具钢/塑料件定制,汽车/医疗/航空航天/机器人零件及模具工装夹具加工
  • 2025 电动缸源头厂家最新推荐榜:剖析国产厂商成本优势与技术实力,附权威选购指南
  • 网络编程实践笔记_4_阿贝云_免费云服务器_简易博客_
  • 10 17
  • 2025年铝单板厂家推荐排行榜,氟碳铝单板,木纹铝单板,冲孔铝单板,外墙铝单板,雕花铝单板,异形铝单板,双曲铝单板公司推荐!
  • 2025 年最新推荐热熔胶源头厂家榜:覆盖书刊装订 / 包装等场景,助企业选高性价比产品
  • 开发日志
  • Gitee 2025:中国开发者生态的崛起与本土化优势
  • C++中的new操作符:new operator、operator new、placement new
  • JavaBean知识总结及范例
  • 2025 年家装管道生产厂家最新推荐排行榜:覆盖云南昆明贵州贵阳四川成都重庆,精选优质 PPR/PVC 管道品牌,解决选购难题
  • 同一设备多账号登录,如何避免消息推送“串门”?
  • 强合规行业DevOps选型:告别工具拼凑,找到真正适配的国产化DevOps方案
  • 大疆无人机RTMP推流至LiveNVR实现web页面实时播放与录像回放,并可以转GB28181协议级联推送给上级监控视频管理平台
  • Character Animator 2025下载安装教程:2D角色动画软件零基础入门,附最新下载安装教程及激活方法
  • 2025年彩钢瓦/镀锌板/折弯件/C型钢/Z型钢/压型瓦/楼承板/次檩条厂家推荐排行榜,专业钢结构安装与定制加工实力解析