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线程更新Label
        this.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);
posted @ 2025-10-17 10:52  【唐】三三  阅读(8)  评论(0)    收藏  举报