.NET 8 C# 委托与事件实战教程

.NET 8 C# 委托与事件实战教程

前言:委托(Delegate)与事件(Event)是 C# 中最具特色的语言机制之一,是解耦模块、实现观察者模式的基础。本文面向有 C# 基础的开发者,系统讲解委托与事件的核心原理,并通过真实案例帮助你在 .NET 8 项目中灵活运用。


一、委托基础(Delegate)

1.1 什么是委托?

委托本质是一种类型安全的函数指针,它封装了对某个方法的引用。委托声明定义了方法的签名(参数类型和返回类型),只有匹配该签名的方法才能被赋值给委托。

.NET 内置了两个常用的泛型委托:

  • Action<T>:无返回值
  • Func<T, TResult>:有返回值

1.2 委托的声明与使用

csharp 复制代码
// ============ 示例一:委托的声明与调用 ============

// 1. 声明一个委托类型(接收 string 参数,无返回值)
delegate void MessageHandler(string message);

class DelegateDemo
{
    static void Main()
    {
        // 2. 将方法赋值给委托变量
        MessageHandler handler = PrintToConsole;

        // 3. 通过委托调用方法
        handler("Hello, Delegate!");

        // 4. 委托支持多播(+=),可绑定多个方法
        handler += SaveToLog;
        handler("Multi-cast call");  // 两个方法都会被调用
    }

    // 签名必须与委托一致:接收 string,无返回值
    static void PrintToConsole(string msg) => Console.WriteLine($"[Console] {msg}");
    static void SaveToLog(string msg)      => Console.WriteLine($"[Log]     {msg}");
}

要点 :多播委托按注册顺序依次执行;使用 -= 可移除某个方法;若委托有返回值,多播时只返回最后一个方法的结果。


二、事件基础(Event)

2.1 什么是事件?

事件是基于委托的封装机制 ,专门用于发布/订阅模式。它限制了外部代码对委托的直接操作(不能赋值、不能直接调用),只允许订阅(+=)和取消订阅(-=)。

.NET 8 标准事件模式使用 EventHandler<TEventArgs>

csharp 复制代码
// ============ 示例二:标准事件模式 ============

// 1. 定义事件参数,继承 EventArgs
public class TemperatureChangedEventArgs : EventArgs
{
    public double OldValue { get; init; }  // init 是 C# 9+ 的只读初始化属性
    public double NewValue { get; init; }
    public DateTime OccurredAt { get; init; } = DateTime.Now;
}

public class Thermometer
{
    private double _temperature;

    // 2. 使用标准模式声明事件
    //    EventHandler<T> 本质是 delegate void EventHandler<T>(object sender, T e)
    public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;

    public double Temperature
    {
        get => _temperature;
        set
        {
            if (Math.Abs(value - _temperature) < 0.01) return; // 变化太小不触发

            var args = new TemperatureChangedEventArgs
            {
                OldValue = _temperature,
                NewValue = value
            };

            _temperature = value;

            // 3. 触发事件(?.Invoke 是线程安全写法)
            TemperatureChanged?.Invoke(this, args);
        }
    }
}

// 订阅方
var sensor = new Thermometer();
sensor.TemperatureChanged += (sender, e) =>
    Console.WriteLine($"温度变化:{e.OldValue:F1}°C → {e.NewValue:F1}°C  时间:{e.OccurredAt:HH:mm:ss}");

sensor.Temperature = 36.5;
sensor.Temperature = 37.8;

三、委托与事件的区别

