目 录CONTENT

文章目录

CSharp(四十二) 中事件的定义、使用和注意事项

C# 中事件的定义、使用和注意事项

一、什么是事件

事件,英文叫 event

在 C# 中,事件可以理解为:

当某件事情发生时,通知其他对象来执行对应的代码。

比如:

  • 按钮被点击了
  • 文件下载完成了
  • 用户登录成功了
  • 角色血量变成 0 了
  • 订单支付成功了
  • 定时器时间到了

这些都可以看成“事件发生了”。

生活中也有类似的例子:

门铃响了,屋里的人听到后去开门。
门铃是事件源,响铃是事件,开门的人是事件处理者。

在程序中也是类似:

某个对象负责“发生事件”,其他对象负责“订阅事件”,一旦事件发生,订阅者就会收到通知并执行代码。


二、为什么需要事件

假设我们有一个下载器,下载完成后要提醒用户。

一种简单写法是直接在下载器里面写提示逻辑:

class Downloader
{
    public void Download()
    {
        Console.WriteLine("开始下载...");
        Console.WriteLine("下载完成");

        Console.WriteLine("弹出提示:下载完成了");
    }
}

这段代码能运行,但问题是:

  • 下载器和提示逻辑绑得太死。
  • 如果以后不想弹窗,而是写日志、发通知、刷新界面,就要修改 Downloader 类。
  • 如果有多个地方都想知道“下载完成了”,代码会越来越乱。

使用事件可以让代码更灵活:

class Downloader
{
    public event Action DownloadCompleted;

    public void Download()
    {
        Console.WriteLine("开始下载...");
        Console.WriteLine("下载完成");

        DownloadCompleted?.Invoke();
    }
}

使用时:

Downloader downloader = new Downloader();

downloader.DownloadCompleted += () =>
{
    Console.WriteLine("收到通知:下载完成了");
};

downloader.Download();

这样 Downloader 只负责下载和发出通知,至于通知之后做什么,由外部订阅者决定。

这就是事件的重要作用:

事件可以降低对象之间的耦合,让“发生事情的一方”和“处理事情的一方”分开。


三、事件中的几个角色

学习事件时,可以先记住三个角色。

角色 说明 例子
事件发布者 负责定义事件、触发事件的对象 按钮、下载器、定时器
事件订阅者 关注事件、处理事件的对象 界面、日志模块、业务模块
事件处理方法 事件发生时执行的方法 点击按钮后执行的方法

例如:

button.Click += Button_Click;

这里:

  • button 是事件发布者。
  • Click 是事件。
  • Button_Click 是事件处理方法。
  • += 表示订阅事件。

四、事件和委托的关系

在 C# 中,事件是基于委托实现的。

简单理解:

委托规定了“事件处理方法长什么样”,事件负责“保存和通知这些方法”。

先看一个委托:

public delegate void MyDelegate();

这个委托表示:

可以指向一个没有参数、没有返回值的方法。

然后可以基于这个委托声明事件:

public event MyDelegate MyEvent;

完整示例:

using System;

class Alarm
{
    public delegate void AlarmHandler();

    public event AlarmHandler Ring;

    public void Start()
    {
        Console.WriteLine("闹钟开始工作");

        Ring?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        Alarm alarm = new Alarm();

        alarm.Ring += WakeUp;

        alarm.Start();
    }

    static void WakeUp()
    {
        Console.WriteLine("起床了");
    }
}

输出:

闹钟开始工作
起床了

这里的关键点:

  • AlarmHandler 是委托类型。
  • Ring 是事件。
  • WakeUp 是事件处理方法。
  • alarm.Ring += WakeUp; 表示订阅事件。
  • Ring?.Invoke(); 表示触发事件。

五、事件的基本语法

事件声明的一般格式:

访问修饰符 event 委托类型 事件名;

例如:

public event Action OnStart;

或者:

public delegate void StartedHandler();

public event StartedHandler Started;

常见写法有两种:

1. 使用自定义委托

public delegate void MessageHandler(string message);

public event MessageHandler MessageReceived;

触发事件:

MessageReceived?.Invoke("你好");

订阅事件:

