写在前面
承接篇2。本篇讲 WinForms 的事件驱动模型:老式 .NET event、订阅退订的讲究,以及最经典的坑——没退订导致的内存泄漏。
WinForms 是事件驱动的:用户点按钮 → 触发 Click 事件 → 你的处理函数跑。整个 UI 编程就是"挂事件、写处理函数"。但 .NET 的 event 模型有个反直觉的特性:订阅了不退订,会内存泄漏。这一篇把这个坑讲透。
一、.NET event 基础回顾
1
2
3
4
5
6
7
8
9
| // 声明
public event EventHandler<EventArgs> SomethingHappened;
// 触发
SomethingHappened?.Invoke(this, EventArgs.Empty);
// 订阅 / 退订
obj.SomethingHappened += Handler;
obj.SomethingHappened -= Handler;
|
event 本质是封装的委托(只能 +=/-=,不能外部直接赋值或触发)。EventHandler 标准签名:(object sender, EventArgs e)。
WinForms 控件的事件都是这个模型:Button.Click、TextBox.TextChanged、Form.FormClosing……
方式一:设计器挂接(最常用)
在 VS 属性面板的"事件"页,双击事件 → 生成处理函数 + 在 .Designer.cs 里 +=:
1
2
3
4
5
| // .Designer.cs
this.button1.Click += button1_Click;
// Form1.cs
private void button1_Click(object sender, EventArgs e) { ... }
|
方式二:代码挂接
1
2
3
| button1.Click += Button1_Click;
// 或 lambda
button1.Click += (s, e) => MessageBox.Show("clicked");
|
lambda 挂接的坑:lambda 没名字,无法 -= 退订(后面内存泄漏会讲)。要退订的事件,别用 lambda。
三、订阅与退订:为什么要退订
关键认知:事件订阅是引用链——事件源(被 += 的对象)持有订阅者(处理函数的 target)的引用。
1
2
3
4
| 事件源.button1.Click (委托链)
→ 持有 → Form 处理函数的 target(通常 this / Form 实例)
即:button1 活着,就持有 Form,Form 不能被 GC
|
这在 Form 内部通常没事(Form 和 button1 同寿)。但跨对象订阅时,泄漏就来了:
1
2
3
4
5
6
7
8
9
10
| // 泄漏场景:短寿命的 DetailForm 订阅长寿命的 EventBus 事件
public class DetailForm : Form
{
public DetailForm(EventBus bus)
{
bus.DataChanged += OnDataChanged; // 订阅
}
void OnDataChanged(object s, EventArgs e) { ... }
// DetailForm 关闭时,忘了 bus.DataChanged -= OnDataChanged
}
|
1
2
3
4
5
6
7
8
| 泄漏链:
EventBus(长寿命,singleton)
.DataChanged (委托链)
→ 持有 → DetailForm 实例(闭包 target)
DetailForm 关闭后,本该被 GC
但 EventBus 的事件链还持有它 → 永远活着 → 泄漏
每开一次 DetailForm 泄漏一个,内存蹭蹭涨
|
修复:DetailForm 关闭时退订:
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class DetailForm : Form
{
private readonly EventBus _bus;
public DetailForm(EventBus bus) { _bus = bus; _bus.DataChanged += OnDataChanged; }
void OnDataChanged(object s, EventArgs e) { ... }
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bus.DataChanged -= OnDataChanged; // 关键:退订
base.OnFormClosed(e);
}
}
|
规则:谁订阅谁退订。订阅的逆操作必须在订阅者生命周期结束时执行(Dispose / FormClosed)。
四、内存泄漏的三种典型场景
1
2
3
4
5
6
7
8
9
10
11
12
| 场景 1:短寿命对象订阅长寿命对象事件(最常见)
→ 上面 DetailForm + EventBus 例子
→ 修复:短寿命对象销毁时退订
场景 2:lambda 捕获 this
longLived.Event += () => this.DoSomething();
→ lambda 闭包持有 this,longLived 活着就持有 this
→ 修复:别用 lambda 捕获 this 订阅长寿命事件;用方法 + 显式 -=
场景 3:忘了退订的事件处理函数持有大对象
→ 处理函数 target 持有大 Bitmap/Form,不退订就一直不释放
→ 修复:同上,规则退订
|
五、弱事件:让事件源不持有订阅者
如果订阅者生命周期不可控(插件、动态加载的 Form),手动退订麻烦,弱事件模式让事件源不阻止订阅者被 GC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 用 WeakReference 包装订阅者(简化版弱事件)
public class WeakEvent<TArgs> where TArgs : EventArgs
{
private readonly List<WeakReference> _handlers = new();
public void Add(EventHandler<TArgs> h) => _handlers.Add(new WeakReference(h));
public void Raise(object sender, TArgs args)
{
for (int i = _handlers.Count - 1; i >= 0; i--)
{
if (_handlers[i].Target is EventHandler<TArgs> h)
h(sender, args);
else
_handlers.RemoveAt(i); // 已被 GC 的清理掉
}
}
}
|
生产别自己造:WPF 有内置 WeakEventManager,WinForms 没原生弱事件,但可用 WeakReference 封装或第三方库。原则:长寿命事件源 + 不可控订阅者用弱事件。
WinForms 几个常用生命周期事件:
| 事件 | 触发时机 | 典型用途 |
|---|
Form.Load | 窗体首次显示前 | 初始化数据、读配置 |
Form.FormClosing | 关闭前(可取消) | “确定退出?“弹窗、e.Cancel = true |
Form.FormClosed | 关闭后 | 退订事件、释放资源 |
Application.Idle | 消息队列空时 | 偶尔做的后台轻任务(已少用) |
Application.ApplicationExit | 进程退出 | 全局清理 |
1
2
3
4
5
| private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (MessageBox.Show("确定退出?", "确认", MessageBoxButtons.YesNo) == DialogResult.No)
e.Cancel = true; // 阻止关闭
}
|
FormClosing(ing,可取消)vs FormClosed(ed,已关闭,不可逆)——命名一致:ing 在前可干预,ed 在后已发生。
七、自定义事件
写自己的控件/组件时,常要发自定义事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class MyTimer : Component
{
// 1. 声明事件
public event EventHandler<TickEventArgs> Tick;
// 2. 触发(受保护的虚方法,子类可重写)
protected virtual void OnTick(TickEventArgs e) => Tick?.Invoke(this, e);
internal void Fire() => OnTick(new TickEventArgs { Count = ++_count });
private int _count;
}
public class TickEventArgs : EventArgs
{
public int Count { get; set; }
}
|
要点:
- 事件用
EventHandler<TArgs> 标准签名((object sender, TArgs e))。 EventArgs 子类装事件数据;无数据的用 EventArgs.Empty。- 触发用受保护虚方法
OnXxx,子类可重写拦截。
八、小结
1
2
3
4
5
6
7
| WinForms 事件核心:
✓ event 是封装的委托;+= 订阅、-= 退订
✓ 事件订阅是引用链:源持有订阅者 → 阻止 GC
✓ 规则:谁订阅谁退订(FormClosed/Dispose 里 -=)
✓ lambda 订阅无法退订 → 慎用于长寿命事件
✓ Form.Load/FormClosing/FormClosed 是常用生命周期事件
✓ 自定义事件用 EventHandler<TArgs> + OnXxx 虚方法
|
下一篇:GDI+ 绘制——Graphics、OnPaint、双缓冲、自定义控件,以及 GDI+ 对象的 Dispose。