.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 时取消订阅 |