obj.MessageReceived += message =>
{
    Console.WriteLine(message);
};

2. 使用系统内置委托

C# 已经提供了很多常用委托,例如:

  • Action
  • Action<T>
  • Func<T>
  • EventHandler
  • EventHandler<TEventArgs>

所以很多时候不需要自己声明委托。

例如:

public event Action<string> MessageReceived;

触发:

MessageReceived?.Invoke("你好");

六、事件的完整使用步骤

定义和使用事件,一般有四步。

第一步:定义事件

class Publisher
{
    public event Action SomethingHappened;
}

第二步:触发事件

class Publisher
{
    public event Action SomethingHappened;

    public void DoSomething()
    {
        Console.WriteLine("正在做事情...");

        SomethingHappened?.Invoke();
    }
}

这里的:

SomethingHappened?.Invoke();

表示:

如果有人订阅了这个事件,就通知他们;如果没人订阅,就什么也不做。

第三步:订阅事件

Publisher publisher = new Publisher();

publisher.SomethingHappened += () =>
{
    Console.WriteLine("收到通知:事情发生了");
};

+= 表示添加一个事件处理方法。

第四步:执行触发事件的方法

publisher.DoSomething();

完整代码:

using System;

class Publisher
{
    public event Action SomethingHappened;

    public void DoSomething()
    {
        Console.WriteLine("正在做事情...");

        SomethingHappened?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        Publisher publisher = new Publisher();

        publisher.SomethingHappened += () =>
        {
            Console.WriteLine("收到通知:事情发生了");
        };

        publisher.DoSomething();
    }
}

输出:

正在做事情...
收到通知:事情发生了

七、事件处理方法的写法

事件处理方法可以有几种常见写法。

1. 使用普通方法

publisher.SomethingHappened += HandleSomethingHappened;

static void HandleSomethingHappened()
{
    Console.WriteLine("事件被处理了");
}

这种写法适合逻辑较多、需要复用、需要取消订阅的情况。

2. 使用匿名函数

publisher.SomethingHappened += () =>
{
    Console.WriteLine("事件被处理了");
};

这种写法适合逻辑很短、只在这里使用的情况。

3. 使用 Lambda 表达式

如果事件带参数:

public event Action<string> MessageReceived;

订阅时:

publisher.MessageReceived += message =>
{
    Console.WriteLine("收到消息:" + message);
};

触发时:

MessageReceived?.Invoke("你好,事件");

八、带参数的事件

很多事件发生时,需要把一些信息传给订阅者。

例如用户登录成功后,需要告诉外部“是谁登录了”。

using System;

class UserService
{
    public event Action<string> UserLoggedIn;

    public void Login(string userName)
    {
        Console.WriteLine($"{userName} 正在登录...");
        Console.WriteLine($"{userName} 登录成功");

        UserLoggedIn?.Invoke(userName);
    }
}

class Program
{
    static void Main()
    {
        UserService userService = new UserService();

        userService.UserLoggedIn += name =>
        {
            Console.WriteLine("日志记录:用户登录成功,用户名是 " + name);
        };

        userService.UserLoggedIn += name =>
        {
            Console.WriteLine("欢迎回来," + name);
        };

        userService.Login("小明");
    }
}

输出:

小明 正在登录...
小明 登录成功
日志记录:用户登录成功,用户名是 小明
欢迎回来,小明

这个例子说明:

  • 一个事件可以有多个订阅者。
  • 事件触发时,会依次调用所有订阅者。
  • 事件参数可以把有用的信息传出去。

九、使用 EventHandler 的标准事件写法

在 .NET 中,事件有一种很常见、很标准的写法:

public event EventHandler SomeEvent;

EventHandler 表示事件处理方法通常长这样:

void 方法名(object sender, EventArgs e)

其中:

  • sender 表示是谁触发了事件。
  • e 表示事件相关的数据。

1. 不带额外数据的标准事件

using System;

class Door
{
    public event EventHandler Opened;

    public void Open()
    {
        Console.WriteLine("门打开了");

        Opened?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        Door door = new Door();

        door.Opened += Door_Opened;

        door.Open();
    }

    static void Door_Opened(object sender, EventArgs e)
    {
        Console.WriteLine("收到通知:门已经打开");
    }
}

