.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 时取消订阅
相关推荐
beyond谚语3 小时前
接口&抽象类
c#·接口隔离原则·抽象类
w6100104663 小时前
CKA-2026-Service
linux·服务器·网络·service·cka
GTgiantech3 小时前
灵活拓展网络边界:电口光模块的智慧选型与部署指南
网络
测试专家3 小时前
天脉3操作系统
网络
新手小新3 小时前
C#学习笔记1-在VS CODE部署C#开发环境
笔记·学习·c#
JS_SWKJ3 小时前
网闸升级、备份、恢复标准化操作全指南
网络
王燕龙(大卫)4 小时前
tcp报文什么时候会真正发送
服务器·网络·tcp/ip
勿忘,瞬间4 小时前
网络编程套接字
运维·服务器·网络
@insist1234 小时前
网络工程师-网络安全基础体系:软考核心考点与合规框架全解析
网络·网络工程师·软考·软件水平考试