
C# 之委托与事件
什么是委托
- 定义:委托是一种类型安全的函数指针,封装了一个或多个具有相同签名(返回类型和参数列表相同)的方法引用。可以把委托看作是一个函数指针,但它比函数指针更安全、更灵活,因为它是面向对象的,并且 C# 的委托支持协变与逆变。
- 作用:通过委托可以实现方法的动态调用、回调机制,支持多路广播(Multicast Delegate),一次调用可执行多方法,以及事件的发布/订阅模式。
- 类型安全:在编译期就能保证所调用的方法签名(参数列表和返回类型)与委托类型一致。
声明格式:
public delegate TResult MyDelegate<T1,…,TResult>(T1 arg1, …);
示例
void PrintNumber(int n) {
Console.WriteLine(n);
}
MyDelegate d = new MyDelegate(PrintNumber);
// 也可简写
MyDelegate d2 = PrintNumber;
// 调用
d(42); // 输出:42
d.Invoke( 100 ); // 等同于 d(100)
// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);
匿名方法与 Lambda 表达式
匿名方法
MyDelegate anon = delegate(int x) {
Console.WriteLine("匿名:" + x);
};
anon(7);
// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);
Lambda 表达式
MyDelegate lambda = x => Console.WriteLine("Lambda:" + x);
lambda(9);
// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);
语法更为简介
多路委托
同一个委托实例可以同时封装多个方法,调用时会按注册顺序依次执行。
使用 +=
和 -=
运算符进行注册和注销。
void PrintNumber(int n) {
Console.WriteLine(n);
}
MyDelegate anon = delegate(int x) {
Console.WriteLine("匿名:" + x);
};
MyDelegate lambda = x => Console.WriteLine("Lambda:" + x);
MyDelegate m = PrintNumber;
m += lambda;
m += anon;
m(1);
// 依次输出:
// 1
// Lambda:1
// 匿名:1
m -= lambda;
m(2);
// 输出:
// 2
// 匿名:2
// 声明一个返回 void、带一个 int 参数的委托
public delegate void MyDelegate(int x);
内置委托类型
.NET 提供了几种常用的泛型委托,无需自定义类型:
委托类型 | 签名 | 说明 |
---|---|---|
Action |
无返回值,0~16 个泛型参数 | 最常用的无返回值委托 |
Action |
void (T1,…,Tn) |
|
Func |
TResult () |
无入参,有返回值 |
Func |
TResult (T1,…,Tn) |
最多支持 16 个入参 |
Predicate |
bool (T) |
专用于判断,返回 bool |
Action<string> printer = s => Console.WriteLine(s);
Func<int, int, int> adder = (a, b) => a + b;
Predicate<int> isEven = x => x % 2 == 0;
printer("Hello");
int sum = adder(3, 4);
bool even = isEven(10);
Console.WriteLine(sum);
Console.WriteLine(even);
// 输出:
// Hello
// 7
// True
什么是事件
- 事件是基于委托的发布/订阅机制(publish–subscribe),用于解耦“发布者”(Publisher)与“订阅者”(Subscriber)。
- 发布者定义事件,负责触发;订阅者注册处理函数,负责响应。
声明与订阅
声明事件
// 1. 定义委托(可用内置类型)
public delegate void NotifyHandler(string message);
// 2. 在类中声明事件
public class Notifier
{
// 外部只能 += 或 -=
public event NotifyHandler Notify;
}
也可直接使用 EventHandler
/EventHandler<TEventArgs>
:
public class Notifier
{
// 标准无参事件
public event EventHandler Initialized;
// 带自定义参数的事件
public event EventHandler<MyEventArgs> DataReceived;
}
public class MyEventArgs : EventArgs
{
public string Data { get; }
public MyEventArgs(string data) => Data = data;
}
订阅与退订
void TestHandler(string message)
{
Console.WriteLine("收到通知:" + message);
}
var n = new Notifier();
// 订阅
n.Notify += TestHandler;
n.Invoke("Hello");
// 退订
n.Notify -= TestHandler;
n.Invoke("World");
public delegate void NotifyHandler(string message);
public class Notifier
{
// event 外部只能 += 或 -=
public event NotifyHandler? Notify;
public void Invoke(string message) => Notify?.Invoke(message);
}
// 输出:
// 收到通知:Hello
触发
public class Notifier
{
public event EventHandler<MyEventArgs> DataReceived;
// 安全触发模式
protected virtual void OnDataReceived(MyEventArgs e)
=> DataReceived?.Invoke(this, e);
public void Receive(string data)
{
// …处理逻辑…
OnDataReceived(new MyEventArgs(data));
}
}
- 空检查:
DataReceived?
确保没有订阅者时不抛空引用。 - 封装触发:把
Invoke
放在受保护的虚方法里,子类可重写。
示例场景
文件下载进度
// 使用者订阅
var downloader = new FileDownloader();
downloader.ProgressChanged += (_, e) =>
Console.WriteLine($"下载 {e.Url}:{e.Progress}%{(e.Completed?" 完成":"")}");
await downloader.DownloadAsync("https://example.com/file.zip");
public class DownloadEventArgs : EventArgs
{
public string Url { get; }
public int Progress { get; }
public bool Completed { get; }
public DownloadEventArgs(string url, int progress, bool done)
=> (Url, Progress, Completed) = (url, progress, done);
}
public class FileDownloader
{
public event EventHandler<DownloadEventArgs>? ProgressChanged;
protected virtual void OnProgressChanged(DownloadEventArgs e)
=> ProgressChanged?.Invoke(this, e);
public async Task DownloadAsync(string url)
{
for (int p = 0; p <= 100; p += 10)
{
await Task.Delay(100);
OnProgressChanged(new DownloadEventArgs(url, p, p == 100));
}
}
}
// 输出
// 下载 https://example.com/file.zip:0%
// 下载 https://example.com/file.zip:10%
// 下载 https://example.com/file.zip:20%
// 下载 https://example.com/file.zip:30%
// 下载 https://example.com/file.zip:40%
// 下载 https://example.com/file.zip:50%
// 下载 https://example.com/file.zip:60%
// 下载 https://example.com/file.zip:70%
// 下载 https://example.com/file.zip:80%
// 下载 https://example.com/file.zip:90%
// 下载 https://example.com/file.zip:100% 完成
- 职责分离:
FileDownloader
只负责下载并触发事件; - 订阅者:只需通过
+=
获取进度更新,完全不关心内部实现。
窗体按钮点击事件处理
在 WinForms、WPF、ASP.NET 等界面和后台框架开发中,按钮点击等 UI 事件本质上就是基于委托+事件实现的。不用在按钮内部硬编码逻辑,而是把处理逻辑“注入”进去。
// 定义一个事件(基于 EventHandler 委托)
public event EventHandler<ButtonClickEventArgs> ButtonClicked;
// 在按钮控件内部触发事件
protected void OnClick()
{
ButtonClicked?.Invoke(this, new ButtonClickEventArgs(/*…*/));
}
// 在使用者处订阅并提供回调
myButton.ButtonClicked += (s, e) =>
{
// 这里就是响应按钮点击的逻辑
MessageBox.Show("用户点击了按钮,参数:" + e.SomeValue);
};
自定义排序(Strategy 模式)
假设有一个产品列表,需要动态地按价格、按销量或按评分排序。可以定义一个接收比较函数的通用方法:
// 使用内置的 Comparison<T> 委托(签名:int (T,T))
void SortProducts(List<Product> list, Comparison<Product> comparison)
{
list.Sort(comparison);
}
// 调用端按不同策略传入不同比较方法
SortProducts(products, (a, b) => a.Price.CompareTo(b.Price)); // 按价格升序
SortProducts(products, (a, b) => b.Sales.CompareTo(a.Sales)); // 按销量降序
SortProducts(products, (a, b) => b.Rating.CompareTo(a.Rating)); // 按评分降序
定时任务调度
使用 System.Timers.Timer
时,通过 Elapsed
事件来注册定时执行的回调:
var timer = new System.Timers.Timer(1000); // 每 1 秒触发一次
timer.Elapsed += (s, e) =>
{
Console.WriteLine($"[{e.SignalTime}] 定时任务执行中…");
};
timer.Start();
与其他语言对比
Python
C# 多路委托:
Action onSave = SaveToFile;
onSave += SaveToDatabase;
onSave += NotifyUser;
// 一次 onSave(),文件、数据库、通知同时触发
onSave();
Python 列表循环:
callbacks = [save_to_file, save_to_db, notify_user]
for cb in callbacks:
cb()
- 得先维护一个列表,然后循环触发。
- C# 委托像“电话会议”,拨一个会议号,所有人都自动接入;
- Python 列表回调,像“一个人一个电话”,得一个个打电话通知。
Java
C#(基于 event
+ 委托):
var btn = new Button();
btn.Click += (_, _) => Console.WriteLine("C# Button clicked!");
btn.SimulateClick();
public delegate void ClickHandler(object sender, EventArgs e);
public class Button
{
public event ClickHandler? Click;
public void SimulateClick()
=> Click?.Invoke(this, EventArgs.Empty);
}
Java(基于接口 + 监听器):
Button btn = new Button();
btn.addClickListener(sender -> System.out.println("Java Button clicked!"));
btn.simulateClick();
public interface ClickListener {
void onClick(Object sender);
}
public class Button {
private List<ClickListener> listeners = new ArrayList<>();
public void addClickListener(ClickListener l) {
listeners.add(l);
}
public void removeClickListener(ClickListener l) {
listeners.remove(l);
}
public void simulateClick() {
for (ClickListener l : listeners)
l.onClick(this);
}
}
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Swaggy Macro
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果