这里的:

Opened?.Invoke(this, EventArgs.Empty);

表示触发事件。

this 表示当前这个 Door 对象就是事件发送者。

EventArgs.Empty 表示没有额外数据。

2. 带额外数据的标准事件

如果事件需要传更多信息,可以自定义一个继承自 EventArgs 的类。

例如订单支付成功事件,需要传订单编号和金额。

using System;

class OrderPaidEventArgs : EventArgs
{
    public string OrderId { get; }
    public decimal Amount { get; }

    public OrderPaidEventArgs(string orderId, decimal amount)
    {
        OrderId = orderId;
        Amount = amount;
    }
}

class OrderService
{
    public event EventHandler<OrderPaidEventArgs> OrderPaid;

    public void Pay(string orderId, decimal amount)
    {
        Console.WriteLine($"订单 {orderId} 支付成功,金额 {amount}");

        OrderPaid?.Invoke(this, new OrderPaidEventArgs(orderId, amount));
    }
}

class Program
{
    static void Main()
    {
        OrderService orderService = new OrderService();

        orderService.OrderPaid += OrderService_OrderPaid;

        orderService.Pay("A001", 99.9m);
    }

    static void OrderService_OrderPaid(object sender, OrderPaidEventArgs e)
    {
        Console.WriteLine($"收到订单支付事件:订单号 {e.OrderId},金额 {e.Amount}");
    }
}

输出:

订单 A001 支付成功,金额 99.9
收到订单支付事件:订单号 A001,金额 99.9

这是 .NET 中非常推荐的事件写法。


十、自定义事件和标准事件的选择

初学时可能会疑惑:

到底应该用 Action,还是用 EventHandler

可以这样理解:

写法 适合场景
event Action 简单事件,不需要参数
event Action<T> 简单事件,需要传少量数据
event EventHandler 标准事件,不需要额外数据
event EventHandler<TEventArgs> 标准事件,需要传事件数据

实际项目中,如果是公开 API、类库、框架代码,更推荐使用 EventHandlerEventHandler<TEventArgs>


十一、事件的订阅和取消订阅

1. 订阅事件:+=

publisher.SomethingHappened += HandleSomething;

意思是:

SomethingHappened 发生时,请调用 HandleSomething

2. 取消订阅事件:-=

publisher.SomethingHappened -= HandleSomething;

意思是:

以后这个事件发生时,不要再调用 HandleSomething

完整示例:

using System;

class TimerLike
{
    public event Action Tick;

    public void Run()
    {
        Tick?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        TimerLike timer = new TimerLike();

        timer.Tick += OnTick;

        timer.Run(); // 会执行 OnTick

        timer.Tick -= OnTick;

        timer.Run(); // 不会执行 OnTick
    }

    static void OnTick()
    {
        Console.WriteLine("时间到了");
    }
}

输出:

时间到了

3. 匿名函数取消订阅的问题

下面这种写法可以订阅:

timer.Tick += () =>
{
    Console.WriteLine("时间到了");
};

但是下面这种写法通常不能取消前面的订阅:

timer.Tick -= () =>
{
    Console.WriteLine("时间到了");
};

原因是:

两段看起来一样的匿名函数,其实是两个不同的函数对象。

如果以后需要取消订阅,应该先把匿名函数保存到变量中:

Action handler = () =>
{
    Console.WriteLine("时间到了");
};

timer.Tick += handler;

// 以后需要取消时
timer.Tick -= handler;

十二、事件可以有多个订阅者

一个事件可以被多个方法订阅。

using System;

class Button
{
    public event Action Click;

    public void Press()
    {
        Console.WriteLine("按钮被按下");

        Click?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        Button button = new Button();

        button.Click += ShowMessage;
        button.Click += WriteLog;
        button.Click += RefreshPage;

        button.Press();
    }

    static void ShowMessage()
    {
        Console.WriteLine("显示提示信息");
    }

    static void WriteLog()
    {
        Console.WriteLine("记录日志");
    }

    static void RefreshPage()
    {
        Console.WriteLine("刷新页面");
    }
}

输出:

按钮被按下
显示提示信息
记录日志
刷新页面