对比维度 委托(Delegate) 事件(Event)
本质 类型安全的函数指针 委托的封装限制
外部赋值 ✅ 可直接赋值(= ❌ 只能 += / -=
外部调用 ✅ 可直接调用 ❌ 只能由类内部触发
适用场景 回调、策略模式 发布/订阅、解耦通知
封装性 较低 较高(推荐用于公开 API)

核心结论 :事件 = 委托 + 访问限制。在设计公开 API 时,优先使用 event 而非裸委托,防止调用方意外覆盖或触发。


四、实战案例:自定义下载进度通知

下面模拟一个文件下载器,通过事件向外部报告进度和完成状态,体现委托/事件在真实业务中的用法。

csharp 复制代码
// ============ 示例三:下载进度通知系统 ============

// ── 事件参数定义 ──────────────────────────────────
public record ProgressEventArgs(int Percent, string FileName) : EventArgs;
public record CompletedEventArgs(string FileName, bool Success, string? ErrorMessage) : EventArgs;

// ── 发布者(下载器)─────────────────────────────────
public class FileDownloader
{
    // 进度事件(每 10% 触发一次)
    public event EventHandler<ProgressEventArgs>?  ProgressChanged;
    // 完成事件
    public event EventHandler<CompletedEventArgs>? DownloadCompleted;

    public async Task DownloadAsync(string fileName, CancellationToken ct = default)
    {
        try
        {
            for (int i = 10; i <= 100; i += 10)
            {
                ct.ThrowIfCancellationRequested();
                await Task.Delay(200, ct);  // 模拟网络 IO

                // 触发进度事件
                ProgressChanged?.Invoke(this, new ProgressEventArgs(i, fileName));
            }
            // 触发完成事件(成功)
            DownloadCompleted?.Invoke(this, new CompletedEventArgs(fileName, true, null));
        }
        catch (OperationCanceledException)
        {
            DownloadCompleted?.Invoke(this, new CompletedEventArgs(fileName, false, "用户取消下载"));
        }
    }
}

// ── 订阅者(UI/日志层)──────────────────────────────
var downloader = new FileDownloader();

// 订阅进度事件
downloader.ProgressChanged += (_, e) =>
    Console.Write($"\r下载 {e.FileName}:[{new string('█', e.Percent / 5),-20}] {e.Percent}%");

// 订阅完成事件
downloader.DownloadCompleted += (_, e) =>
{
    Console.WriteLine();
    if (e.Success)
        Console.WriteLine($"✅ {e.FileName} 下载成功!");
    else
        Console.WriteLine($"❌ 下载失败:{e.ErrorMessage}");
};

await downloader.DownloadAsync("dotnet8-runtime.zip");

五、进阶:弱事件与内存泄漏防范

事件订阅是导致内存泄漏的常见原因:只要发布者存活,订阅者就不会被 GC 回收。

csharp 复制代码
// ============ 示例四:避免事件内存泄漏 ============

public class DataService
{
    public event EventHandler? DataRefreshed;
    public void Refresh() => DataRefreshed?.Invoke(this, EventArgs.Empty);
}

public class ViewModel : IDisposable
{
    private readonly DataService _service;

    public ViewModel(DataService service)
    {
        _service = service;
        // 订阅事件
        _service.DataRefreshed += OnDataRefreshed;
    }

    private void OnDataRefreshed(object? sender, EventArgs e)
        => Console.WriteLine("数据已刷新,更新 UI...");

    // ✅ 关键:在 Dispose 中取消订阅,断开强引用
    public void Dispose()
    {
        _service.DataRefreshed -= OnDataRefreshed;
        GC.SuppressFinalize(this);
    }
}

// 使用 using 确保 Dispose 被调用
var service = new DataService();
using (var vm = new ViewModel(service))
{
    service.Refresh();
}
// ViewModel 离开 using 块后订阅已清除,可被 GC 回收

最佳实践 :凡是订阅了事件的类,都应实现 IDisposable 并在 Dispose() 中取消订阅(-=)。在 WPF / MAUI 等框架中,页面销毁时尤其要注意这一点。


六、总结

知识点 核心要点
委托本质 类型安全的函数指针,支持多播
事件本质 委托 + 访问限制,仅供内部触发
标准模式 使用 EventHandler<TEventArgs>,参数继承 EventArgs
触发写法 event?.Invoke(this, args) 保证线程安全
内存管理 实现 IDisposable,Dispose 时取消订阅
相关推荐
不会编程的懒洋洋2 分钟前
C# P/Invoke 基础
开发语言·c++·笔记·安全·机器学习·c#·p/invoke
段一凡-华北理工大学4 分钟前
【高炉炼铁领域炉温监测、预警、调控智能体设计与应用】~系列文章06:智能决策:从经验驱动到数据驱动
网络·人工智能·数据挖掘·高炉炼铁·工业智能体·高炉炉温
Avalon71228 分钟前
Unity3D响应式渲染UI框架UniVue
游戏·ui·unity·c#·游戏引擎
njsgcs43 分钟前
solidworks折弯自动标注5 非90度折弯
c#·solidworks
狼与自由1 小时前
clickhouse引擎
clickhouse·c#·linq
wangnaisheng1 小时前
【C#】死锁详解:发生原因、优化解决方案
c#
时空系2 小时前
第7篇:功能——打造你的工具箱 Rust中文编程
开发语言·网络·rust
BizViewStudio2 小时前
甄选方法:2026 企业新媒体代运营的短视频精细化运营与流量转化技巧
大数据·网络·人工智能·媒体
tiger从容淡定是人生2 小时前
AI替代软件战略(一):从 CCleaner 到 MCP 架构重构 —— TigerCleaner 的工程实践
人工智能·重构·架构·c#·mcp
凯瑟琳.奥古斯特2 小时前
NAT原理及作用详解
网络·网络协议