委托与事件
委托概念
委托(Delegate)是C#中一种类型安全的函数指针,它可以将方法作为参数传递。委托定义了方法的签名(返回类型和参数列表),只有匹配该签名的方法才能赋值给委托实例。
与C/C++函数指针不同,委托是面向对象的、类型安全的,并且支持多播调用。
声明与使用委托
使用 delegate 关键字声明委托类型,然后创建实例并关联方法。
示例
CSHARP
delegate int MathOp(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
MathOp op = Add;
Console.WriteLine(op(3, 4));
op = Multiply;
Console.WriteLine(op(3, 4));
}
}
TEXT
7
12
多播委托
委托支持多播(Multicast),即一个委托实例可以引用多个方法。使用 += 添加方法,-= 移除方法。调用时按添加顺序依次执行所有方法。对于有返回值的委托,多播调用仅返回最后一个方法的结果。
示例
CSHARP
delegate void NotifyHandler(string msg);
class Program
{
static void LogToConsole(string msg) => Console.WriteLine($"控制台: {msg}");
static void LogToFile(string msg) => Console.WriteLine($"文件: {msg}");
static void LogToDb(string msg) => Console.WriteLine($"数据库: {msg}");
static void Main()
{
NotifyHandler handler = LogToConsole;
handler += LogToFile;
handler += LogToDb;
handler("系统启动");
Console.WriteLine("---");
handler -= LogToFile;
handler("系统关闭");
}
}
TEXT
控制台: 系统启动
文件: 系统启动
数据库: 系统启动
---
控制台: 系统关闭
数据库: 系统关闭
内置委托
C#提供了三种常用内置委托,无需自定义即可覆盖大多数场景。
Action
Action 表示无返回值的方法。支持0到16个泛型参数:Action、Action<T>、Action<T1,T2> 等。
示例
CSHARP
class Program
{
static void Main()
{
Action<string> greet = name => Console.WriteLine($"你好, {name}!");
greet("张三");
Action noParam = () => Console.WriteLine("无参数Action");
noParam();
}
}
TEXT
你好, 张三!
无参数Action
Func
Func<T, TResult> 表示有返回值的方法。最后一个泛型参数为返回类型,前面为输入参数:Func<TResult>、Func<T, TResult>、Func<T1,T2,TResult> 等。
示例
CSHARP
class Program
{
static void Main()
{
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(10, 20));
Func<string> getTime = () => DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine(getTime());
}
}
TEXT
30
14:30:00
Predicate
Predicate<T> 表示返回 bool 的方法,常用于筛选和判断。
示例
CSHARP
class Program
{
static void Main()
{
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4));
Console.WriteLine(isEven(7));
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evens = numbers.FindAll(isEven);
Console.WriteLine(string.Join(", ", evens));
}
}
TEXT
True
False
2, 4, 6
匿名方法
匿名方法使用 delegate 关键字内联定义代码块,无需单独命名方法。
示例
CSHARP
class Program
{
static void Main()
{
Func<int, int> square = delegate(int x) { return x * x; };
Console.WriteLine(square(5));
Action<string> log = delegate(string msg) { Console.WriteLine($"[LOG] {msg}"); };
log("匿名方法示例");
}
}
TEXT
25
[LOG] 匿名方法示例
💡 匿名方法是Lambda表达式的前身,现代C#开发中更推荐使用Lambda表达式
x => x * x,语法更简洁。
事件
事件(Event)是基于委托的发布-订阅机制。使用 event 关键字修饰委托字段后,只有声明事件的类内部可以触发事件,外部类只能通过 += 和 -= 订阅或取消订阅,无法直接调用。
示例
CSHARP
class Button
{
public event EventHandler<string> OnClick;
public void Click()
{
OnClick?.Invoke(this, "按钮被点击");
}
}
class Program
{
static void Main()
{
var btn = new Button();
btn.OnClick += (sender, msg) => Console.WriteLine($"收到事件: {msg}");
btn.Click();
btn.Click();
}
}
TEXT
收到事件: 按钮被点击
收到事件: 按钮被点击
⚠️ 如果没有订阅者,直接调用
OnClick.Invoke() 会抛出 NullReferenceException,因此应使用 ?.Invoke() 空条件调用。
发布-订阅模式
发布-订阅模式(Publisher-Subscriber)是事件的核心应用场景。发布者负责触发事件,订阅者负责响应事件,两者完全解耦。
示例
CSHARP
class NewsPublisher
{
public event EventHandler<string> OnNewsPublished;
public void Publish(string news)
{
Console.WriteLine($"发布新闻: {news}");
OnNewsPublished?.Invoke(this, news);
}
}
class NewsSubscriber
{
public string Name { get; }
public NewsSubscriber(string name)
{
Name = name;
}
public void Subscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished += HandleNews;
}
public void Unsubscribe(NewsPublisher publisher)
{
publisher.OnNewsPublished -= HandleNews;
}
private void HandleNews(object sender, string news)
{
Console.WriteLine($" [{Name}] 收到: {news}");
}
}
class Program
{
static void Main()
{
var publisher = new NewsPublisher();
var sub1 = new NewsSubscriber("读者A");
var sub2 = new NewsSubscriber("读者B");
sub1.Subscribe(publisher);
sub2.Subscribe(publisher);
publisher.Publish("C# 12 正式发布");
sub2.Unsubscribe(publisher);
publisher.Publish(".NET 9 性能提升50%");
}
}
TEXT
发布新闻: C# 12 正式发布
[读者A] 收到: C# 12 正式发布
[读者B] 收到: C# 12 正式发布
发布新闻: .NET 9 性能提升50%
[读者A] 收到: .NET 9 性能提升50%
自定义 EventArgs
EventHandler<T> 中的 T 通常继承自 EventArgs,用于携带自定义事件数据。
示例
CSHARP
class TemperatureEventArgs : EventArgs
{
public double Temperature { get; }
public DateTime Timestamp { get; }
public TemperatureEventArgs(double temperature)
{
Temperature = temperature;
Timestamp = DateTime.Now;
}
}
class TemperatureMonitor
{
public event EventHandler<TemperatureEventArgs> OnTemperatureAlert;
private double _threshold;
public TemperatureMonitor(double threshold)
{
_threshold = threshold;
}
public void CheckTemperature(double current)
{
Console.WriteLine($"当前温度: {current}°C");
if (current > _threshold)
{
var args = new TemperatureEventArgs(current);
OnTemperatureAlert?.Invoke(this, args);
}
}
}
class Cooler
{
public void StartCooling(object sender, TemperatureEventArgs e)
{
Console.WriteLine($" 🚨 温度超限! {e.Temperature}°C > 阈值, 启动降温 (时间: {e.Timestamp:HH:mm:ss})");
}
}
class Program
{
static void Main()
{
var monitor = new TemperatureMonitor(30.0);
var cooler = new Cooler();
monitor.OnTemperatureAlert += cooler.StartCooling;
monitor.CheckTemperature(25.0);
monitor.CheckTemperature(32.5);
monitor.CheckTemperature(28.0);
monitor.CheckTemperature(35.0);
}
}
TEXT
当前温度: 25°C
当前温度: 32.5°C
🚨 温度超限! 32.5°C > 阈值, 启动降温 (时间: 14:30:00)
当前温度: 28°C
当前温度: 35°C
🚨 温度超限! 35°C > 阈值, 启动降温 (时间: 14:30:01)
委托与事件的区别
| 特性 | 委托 | 事件 |
|---|---|---|
| 本质 | 类型(函数指针) | 修饰符(受限委托实例) |
| 外部调用 | 任何地方可调用 | 仅声明类内部可触发 |
| 外部赋值 | 可用 = 覆盖 |
只能用 += / -= |
| 用途 | 回调、策略模式 | 发布-订阅、通知机制 |
| 安全性 | 较低 | 较高,封装触发权限 |
📌 事件本质上是委托加上
event 修饰符后的封装,限制了外部对委托的调用和赋值权限,从而保证只有发布者能触发事件。
❓ 常见问题
Q 委托和事件有什么区别?
A 事件是加上了
event 修饰符的委托,外部只能用 +=/-= 订阅,无法直接调用或覆盖,只有声明类内部可以触发。Q 多播委托有返回值时返回什么?
A 返回最后一个被调用方法的返回值,前面的返回值会被丢弃。
Q Action 和 Func 有什么区别?
A Action 无返回值(void),Func 有返回值,最后一个泛型参数为返回类型。
Q 为什么要用
?.Invoke() 调用事件?A 防止事件无订阅者时抛出
NullReferenceException,?.Invoke() 在事件为 null 时自动跳过。Q Predicate 和 Func<T, bool> 等价吗?
A 功能等价,
Predicate<T> 是 Func<T, bool> 的特化版本,Predicate<T> 语义更明确,常用于筛选场景。📖 小节
- 委托是类型安全的函数指针,定义方法签名
- 使用
delegate关键字声明自定义委托类型 - 多播委托通过
+=/-=组合多个方法,按顺序执行 Action无返回值,Func有返回值,Predicate返回 bool- 匿名方法用
delegate{ }内联定义,Lambda 表达式是更简洁的替代 event关键字限制委托的调用和赋值权限,仅声明类可触发- 发布-订阅模式实现发布者与订阅者解耦
- 自定义 EventArgs 继承 EventArgs 基类,携带事件数据
📝 作业
- 定义一个
delegate string Transform(string s);委托,分别关联转大写、转小写、反转字符串三个方法,并测试多播调用 - 使用
Func<int, int, bool>实现一个比较器,判断第一个数是否大于第二个数 - 创建一个
FileWatcher类,定义event EventHandler<string> OnFileChanged,模拟文件变更时通知订阅者 - 自定义
StockPriceEventArgs(包含股票代码、价格、涨跌幅),实现一个股票价格监控器,当涨跌幅超过阈值时触发事件 - 对比使用自定义委托和
EventHandler<T>实现同一个事件,总结各自适用场景