这说明事件很适合“一件事发生后,多个地方都要做出反应”的场景。


十三、事件只能由声明它的类触发

事件和普通委托很像,但事件有一个重要限制:

在类的外部,只能订阅和取消订阅事件,不能直接触发事件。

例如:

class Publisher
{
    public event Action SomethingHappened;

    public void RaiseEvent()
    {
        SomethingHappened?.Invoke();
    }
}

外部可以这样写:

publisher.SomethingHappened += Handler;
publisher.SomethingHappened -= Handler;

但不能这样写:

// 错误:外部不能直接触发事件
publisher.SomethingHappened?.Invoke();

这个限制很重要。

它保证了:

事件什么时候发生,只能由事件发布者自己决定。

如果不用 event,只暴露一个普通委托字段:

public Action SomethingHappened;

外部就可以随便调用、清空、覆盖它:

publisher.SomethingHappened = null;
publisher.SomethingHappened?.Invoke();

这会破坏封装。

所以事件比直接暴露委托更安全。


十四、事件触发时为什么常用 ?.Invoke()

触发事件时,经常看到这种写法:

SomethingHappened?.Invoke();

它等价于:

if (SomethingHappened != null)
{
    SomethingHappened();
}

如果没人订阅事件,事件就是 null

直接调用会报错:

// 如果没人订阅,会出现 NullReferenceException
SomethingHappened();

所以要先判断是否为空。

?.Invoke() 是更简洁、更常见的写法。

带参数时:

MessageReceived?.Invoke("你好");

标准事件时:

Opened?.Invoke(this, EventArgs.Empty);

十五、建议封装触发事件的方法

在较规范的代码中,通常会把触发事件的逻辑封装到一个 On事件名 方法中。

例如:

class Door
{
    public event EventHandler Opened;

    public void Open()
    {
        Console.WriteLine("门打开了");

        OnOpened();
    }

    protected virtual void OnOpened()
    {
        Opened?.Invoke(this, EventArgs.Empty);
    }
}

为什么这样写?

  • 触发事件的逻辑集中在一个方法里。
  • 子类可以重写 OnOpened,扩展行为。
  • 更符合 .NET 的常见编码习惯。

如果带参数:

protected virtual void OnOrderPaid(OrderPaidEventArgs e)
{
    OrderPaid?.Invoke(this, e);
}

然后调用:

OnOrderPaid(new OrderPaidEventArgs(orderId, amount));

十六、事件和普通方法调用的区别

普通方法调用:

SendMessage();

特点:

  • 调用者明确知道要调用谁。
  • 通常是一对一调用。
  • 关系比较直接。

事件调用:

MessageReceived?.Invoke();

特点:

  • 发布者不知道具体谁在处理。
  • 可以有多个订阅者。
  • 发布者和订阅者之间关系更松散。

对比:

对比项 普通方法调用 事件
调用关系 调用者知道被调用者 发布者不知道订阅者
数量 通常一对一 可以一对多
耦合度 较高 较低
适合场景 明确调用某个功能 某件事发生后通知外部

十七、事件和委托字段的区别

很多初学者会觉得事件和委托字段很像。

例如:

public Action OnChanged;

和:

public event Action Changed;

它们确实有关系,但不一样。

普通委托字段的问题

class Counter
{
    public Action Changed;
}

外部可以这样做:

counter.Changed = null;
counter.Changed = SomeOtherMethod;
counter.Changed?.Invoke();

也就是说,外部可以:

  • 覆盖所有订阅者。
  • 清空所有订阅者。
  • 在类外部随便触发。

这不安全。

使用事件更安全

class Counter
{
    public event Action Changed;
}

外部只能:

counter.Changed += Handler;
counter.Changed -= Handler;

不能直接:

counter.Changed = null;
counter.Changed?.Invoke();

所以:

对外通知时,应该优先使用 event,不要直接暴露 public 委托字段。


十八、事件的命名习惯

C# 中事件命名通常使用 PascalCase,也就是每个单词首字母大写。

常见事件名:

Clicked
Opened
Closed
Changed
Completed
Failed
UserLoggedIn
OrderPaid

事件处理方法常见命名:

