为什么我要把事件放在委托这个专题里呢?主要的原因是事件是委托的高级封装。
换句话说,先有委托才有事件,委托是事件的基础,事件是委托的封装。
我们先看一个不用委托的例子,这个代码要求实现这样的功能:小猫叫->小孩哭->妈妈安慰->爸爸询问->邻居抱怨,这样的一个由小猫叫引发的一系列事件。
同时这个例子也是一个简单的观察者模式,如果没有了解过观察者模式,请大家仔细阅读代码或者询问AI大模型,相信你可以理解。
cs
using System;
// 第一步:定义委托(小猫叫的方法签名:无参数、无返回值)
public delegate void CatCryHandler();
// 第二步:定义猫类(发布者),包含公共委托字段
public class Cat
{
// 公开的委托字段(无任何封装)
public CatCryHandler CatCryDelegate;
// 小猫叫的方法
public void Miao()
{
Console.WriteLine("🐱 小猫:喵呜~~~");
// 调用委托,触发所有绑定的方法
CatCryDelegate?.Invoke();
}
}
// 第三步:定义订阅者(小孩、妈妈、爸爸、邻居)
public class Child
{
public void Cry() => Console.WriteLine("👶 小孩:哇呜呜呜,怕怕~");
}
public class Mother
{
public void Comfort() => Console.WriteLine("👩 妈妈:宝宝不怕,妈妈抱~");
}
public class Father
{
public void Ask() => Console.WriteLine("👨 爸爸:咋了?猫又叫了?");
}
public class Neighbor
{
public void Angry() => Console.WriteLine("👴 邻居:大半夜的,吵死了!");
}
// 测试代码
class Program
{
static void Main(string[] args)
{
// 1. 创建对象
Cat kitty = new Cat();
Child child = new Child();
Mother mom = new Mother();
Father dad = new Father();
Neighbor neighbor = new Neighbor();
// 2. 绑定委托(订阅)
kitty.CatCryDelegate += child.Cry;
kitty.CatCryDelegate += mom.Comfort;
kitty.CatCryDelegate += dad.Ask;
kitty.CatCryDelegate += neighbor.Angry;
// ❌ 问题1:外部可以直接赋值,覆盖所有之前的绑定!
// 比如不小心写了=,而不是+=,之前的4个方法全没了
kitty.CatCryDelegate = child.Cry; // 现在委托里只剩小孩哭,其他都没了
// ❌ 问题2:外部可以直接调用委托,不用等小猫叫!
Console.WriteLine("=== 外部直接调用委托(小猫还没叫)===");
kitty.CatCryDelegate.Invoke(); // 直接触发小孩哭,逻辑混乱
// ❌ 问题3:外部可以直接置空委托,清空所有绑定
kitty.CatCryDelegate = null;
// 3. 调用小猫叫方法,但委托已经被置空,啥都不执行
Console.WriteLine("\n=== 小猫真的叫了 ===");
kitty.Miao();
Console.ReadLine();
}
}
运行结果(暴露的坑):
diff
=== 外部直接调用委托(小猫还没叫)===
👶 小孩:哇呜呜呜,怕怕~
=== 小猫真的叫了 ===
🐱 小猫:喵呜~~~
能看到:直接用公共委托字段,外部可以随意修改、触发、清空委托,完全破坏了 "只有小猫叫才触发动作" 的逻辑 ------ 这就是事件要解决的核心问题:给委托加 "保护罩"。
二、第二步:引入事件,解决委托的坑
事件的本质就是 "封装委托的保护罩",只开放「订阅(+=)」和「取消订阅(-=)」,禁止外部赋值、直接调用、置空。
核心改法:把委托字段换成事件
只需要改猫类里的一行代码,再理解事件的核心规则即可:
cs
public class Cat
{
// ❌ 原来的公共委托字段(有坑)
// public CatCryHandler CatCryDelegate;
✅ // 改成事件(基于同一个委托)
public event CatCryHandler CatCryEvent;
public void Miao()
{
Console.WriteLine("🐱 小猫:喵呜~~~");
// 只有猫类内部能调用事件(触发委托)
CatCryEvent?.Invoke();
}
}
三、完整的 "小猫叫 + 事件" 实现(从头写,逐行解释)
下面是完整、可运行的代码,每一步都配解释,跟着看就能懂:
cs
using System;
// ===================== 第一步:定义委托(事件的"底层契约")=====================
// 委托定义了"小猫叫要触发的方法"的签名:无参数、无返回值
// 所有要绑定到事件的方法,必须符合这个签名
public delegate void CatCryHandler();
// ===================== 第二步:定义发布者(猫类,拥有事件)=====================
public class Cat
{
// 定义事件:语法是「public event 委托类型 事件名;」
// 编译器会自动生成:私有委托字段 + 仅开放+=/-=的add/remove方法
public event CatCryHandler CatCryEvent;
// 小猫叫的核心方法(只有这个方法能触发事件)
public void Miao()
{
Console.WriteLine("\n🐱 小猫:喵呜~~~");
// 触发事件(调用底层委托):只有猫类内部能执行这行代码!
// ?. 是"空值保护":如果没有订阅者,委托为null,不会报错
CatCryEvent?.Invoke();
}
}
// ===================== 第三步:定义订阅者(关注小猫叫的对象)=====================
// 订阅者1:小孩
public class Child
{
public string Name { get; }
public Child(string name) => Name = name;
// 订阅方法:签名必须和委托CatCryHandler一致(无参数、无返回值)
public void Cry() => Console.WriteLine($"👶 {Name}:哇呜呜呜,怕小猫~");
}
// 订阅者2:妈妈
public class Mother
{
public string Name { get; }
public Mother(string name) => Name = name;
public void ComfortChild() => Console.WriteLine($"👩 {Name}:宝宝不怕,小猫不咬人~");
}
// 订阅者3:爸爸
public class Father
{
public string Name { get; }
public Father(string name) => Name = name;
public void CheckCat() => Console.WriteLine($"👨 {Name}:别慌,我去看看小猫~");
}
// 订阅者4:邻居
public class Neighbor
{
public string Name { get; }
public Neighbor(string name) => Name = name;
public void Complain() => Console.WriteLine($"👴 {Name}:谁家的猫啊,吵死了!");
}
// ===================== 第四步:使用事件(订阅、触发、取消订阅)=====================
class Program
{
static void Main(string[] args)
{
// 1. 创建发布者(小猫)
Cat kitty = new Cat();
// 2. 创建订阅者
Child xiaoMing = new Child("小明");
Mother liLi = new Mother("李丽");
Father zhangSan = new Father("张三");
Neighbor wangYe = new Neighbor("王大爷");
// 3. 订阅事件(外部只能用 +=,不能用=!)
Console.WriteLine("=== 开始订阅小猫叫事件 ===");
kitty.CatCryEvent += xiaoMing.Cry; // 小明订阅:小猫叫→小明哭
kitty.CatCryEvent += liLi.ComfortChild; // 李丽订阅:小猫叫→妈妈安慰
kitty.CatCryEvent += zhangSan.CheckCat; // 张三订阅:小猫叫→爸爸查看
kitty.CatCryEvent += wangYe.Complain; // 王大爷订阅:小猫叫→邻居抱怨
// 4. 触发事件(只能通过猫类的Miao方法,外部不能直接调用!)
Console.WriteLine("\n=== 第一次小猫叫 ===");
kitty.Miao();
// 5. 取消订阅(外部只能用 -=)
Console.WriteLine("\n=== 王大爷取消订阅 ===");
kitty.CatCryEvent -= wangYe.Complain; // 王大爷不想听了,取消订阅
// 6. 再次触发事件
Console.WriteLine("\n=== 第二次小猫叫(王大爷已取消)===");
kitty.Miao();
// ❌ 以下操作全部编译报错(事件的保护机制),注释掉可验证:
// kitty.CatCryEvent = xiaoMing.Cry; // 错误:不能用=赋值,只能+=/-=
// kitty.CatCryEvent.Invoke(); // 错误:外部不能直接触发事件
// kitty.CatCryEvent = null; // 错误:外部不能置空事件
Console.ReadLine();
}
}
运行结果(符合预期,无安全隐患):
diff
=== 开始订阅小猫叫事件 ===
=== 第一次小猫叫 ===
🐱 小猫:喵呜~~~
👶 小明:哇呜呜呜,怕小猫~
👩 李丽:宝宝不怕,小猫不咬人~
👨 张三:别慌,我去看看小猫~
👴 王大爷:谁家的猫啊,吵死了!
=== 王大爷取消订阅 ===
=== 第二次小猫叫(王大爷已取消)===
🐱 小猫:喵呜~~~
👶 小明:哇呜呜呜,怕小猫~
👩 李丽:宝宝不怕,小猫不咬人~
👨 张三:别慌,我去看看小猫~
四、拆解事件的核心规则(结合小猫例子)
用表格总结,每一条都对应上面的代码,一看就懂:
| 操作 / 规则 | 具体说明(小猫例子) | 是否允许 |
|---|---|---|
| 定义事件 | 猫类里写 public event CatCryHandler CatCryEvent; |
✅ 必须在类内部定义 |
| 订阅事件 | 外部用 kitty.CatCryEvent += 方法名(如+= xiaoMing.Cry) |
✅ 外部仅允许这个操作 |
| 取消订阅 | 外部用 kitty.CatCryEvent -= 方法名(如-= wangYe.Complain) |
✅ 外部仅允许这个操作 |
| 触发事件 | 只有猫类内部能写 CatCryEvent?.Invoke()(在 Miao 方法里) |
❌ 外部绝对不能 |
| 直接赋值事件 | 外部写 kitty.CatCryEvent = xiaoMing.Cry |
❌ 编译报错 |
| 置空事件 | 外部写 kitty.CatCryEvent = null |
❌ 编译报错 |
| 事件的本质 | 编译器自动生成 "私有委托字段 + 仅开放 +=/-= 的方法" | ✅ 不用自己写,编译器帮你封装 |
五、进阶:用内置委托(Action)简化代码(实战常用)
上面我们自定义了 CatCryHandler 委托,实际开发中可以用 .NET 内置的 Action(无参数、无返回值),省去自定义委托的步骤,代码更简洁:
cs
using System;
// 猫类:直接用Action定义事件,无需自定义委托
public class Cat
{
// 用内置Action替代自定义CatCryHandler
public event Action CatCryEvent;
public void Miao()
{
Console.WriteLine("\n🐱 小猫:喵呜~~~");
CatCryEvent?.Invoke();
}
}
// 订阅者、测试代码和之前完全一样,无需修改!
// (因为Action的签名就是"无参数、无返回值",和我们的订阅方法匹配)
运行结果和之前完全一致,但少写了 public delegate void CatCryHandler(); 这一行 ------ 这是实际开发中最常用的写法。
总结(核心要点,记牢这 3 条就够了)
-
事件的本质:是委托的 "安全封装",就像给委托加了个 "保护罩",只允许外部做「订阅(+=)」和「取消订阅(-=)」;
-
核心权限 :只有定义事件的类(猫类)能触发事件(调用
Invoke),外部只能订阅 / 取消订阅,不能赋值、不能直接触发、不能置空; -
使用流程:
- 定义委托(或用内置 Action/Func)→ 类里定义事件 → 外部订阅事件 → 类内部触发事件 → (可选)外部取消订阅。
用小猫叫的例子再梳理一遍:猫(事件拥有者)只在 "叫" 的时候触发事件,小孩 / 妈妈 / 邻居(订阅者)只能选择 "听"(订阅)或 "不听"(取消订阅),不能强迫猫叫(外部触发),也不能把别人的 "听" 权限删掉(覆盖委托) ------ 这就是事件的核心逻辑。