Skip to Content
Week 03Day 5 - 事件机制

Day 5 - 事件机制

建议用时:220-260 分钟

你将学会什么

  • 事件为什么存在,它和普通方法调用有什么区别
  • 发布者、订阅者、触发事件分别是什么意思
  • event Actionevent EventHandler<T> 怎么写
  • += 订阅和 -= 取消订阅到底做了什么
  • 为什么触发事件要写 ?.Invoke(...)
  • 如何用事件把“发生了什么”和“发生后做什么”分开

事件不是界面专属知识。按钮点击是事件,订单创建是事件,下载进度变化也是事件。先用控制台把事件机制学清楚,再去看桌面界面里的点击事件会更稳。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“委托与 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. 今天要形成的代码判断

看到“某件事发生后,别的地方要响应”,就可以考虑事件。

写事件时按这个顺序:

  1. 找到谁会发生这件事。
  2. 在这个类里声明事件。
  3. 在事情真正完成后触发事件。
  4. 外部用 += 订阅。
  5. 不需要继续监听时用 -= 取消订阅。

第二部分:把知识组合成完整例子

这一部分只看效果。先知道事件最终能写成什么样,再去拆它的原理。

先看效果:订单创建后自动通知

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. 打开一个控制台项目。
  2. 每次只保留一个例子的代码,运行通过后再换下一个。
  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 只是用控制台模拟按钮。真正要理解的是:

  1. Clicked 是事件。
  2. += 是订阅。
  3. 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(); } }

这里有两个事件:

  1. ProgressChanged 表示进度变化。
  2. 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(...)
事件里写复杂业务触发者和响应者耦合事件只通知发生了什么,处理逻辑放订阅者
忘记取消订阅长生命周期对象可能被引用住不再需要时取消订阅
事件参数太少订阅者不知道发生细节定义清楚的事件参数对象
事件名不表达过去发生语义不清楚使用 SavedChangedCompleted 这类名称

小白重复敲写训练

事件要分成“发布者发出通知”和“订阅者收到通知”两部分来敲。

训练 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); } }

改动任务:依次发送 CreatedPaidShipped

训练 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:按钮点击事件

要求:

  1. 写一个 ButtonLike 类。
  2. 声明 Clicked 事件。
  3. 调用 Click() 时触发事件。
  4. 外部订阅后输出提示。

完整答案

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); } }

这份答案要看懂三点:

  1. ButtonLike 是发布者。
  2. Clicked 是事件。
  3. saveButton.Clicked += ... 是订阅。

作业 2:订单创建事件

要求:

  1. OrderService
  2. 创建订单后触发 OrderCreated
  3. 外部订阅日志和通知两个处理逻辑。

完整答案

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

要求:

  1. 订阅一次事件。
  2. 第一次触发时能输出。
  3. 取消订阅。
  4. 第二次触发时不再输出处理内容。

完整答案

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:标准事件数据

要求:

  1. EventHandler<T> 写一个支付成功事件。
  2. 事件数据包含订单号和金额。
  3. 外部订阅后输出事件来源和支付数据。

完整答案

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:完整库存变化通知

要求:

  1. 写一个库存类。
  2. 入库和出库时改变库存。
  3. 每次库存变化后触发事件。
  4. 事件数据包含商品名、旧库存、新库存。

完整答案

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; } }

这份答案包含完整的输入检查、状态变化、事件触发和事件处理。

本页最后要记住

  1. 事件表示“某件事发生了”。
  2. 发布者声明事件并在合适时机触发。
  3. 订阅者用 += 注册处理逻辑。
  4. 不再需要监听时用 -= 取消订阅。
  5. ?.Invoke(...) 可以避免没人订阅时空引用报错。
  6. 简单事件可以用 Action,正式事件常用 EventHandler<TEventArgs>
  7. 事件的价值是让发布者和订阅者不要互相绑死。