Button_Click
Door_Opened
OrderService_OrderPaid

触发事件的方法常见命名:

OnClicked()
OnOpened()
OnOrderPaid()

事件参数类通常以 EventArgs 结尾:

OrderPaidEventArgs
UserLoggedInEventArgs
FileDownloadedEventArgs

十九、常见使用场景

1. UI 控件事件

WinForms 或 WPF 中,按钮点击就是典型事件。

button1.Click += (sender, e) =>
{
    MessageBox.Show("按钮被点击了");
};

这里:

  • button1 是事件发布者。
  • Click 是事件。
  • Lambda 是事件处理方法。

2. 业务状态变化通知

例如用户登录成功:

public event EventHandler<UserLoggedInEventArgs> UserLoggedIn;

登录成功后触发:

UserLoggedIn?.Invoke(this, new UserLoggedInEventArgs(userName));

3. 进度通知

例如文件下载进度变化:

public event Action<int> ProgressChanged;

public void Download()
{
    for (int progress = 0; progress <= 100; progress += 10)
    {
        ProgressChanged?.Invoke(progress);
    }
}

订阅:

downloader.ProgressChanged += progress =>
{
    Console.WriteLine($"当前进度:{progress}%");
};

4. 定时器

timer.Tick += (sender, e) =>
{
    Console.WriteLine("定时器触发");
};

5. 游戏开发

例如角色死亡:

public event Action PlayerDied;

public void TakeDamage(int damage)
{
    hp -= damage;

    if (hp <= 0)
    {
        PlayerDied?.Invoke();
    }
}

订阅:

player.PlayerDied += () =>
{
    Console.WriteLine("播放死亡动画");
};

player.PlayerDied += () =>
{
    Console.WriteLine("显示失败界面");
};

二十、事件的注意事项

1. 触发事件前要判断是否有人订阅

推荐:

Changed?.Invoke();

不推荐:

Changed();

如果没人订阅,直接调用会报空引用异常。

2. 不要在类外部直接控制事件触发

事件应该由声明它的类自己触发。

外部对象只负责订阅和取消订阅。

这样可以保持封装性。

3. 长时间存在的事件要注意取消订阅

如果一个短生命周期对象订阅了一个长生命周期对象的事件,却没有取消订阅,可能导致短生命周期对象无法被垃圾回收。

例如:

globalService.Changed += page.HandleChanged;

如果 globalService 一直存在,而 page 已经不需要了,但没有取消订阅,那么 globalService 仍然持有 page 的引用。

更稳妥的做法:

globalService.Changed -= page.HandleChanged;

在 UI 页面关闭、对象释放、服务停止时,要特别注意取消订阅。

4. 匿名函数订阅后不方便取消

下面写法很简洁:

button.Click += (sender, e) =>
{
    Console.WriteLine("点击了按钮");
};

但如果以后需要取消订阅,就不方便。

如果需要取消订阅,请保存事件处理器:

EventHandler handler = (sender, e) =>
{
    Console.WriteLine("点击了按钮");
};

button.Click += handler;
button.Click -= handler;

5. 事件处理方法中不要写过重逻辑

事件触发后,所有订阅者通常会依次执行。

如果某个事件处理方法执行很慢,可能会影响整个流程。

例如:

button.Click += (sender, e) =>
{
    // 不推荐在 UI 事件里直接执行很耗时的操作
    LongTimeWork();
};

更好的做法是:

button.Click += async (sender, e) =>
{
    await LongTimeWorkAsync();
};

尤其是在 UI 程序中,耗时操作容易导致界面卡住。

6. 事件处理方法中出现异常要小心

如果一个事件有多个订阅者,其中一个订阅者抛出异常,后面的订阅者可能不会继续执行。

例如:

publisher.Changed += Handler1;
publisher.Changed += Handler2;
publisher.Changed += Handler3;

如果 Handler2 抛出异常,Handler3 可能无法执行。

如果事件很关键,可以在触发时分别调用每个订阅者,并单独处理异常。

示例:

var handlers = Changed?.GetInvocationList();

if (handlers != null)
{
    foreach (Action handler in handlers)
    {
        try
        {
            handler();
        }
        catch (Exception ex)
        {
            Console.WriteLine("事件处理出错:" + ex.Message);
        }
    }
}

