Day 5 - 事件机制
建议用时:220-260 分钟
你将学会什么
- 事件为什么存在,它和普通方法调用有什么区别
- 发布者、订阅者、触发事件分别是什么意思
event Action和event EventHandler<T>怎么写+=订阅和-=取消订阅到底做了什么- 为什么触发事件要写
?.Invoke(...) - 如何用事件把“发生了什么”和“发生后做什么”分开
事件不是界面专属知识。按钮点击是事件,订单创建是事件,下载进度变化也是事件。先用控制台把事件机制学清楚,再去看桌面界面里的点击事件会更稳。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“委托与 Lambda”,今天继续学习“事件机制”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“事件机制”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。事件的语法看起来多,但它背后的思想很直接:一件事发生后,通知关心它的人。
1. 普通方法调用的问题
如果不用事件,订单服务可能会这样写:
创建订单
保存订单
写日志
发短信
发邮件
更新统计这样的问题是:订单服务知道太多事情。
订单服务本来只应该负责“订单怎么创建”,现在它还必须知道:
| 后续动作 | 问题 |
|---|---|
| 写日志 | 日志格式变了,订单服务也要改 |
| 发短信 | 通知方式变了,订单服务也要改 |
| 发邮件 | 邮件逻辑失败,可能影响订单逻辑 |
| 更新统计 | 统计规则变了,订单服务也要改 |
事件的思路是:
订单服务只宣布:订单已创建。
谁关心这件事,谁自己订阅。2. 事件中的三个角色
| 角色 | 含义 | 在代码里通常是谁 |
|---|---|---|
| 发布者 | 拥有事件、负责触发事件 | OrderService |
| 订阅者 | 关心事件、注册处理逻辑 | 日志模块、通知模块 |
| 事件数据 | 事件发生时附带的信息 | 订单号、金额、进度 |
发布者不需要知道订阅者是谁,只需要在合适时机触发事件。
订阅者也不需要改发布者的内部代码,只要用 += 把处理逻辑挂上去。
3. event 和委托的关系
昨天学过委托:委托可以保存方法。
事件建立在委托之上。可以这样理解:
委托:保存可以调用的方法
事件:限制外部只能订阅和取消订阅,不能随便触发如果写成普通委托字段:
public Action? Changed;外部代码可以直接把它清空,也可以直接触发它,这不安全。
如果写成事件:
public event Action? Changed;外部代码只能:
Changed += ...
Changed -= ...外部不能直接:
Changed.Invoke()
Changed = null这就是 event 的价值:发布者控制什么时候触发,订阅者只能登记自己关心。
4. += 是订阅,-= 是取消订阅
订阅事件:
button.Clicked += OnClicked意思是:
当 Clicked 发生时,请调用 OnClicked取消订阅:
button.Clicked -= OnClicked意思是:
以后 Clicked 发生时,不要再调用 OnClicked如果同一个方法订阅两次,它就会执行两次。这不是事件坏了,而是你真的登记了两次。
5. 为什么触发事件要写 ?.Invoke
事件可能没人订阅。
如果没人订阅,事件字段就是 null。直接调用会报错。
所以触发事件通常写:
Changed?.Invoke()读法是:
如果 Changed 不是 null,就调用它;如果是 null,就什么也不做。6. Action 事件适合简单场景
如果事件数据很少,可以用 Action:
| 写法 | 含义 |
|---|---|
event Action? Saved | 事件发生时不带数据 |
event Action<string>? Created | 事件发生时带一个字符串 |
event Action<int>? ProgressChanged | 事件发生时带一个数字 |
event Action<string, decimal>? Paid | 事件发生时带订单号和金额 |
Action 简单、直接,适合练习和小程序。
7. EventHandler<T> 是标准事件模式
真实项目里还会经常看到:
event EventHandler<OrderCreatedEventArgs>? OrderCreated它的处理方法形状通常是:
(object? sender, OrderCreatedEventArgs e) => { ... }其中:
| 参数 | 含义 |
|---|---|
sender | 谁触发了这个事件 |
e | 事件附带的数据 |
EventArgs 是事件数据的标准基类。
事件常用写法速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 定义简单事件 | public event Action? Saved; | 没有事件数据 |
| 定义带数据事件 | public event Action<Product>? ProductSaved; | 传一个对象 |
| 标准事件 | event EventHandler<TEventArgs>? | 框架常见写法 |
| 订阅事件 | obj.Saved += Handler; | 发生时调用 Handler |
| 取消订阅 | obj.Saved -= Handler; | 不再接收通知 |
| 触发事件 | Saved?.Invoke(); | 没人订阅也安全 |
| 触发带数据事件 | ProductSaved?.Invoke(product); | 把数据传给订阅者 |
简单事件模板:
class ProductService
{
public event Action<string>? ProductCreated;
public void Create(string name)
{
ProductCreated?.Invoke(name);
}
}订阅模板:
var service = new ProductService();
service.ProductCreated += name => Console.WriteLine($"商品已创建: {name}");
service.Create("Keyboard");8. 事件和直接调用的区别
直接调用:
订单服务直接调用短信服务特点是:订单服务必须知道短信服务。
事件通知:
订单服务只触发 OrderCreated
短信服务自己订阅 OrderCreated特点是:订单服务不需要知道谁在听。
9. 什么时候适合用事件
适合:
| 场景 | 原因 |
|---|---|
| 某件事发生后,可能有多个后续动作 | 一个事件可以有多个订阅者 |
| 发布者不应该知道订阅者细节 | 降低耦合 |
| 状态变化需要通知外部 | 比如进度变化、数据变化 |
| 界面点击、输入变化 | 用户操作天然是事件 |
不适合:
| 场景 | 原因 |
|---|---|
| 必须马上得到返回值 | 事件一般不负责返回业务结果 |
| 调用顺序必须非常严格 | 普通方法更清晰 |
| 只有一个固定动作 | 直接调用更简单 |
| 复杂业务主流程 | 事件里应只做入口,复杂逻辑交给方法或服务 |
10. 今天要形成的代码判断
看到“某件事发生后,别的地方要响应”,就可以考虑事件。
写事件时按这个顺序:
- 找到谁会发生这件事。
- 在这个类里声明事件。
- 在事情真正完成后触发事件。
- 外部用
+=订阅。 - 不需要继续监听时用
-=取消订阅。
第二部分:把知识组合成完整例子
这一部分只看效果。先知道事件最终能写成什么样,再去拆它的原理。
先看效果:订单创建后自动通知
OrderService orderService = new OrderService();
orderService.OrderCreated += orderNo => Console.WriteLine($"日志系统记录订单: {orderNo}");
orderService.OrderCreated += orderNo => Console.WriteLine($"通知系统发送消息: {orderNo}");
orderService.Create("SO-001");
orderService.Create("SO-002");
class OrderService
{
public event Action<string>? OrderCreated;
public void Create(string orderNo)
{
Console.WriteLine($"正在创建订单: {orderNo}");
Console.WriteLine("订单保存完成");
OrderCreated?.Invoke(orderNo);
Console.WriteLine();
}
}运行后你会看到:订单服务只负责创建订单,日志和通知是在订单创建完成后自动执行的。
这里最重要的一句是:
public event Action<string>? OrderCreated;它表示:OrderService 对外声明了一个事件,事件发生时会附带一个 string 类型的订单号。
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 打开一个控制台项目。
- 每次只保留一个例子的代码,运行通过后再换下一个。
- 每个例子运行后,至少改一次事件数据,再运行确认变化。
例子 1:最小事件
ButtonLike button = new ButtonLike();
button.Clicked += () => Console.WriteLine("按钮被点击");
button.Click();
class ButtonLike
{
public event Action? Clicked;
public void Click()
{
Console.WriteLine("内部:准备触发点击事件");
Clicked?.Invoke();
}
}这里的 ButtonLike 只是用控制台模拟按钮。真正要理解的是:
Clicked是事件。+=是订阅。Click()内部触发事件。
例子 2:没人订阅时也不会报错
ButtonLike button = new ButtonLike();
button.Click();
class ButtonLike
{
public event Action? Clicked;
public void Click()
{
Console.WriteLine("点击发生");
Clicked?.Invoke();
Console.WriteLine("点击处理结束");
}
}因为用了 Clicked?.Invoke(),所以没人订阅时也不会抛异常。
例子 3:一个事件可以有多个订阅者
ButtonLike button = new ButtonLike();
button.Clicked += () => Console.WriteLine("日志:记录点击");
button.Clicked += () => Console.WriteLine("界面:显示提示");
button.Clicked += () => Console.WriteLine("统计:点击次数加一");
button.Click();
class ButtonLike
{
public event Action? Clicked;
public void Click()
{
Console.WriteLine("点击发生");
Clicked?.Invoke();
}
}一个事件可以挂多个处理逻辑。触发时,这些逻辑会依次执行。
例子 4:同一个方法订阅两次会执行两次
ButtonLike button = new ButtonLike();
button.Clicked += OnClicked;
button.Clicked += OnClicked;
button.Click();
void OnClicked()
{
Console.WriteLine("处理点击");
}
class ButtonLike
{
public event Action? Clicked;
public void Click()
{
Clicked?.Invoke();
}
}如果输出了两次“处理点击”,说明事件机制是正常的,因为你确实订阅了两次。
例子 5:取消订阅
ButtonLike button = new ButtonLike();
button.Clicked += OnClicked;
button.Click();
button.Clicked -= OnClicked;
button.Click();
void OnClicked()
{
Console.WriteLine("处理点击");
}
class ButtonLike
{
public event Action? Clicked;
public void Click()
{
Console.WriteLine("点击发生");
Clicked?.Invoke();
Console.WriteLine();
}
}第一次 Click() 会输出“处理点击”。取消订阅后,第二次 Click() 不会再输出这句话。
例子 6:事件带一个数据
Counter counter = new Counter();
counter.Changed += value => Console.WriteLine($"计数变化为: {value}");
counter.Add(1);
counter.Add(5);
class Counter
{
private int _value;
public event Action<int>? Changed;
public void Add(int number)
{
_value += number;
Changed?.Invoke(_value);
}
}Action<int> 表示事件触发时会传出一个 int。
例子 7:事件带两个数据
PaymentService paymentService = new PaymentService();
paymentService.Paid += (orderNo, amount) =>
{
Console.WriteLine($"订单 {orderNo} 已支付,金额 {amount}");
};
paymentService.Pay("SO-001", 199m);
class PaymentService
{
public event Action<string, decimal>? Paid;
public void Pay(string orderNo, decimal amount)
{
Console.WriteLine("正在处理支付");
Paid?.Invoke(orderNo, amount);
}
}Action<string, decimal> 表示事件触发时传出订单号和金额。
例子 8:订单创建事件
OrderService orderService = new OrderService();
orderService.OrderCreated += orderNo => Console.WriteLine($"写日志: {orderNo}");
orderService.OrderCreated += orderNo => Console.WriteLine($"发送通知: {orderNo}");
orderService.Create("SO-001");
orderService.Create("SO-002");
class OrderService
{
public event Action<string>? OrderCreated;
public void Create(string orderNo)
{
Console.WriteLine($"创建订单: {orderNo}");
OrderCreated?.Invoke(orderNo);
Console.WriteLine();
}
}订单服务不知道日志和通知的具体实现,它只是宣布“订单已创建”。
例子 9:进度变化事件
FileDownloader downloader = new FileDownloader();
downloader.ProgressChanged += percent => Console.WriteLine($"下载进度: {percent}%");
downloader.Completed += () => Console.WriteLine("下载完成");
downloader.Start();
class FileDownloader
{
public event Action<int>? ProgressChanged;
public event Action? Completed;
public void Start()
{
for (int percent = 0; percent <= 100; percent += 25)
{
ProgressChanged?.Invoke(percent);
}
Completed?.Invoke();
}
}这里有两个事件:
ProgressChanged表示进度变化。Completed表示下载完成。
例子 10:事件里不要塞太多业务
OrderService orderService = new OrderService();
NotificationService notificationService = new NotificationService();
orderService.OrderCreated += orderNo =>
{
notificationService.SendOrderCreatedMessage(orderNo);
};
orderService.Create("SO-001");
class OrderService
{
public event Action<string>? OrderCreated;
public void Create(string orderNo)
{
Console.WriteLine($"订单保存: {orderNo}");
OrderCreated?.Invoke(orderNo);
}
}
class NotificationService
{
public void SendOrderCreatedMessage(string orderNo)
{
Console.WriteLine($"发送订单创建通知: {orderNo}");
}
}事件处理代码里只做入口转发,真正逻辑放到 NotificationService 的方法里。
例子 11:标准 EventHandler 写法
OrderService orderService = new OrderService();
orderService.OrderCreated += (sender, e) =>
{
Console.WriteLine($"事件来源: {sender?.GetType().Name}");
Console.WriteLine($"订单号: {e.OrderNo}");
Console.WriteLine($"金额: {e.Amount}");
};
orderService.Create("SO-001", 199m);
class OrderService
{
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
public void Create(string orderNo, decimal amount)
{
Console.WriteLine("创建订单");
OrderCreated?.Invoke(this, new OrderCreatedEventArgs(orderNo, amount));
}
}
class OrderCreatedEventArgs : EventArgs
{
public OrderCreatedEventArgs(string orderNo, decimal amount)
{
OrderNo = orderNo;
Amount = amount;
}
public string OrderNo { get; }
public decimal Amount { get; }
}这就是 .NET 里常见的标准事件写法:
EventHandler<TEventArgs>事件来源放在 sender,事件数据放在 e。
例子 12:完整的库存变化事件
Inventory inventory = new Inventory("键盘", 10);
inventory.StockChanged += (sender, e) =>
{
Console.WriteLine($"商品: {e.ProductName}");
Console.WriteLine($"原库存: {e.OldStock}");
Console.WriteLine($"新库存: {e.NewStock}");
Console.WriteLine($"变化数量: {e.NewStock - e.OldStock}");
};
inventory.Add(5);
inventory.Remove(3);
class Inventory
{
public Inventory(string productName, int stock)
{
ProductName = productName;
Stock = stock;
}
public string ProductName { get; }
public int Stock { get; private set; }
public event EventHandler<StockChangedEventArgs>? StockChanged;
public void Add(int count)
{
ChangeStock(Stock + count);
}
public void Remove(int count)
{
ChangeStock(Stock - count);
}
private void ChangeStock(int newStock)
{
int oldStock = Stock;
Stock = newStock;
StockChanged?.Invoke(this, new StockChangedEventArgs(ProductName, oldStock, newStock));
}
}
class StockChangedEventArgs : EventArgs
{
public StockChangedEventArgs(string productName, int oldStock, int newStock)
{
ProductName = productName;
OldStock = oldStock;
NewStock = newStock;
}
public string ProductName { get; }
public int OldStock { get; }
public int NewStock { get; }
}这个例子把事件用在一个真实状态变化上:库存改变后,外部收到旧库存和新库存。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 事件没有判空就触发 | 没有订阅者时可能出错 | 使用 ?.Invoke(...) |
| 事件里写复杂业务 | 触发者和响应者耦合 | 事件只通知发生了什么,处理逻辑放订阅者 |
| 忘记取消订阅 | 长生命周期对象可能被引用住 | 不再需要时取消订阅 |
| 事件参数太少 | 订阅者不知道发生细节 | 定义清楚的事件参数对象 |
| 事件名不表达过去发生 | 语义不清楚 | 使用 Saved、Changed、Completed 这类名称 |
小白重复敲写训练
事件要分成“发布者发出通知”和“订阅者收到通知”两部分来敲。
训练 1:最小事件
var button = new FakeButton();
button.Clicked += () => Console.WriteLine("收到点击通知");
button.Click();
class FakeButton
{
public event Action? Clicked;
public void Click()
{
Clicked?.Invoke();
}
}第二遍再订阅一个处理器,观察一次点击输出两行。
训练 2:事件携带文本
var order = new Order();
order.StatusChanged += status => Console.WriteLine($"新状态: {status}");
order.ChangeStatus("Paid");
class Order
{
public event Action<string>? StatusChanged;
public void ChangeStatus(string status)
{
StatusChanged?.Invoke(status);
}
}改动任务:依次发送 Created、Paid、Shipped。
训练 3:取消订阅
var source = new MessageSource();
Action handler = () => Console.WriteLine("收到消息");
source.Changed += handler;
source.Raise();
source.Changed -= handler;
source.Raise();
class MessageSource
{
public event Action? Changed;
public void Raise() => Changed?.Invoke();
}先预测为什么第二次 Raise 没有输出。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:事件机制。今天至少要掌握的 3 个点是:
1. 事件为什么存在,它和普通方法调用有什么区别
2. 发布者、订阅者、触发事件分别是什么意思
3. `event Action` 和 `event EventHandler<T>` 怎么写答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:事件为什么存在,它和普通方法调用有什么区别。
上位机专项练习
事件让连接、采集值和报警发生变化时主动通知界面与日志模块。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:连接状态变化事件
class Device
{
public event Action<bool>? ConnectionChanged;
public void Connect() { ConnectionChanged?.Invoke(true); }
}
class Program
{
static void Main()
{
var device = new Device();
device.ConnectionChanged += online => Console.WriteLine($"在线: {online}");
device.Connect();
}
}运行结果或界面效果:
在线: True改动任务: 增加 Disconnect 并发布 false。
专项例子 2:新采集值事件
class Reader
{
public event Action<double>? ValueReceived;
public void Simulate(double value) { ValueReceived?.Invoke(value); }
}
class Program
{
static void Main()
{
var reader = new Reader();
reader.ValueReceived += value => Console.WriteLine($"收到: {value:F1}");
reader.Simulate(25.6);
}
}运行结果或界面效果:
收到: 25.6改动任务: 连续模拟三个数值。
专项例子 3:一个报警通知两个模块
class AlarmSource
{
public event Action<string>? AlarmRaised;
public void Raise(string message) => AlarmRaised?.Invoke(message);
}
class Program
{
static void Main()
{
var source = new AlarmSource();
source.AlarmRaised += message => Console.WriteLine($"界面: {message}");
source.AlarmRaised += message => Console.WriteLine($"日志: {message}");
source.Raise("温度过高");
}
}运行结果或界面效果:
界面: 温度过高
日志: 温度过高改动任务: 取消一个订阅。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着完整代码敲一遍,再修改订单号、金额和订阅逻辑验证。
作业 1:按钮点击事件
要求:
- 写一个
ButtonLike类。 - 声明
Clicked事件。 - 调用
Click()时触发事件。 - 外部订阅后输出提示。
完整答案
ButtonLike saveButton = new ButtonLike("保存");
saveButton.Clicked += name => Console.WriteLine($"{name} 按钮被点击");
saveButton.Click();
class ButtonLike
{
public ButtonLike(string name)
{
Name = name;
}
public string Name { get; }
public event Action<string>? Clicked;
public void Click()
{
Clicked?.Invoke(Name);
}
}这份答案要看懂三点:
ButtonLike是发布者。Clicked是事件。saveButton.Clicked += ...是订阅。
作业 2:订单创建事件
要求:
- 写
OrderService。 - 创建订单后触发
OrderCreated。 - 外部订阅日志和通知两个处理逻辑。
完整答案
OrderService orderService = new OrderService();
orderService.OrderCreated += orderNo => Console.WriteLine($"日志记录: {orderNo}");
orderService.OrderCreated += orderNo => Console.WriteLine($"发送通知: {orderNo}");
orderService.Create("SO-001");
orderService.Create("SO-002");
class OrderService
{
public event Action<string>? OrderCreated;
public void Create(string orderNo)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
Console.WriteLine("订单号不能为空");
return;
}
Console.WriteLine($"保存订单: {orderNo}");
OrderCreated?.Invoke(orderNo);
Console.WriteLine();
}
}这份答案要看懂一点:
OrderService 不直接调用日志系统和通知系统,它只触发 OrderCreated。作业 3:取消订阅
要求:
- 订阅一次事件。
- 第一次触发时能输出。
- 取消订阅。
- 第二次触发时不再输出处理内容。
完整答案
Counter counter = new Counter();
counter.Changed += OnCounterChanged;
counter.Add(1);
counter.Changed -= OnCounterChanged;
counter.Add(1);
void OnCounterChanged(int value)
{
Console.WriteLine($"计数器变化: {value}");
}
class Counter
{
private int _value;
public event Action<int>? Changed;
public void Add(int number)
{
_value += number;
Console.WriteLine($"当前值: {_value}");
Changed?.Invoke(_value);
Console.WriteLine();
}
}这份答案运行后,第一次会输出事件处理内容,第二次只会输出当前值。
作业 4:标准事件数据
要求:
- 用
EventHandler<T>写一个支付成功事件。 - 事件数据包含订单号和金额。
- 外部订阅后输出事件来源和支付数据。
完整答案
PaymentService paymentService = new PaymentService();
paymentService.Paid += (sender, e) =>
{
Console.WriteLine($"事件来源: {sender?.GetType().Name}");
Console.WriteLine($"订单号: {e.OrderNo}");
Console.WriteLine($"金额: {e.Amount}");
};
paymentService.Pay("SO-001", 268m);
class PaymentService
{
public event EventHandler<PaymentEventArgs>? Paid;
public void Pay(string orderNo, decimal amount)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
Console.WriteLine("订单号不能为空");
return;
}
if (amount <= 0)
{
Console.WriteLine("支付金额必须大于 0");
return;
}
Console.WriteLine("支付成功");
Paid?.Invoke(this, new PaymentEventArgs(orderNo, amount));
}
}
class PaymentEventArgs : EventArgs
{
public PaymentEventArgs(string orderNo, decimal amount)
{
OrderNo = orderNo;
Amount = amount;
}
public string OrderNo { get; }
public decimal Amount { get; }
}这份答案把事件数据单独放进 PaymentEventArgs,比直接传多个参数更清晰,也更接近真实项目写法。
作业 5:完整库存变化通知
要求:
- 写一个库存类。
- 入库和出库时改变库存。
- 每次库存变化后触发事件。
- 事件数据包含商品名、旧库存、新库存。
完整答案
Inventory inventory = new Inventory("显示器", 5);
inventory.StockChanged += (sender, e) =>
{
Console.WriteLine($"{e.ProductName} 库存变化");
Console.WriteLine($"旧库存: {e.OldStock}");
Console.WriteLine($"新库存: {e.NewStock}");
Console.WriteLine();
};
inventory.Increase(3);
inventory.Decrease(2);
inventory.Decrease(20);
class Inventory
{
public Inventory(string productName, int stock)
{
ProductName = productName;
Stock = stock;
}
public string ProductName { get; }
public int Stock { get; private set; }
public event EventHandler<StockChangedEventArgs>? StockChanged;
public void Increase(int count)
{
if (count <= 0)
{
Console.WriteLine("入库数量必须大于 0");
return;
}
ChangeStock(Stock + count);
}
public void Decrease(int count)
{
if (count <= 0)
{
Console.WriteLine("出库数量必须大于 0");
return;
}
if (count > Stock)
{
Console.WriteLine("库存不足,不能出库");
return;
}
ChangeStock(Stock - count);
}
private void ChangeStock(int newStock)
{
int oldStock = Stock;
Stock = newStock;
StockChanged?.Invoke(this, new StockChangedEventArgs(ProductName, oldStock, newStock));
}
}
class StockChangedEventArgs : EventArgs
{
public StockChangedEventArgs(string productName, int oldStock, int newStock)
{
ProductName = productName;
OldStock = oldStock;
NewStock = newStock;
}
public string ProductName { get; }
public int OldStock { get; }
public int NewStock { get; }
}这份答案包含完整的输入检查、状态变化、事件触发和事件处理。
本页最后要记住
- 事件表示“某件事发生了”。
- 发布者声明事件并在合适时机触发。
- 订阅者用
+=注册处理逻辑。 - 不再需要监听时用
-=取消订阅。 ?.Invoke(...)可以避免没人订阅时空引用报错。- 简单事件可以用
Action,正式事件常用EventHandler<TEventArgs>。 - 事件的价值是让发布者和订阅者不要互相绑死。