.NET进阶——深入理解委托(3)事件入门

为什么我要把事件放在委托这个专题里呢?主要的原因是事件是委托的高级封装

换句话说,先有委托才有事件,委托是事件的基础,事件是委托的封装

我们先看一个不用委托的例子,这个代码要求实现这样的功能:小猫叫->小孩哭->妈妈安慰->爸爸询问->邻居抱怨,这样的一个由小猫叫引发的一系列事件。

同时这个例子也是一个简单的观察者模式,如果没有了解过观察者模式,请大家仔细阅读代码或者询问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 条就够了)

  1. 事件的本质:是委托的 "安全封装",就像给委托加了个 "保护罩",只允许外部做「订阅(+=)」和「取消订阅(-=)」;

  2. 核心权限 :只有定义事件的类(猫类)能触发事件(调用Invoke),外部只能订阅 / 取消订阅,不能赋值、不能直接触发、不能置空;

  3. 使用流程

    • 定义委托(或用内置 Action/Func)→ 类里定义事件 → 外部订阅事件 → 类内部触发事件 → (可选)外部取消订阅。

用小猫叫的例子再梳理一遍:猫(事件拥有者)只在 "叫" 的时候触发事件,小孩 / 妈妈 / 邻居(订阅者)只能选择 "听"(订阅)或 "不听"(取消订阅),不能强迫猫叫(外部触发),也不能把别人的 "听" 权限删掉(覆盖委托) ------ 这就是事件的核心逻辑。

相关推荐
总有刁民想爱朕ha2 小时前
.NET 8 和 .NET 6 性能对比的测试
.net·性能测试·.net6·.net8
总有刁民想爱朕ha3 小时前
银河麒麟v10服务器版Docker部署.NET 8 WebAPI教程
docker·容器·.net·银河麒麟v10服务器版
仪***沿4 小时前
C# 与台达 PLC 串口通讯实现实时监控
.net
武藤一雄5 小时前
C# 万字拆解线程间通讯?
后端·微软·c#·.net·.netcore·多线程
赵庆明老师5 小时前
NET 10 集成Session
缓存·.net
lljss20205 小时前
C# 定时器类实现1s定时器更新UI
开发语言·c#
白杨攻城狮5 小时前
C# 关于 barierr 心得
开发语言·c#
SEO-狼术5 小时前
Detect Trends with Compact In-Cell Visuals
.net
江沉晚呤时6 小时前
延迟加载(Lazy Loading)详解及在 C# 中的应用
java·开发语言·microsoft·c#