初学阶段不必一开始就写这么复杂,但要知道:

多个事件处理方法是依次执行的,一个方法出错可能影响后面的处理。

7. 多线程环境下要注意线程安全

在简单程序中,使用:

Changed?.Invoke();

通常就够了。

在多线程环境中,有时会先复制一份事件引用:

var handler = Changed;
handler?.Invoke();

这样可以减少事件在检查和调用之间被其他线程修改带来的问题。

不过对于初学者来说,先掌握 ?.Invoke() 即可。

8. 异步事件处理要小心 async void

UI 事件中常见:

button.Click += async (sender, e) =>
{
    await SaveAsync();
};

这种情况下事件处理器本质上接近 async void,在 UI 事件中是可以接受的。

但在普通业务代码中,尽量不要随便使用 async void,因为异常处理和流程控制会更困难。

如果自己设计异步通知机制,可以考虑使用返回 Task 的委托:

public event Func<Task> SavedAsync;

不过事件本身对异步支持没有普通方法调用那么直接,所以初学阶段先了解即可。

9. 不要滥用事件

事件适合“某件事发生后通知外部”。

如果只是普通的一步调用,比如“保存用户”“计算价格”“发送短信”,直接用方法可能更清楚。

不推荐为了使用事件而使用事件。

可以这样判断:

  • 如果调用者明确知道要调用谁,用方法。
  • 如果发布者只负责通知,不关心谁处理,用事件。
  • 如果一件事发生后可能有多个对象响应,用事件。

10. 不要把事件当成返回结果的工具

事件通常是通知机制,不适合用来获取返回值。

例如不建议:

// 不推荐:试图通过事件向外部要一个计算结果
public event Func<int> NeedNumber;

如果你需要一个明确的返回值,通常更适合用方法、接口或回调。


二十一、课堂讲解建议

讲事件时,不建议一开始就讲复杂的 EventHandler<TEventArgs>

可以按这个顺序讲:

  1. 先讲生活例子:门铃响了,有人去开门。
  2. 再讲程序例子:按钮点击了,执行点击方法。
  3. 再用 Action 写一个最简单事件。
  4. 然后讲 += 是订阅,-= 是取消订阅。
  5. 再讲 ?.Invoke() 是触发事件。
  6. 最后讲 .NET 标准写法 EventHandler

学生最容易混淆的地方有三个:

  1. 事件不是方法,事件是“通知机制”。
  2. += 不是执行事件,而是订阅事件。
  3. ?.Invoke() 才是触发事件。

可以用下面这句话帮助理解:

订阅事件就像留下电话号码,事件发生时,发布者会按名单通知所有订阅者。


二十二、完整示例:商品库存不足事件

下面通过一个完整例子,把事件的定义、订阅、触发、传参都串起来。

using System;

class StockLowEventArgs : EventArgs
{
    public string ProductName { get; }
    public int CurrentStock { get; }

    public StockLowEventArgs(string productName, int currentStock)
    {
        ProductName = productName;
        CurrentStock = currentStock;
    }
}

class Product
{
    public string Name { get; }
    public int Stock { get; private set; }

    public event EventHandler<StockLowEventArgs> StockLow;

    public Product(string name, int stock)
    {
        Name = name;
        Stock = stock;
    }

    public void Sell(int count)
    {
        if (count <= 0)
        {
            Console.WriteLine("销售数量必须大于 0");
            return;
        }

        if (count > Stock)
        {
            Console.WriteLine("库存不足,无法销售");
            return;
        }

        Stock -= count;
        Console.WriteLine($"卖出 {count} 件 {Name},当前库存 {Stock}");

        if (Stock < 5)
        {
            OnStockLow(new StockLowEventArgs(Name, Stock));
        }
    }

    protected virtual void OnStockLow(StockLowEventArgs e)
    {
        StockLow?.Invoke(this, e);
    }
}

class Program
{
    static void Main()
    {
        Product product = new Product("键盘", 10);

        product.StockLow += Product_StockLow;

        product.Sell(3);
        product.Sell(3);
        product.Sell(2);
    }

    static void Product_StockLow(object sender, StockLowEventArgs e)
    {
        Console.WriteLine($"库存预警:{e.ProductName} 当前只剩 {e.CurrentStock} 件");
    }
}

输出:

卖出 3 件 键盘,当前库存 7
卖出 3 件 键盘,当前库存 4
库存预警:键盘 当前只剩 4 件
卖出 2 件 键盘,当前库存 2
库存预警:键盘 当前只剩 2 件

这个例子中:

  • Product 是事件发布者。
  • StockLow 是事件。
  • StockLowEventArgs 是事件数据。
  • Product_StockLow 是事件处理方法。
  • product.StockLow += Product_StockLow; 是订阅事件。
  • StockLow?.Invoke(this, e); 是触发事件。

二十三、练习题

练习 1:定义一个简单事件

定义一个 Light 类,当调用 TurnOn() 方法时,触发 TurnedOn 事件。

参考答案:

using System;

class Light
{
    public event Action TurnedOn;

    public void TurnOn()
    {
        Console.WriteLine("灯打开了");
        TurnedOn?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        Light light = new Light();

        light.TurnedOn += () =>
        {
            Console.WriteLine("收到通知:灯已经打开");
        };

        light.TurnOn();
    }
}

练习 2:定义一个带参数的事件

定义一个 ScoreManager 类,当添加分数后,触发 ScoreChanged 事件,并传出当前分数。

参考答案:

using System;

class ScoreManager
{
    public event Action<int> ScoreChanged;

    public int Score { get; private set; }

    public void AddScore(int value)
    {
        Score += value;
        ScoreChanged?.Invoke(Score);
    }
}

class Program
{
    static void Main()
    {
        ScoreManager manager = new ScoreManager();

        manager.ScoreChanged += score =>
        {
            Console.WriteLine("当前分数:" + score);
        };

        manager.AddScore(10);
        manager.AddScore(20);
    }
}

练习 3:使用标准 EventHandler

定义一个 DownloadService 类,当下载完成时触发 DownloadCompleted 事件。

参考答案:

using System;

class DownloadService
{
    public event EventHandler DownloadCompleted;

    public void Download()
    {
        Console.WriteLine("正在下载...");
        Console.WriteLine("下载完成");

        DownloadCompleted?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main()
    {
        DownloadService service = new DownloadService();

        service.DownloadCompleted += Service_DownloadCompleted;

        service.Download();
    }

    static void Service_DownloadCompleted(object sender, EventArgs e)
    {
        Console.WriteLine("收到通知:下载已经完成");
    }
}

练习 4:取消订阅事件

订阅一个事件后,再取消订阅,观察事件处理方法是否还会执行。

参考答案:

using System;

class Notifier
{
    public event Action Notified;

    public void Notify()
    {
        Notified?.Invoke();
    }
}

class Program
{
    static void Main()
    {
        Notifier notifier = new Notifier();

        notifier.Notified += HandleNotify;
        notifier.Notify();

        notifier.Notified -= HandleNotify;
        notifier.Notify();
    }

    static void HandleNotify()
    {
        Console.WriteLine("收到通知");
    }
}

输出只有一次:

收到通知

二十四、总结

事件是 C# 中非常重要的机制,常用于 UI、业务通知、状态变化、进度更新、定时器和插件扩展等场景。

可以记住下面几句话:

  1. 事件表示“某件事情发生了”。
  2. 事件基于委托,委托决定事件处理方法的参数和返回值。
  3. event 关键字可以保护委托,避免外部随意触发或覆盖。
  4. += 表示订阅事件。
  5. -= 表示取消订阅事件。
  6. ?.Invoke() 表示触发事件。
  7. 一个事件可以有多个订阅者。
  8. 简单事件可以用 Action,标准事件推荐用 EventHandler
  9. 需要传事件数据时,可以使用 EventHandler<TEventArgs>
  10. 长生命周期对象的事件要注意取消订阅,避免内存泄漏。

一句话概括:

C# 事件就是一种“对象之间的通知机制”:发布者只负责宣布事情发生了,订阅者自己决定听到通知后要做什么。

0
博主关闭了当前页面的评论