二、事件详解
事件是基于委托实现的高级机制,常用于发布-订阅模式。它是一种编程方式,让一个对象(发布者)可以通知其他对象(订阅者)某些事情发生。事件最常见的应用场景是
GUI 编程和消息广播。下面,我们从基础概念开始,结合代码示例,详细说明事件的概念、使用和工作机制。
1. 什么是事件?
事件是对委托的封装,它将方法的调用权交给事件发布者,只有事件发布者才能触发事件,而订阅者可以注册要调用的方法。
事件的核心特点
1.只能触发者调用:外部对象不能直接调用事件,只能注册或取消订阅。
2.基于委托:事件的底层是委托。
3.多播机制:一个事件可以通知多个订阅者。
2. 声明和使用事件
代码示例:按钮点击事件
假设我们模拟一个简单的按钮点击事件:
using System;
class Program
{
// 定义一个委托作为事件的签名
public delegate void ClickHandler(string message);
// 声明一个事件
public event ClickHandler ButtonClicked;
static void Main(string[] args)
{
var program = new Program();
// 订阅事件
program.ButtonClicked += msg => Console.WriteLine($"Subscriber 1 received: {msg}");
program.ButtonClicked += msg => Console.WriteLine($"Subscriber 2 received: {msg}");
// 模拟按钮点击,触发事件
program.OnButtonClick("Button was clicked!");
}
// 触发事件的方法
public void OnButtonClick(string message)
{
ButtonClicked?.Invoke(message); // 触发事件,调用订阅者的方法
}
}
执行流程:
1.ButtonClicked 是一个事件,它可以被订阅(+=)或取消订阅(-=)。
2.当调用 OnButtonClick 方法时,会触发 ButtonClicked 事件。
3.所有订阅了 ButtonClicked 的方法都会被依次调用。
输出:
Subscriber 1 received: Button was clicked!
Subscriber 2 received: Button was clicked!
事件执行流程详解
1. 委托定义
public delegate void ClickHandler(string message);
- 定义了一个委托类型 ClickHandler。
- 它表示一个方法签名:接收一个 string 参数,无返回值。
- 用于指定事件的处理方法的签名,所有订阅事件的方法必须符合这个签名。
2. 事件声明
public event ClickHandler ButtonClicked;
- event 关键字修饰了 ButtonClicked,表明它是一个事件。
- ButtonClicked 使用 ClickHandler 委托类型,表示它只能绑定符合该委托签名的方法。
- 事件的本质:一个多播委托,但只能通过类内部触发,而不能在外部直接调用。
- 订阅者(外部):只能使用 += 或 -= 添加或移除方法。
- 发布者(类内部):只能使用 Invoke 方法触发事件。
3. 订阅事件
program.ButtonClicked += msg => Console.WriteLine($"Subscriber 1 received: {msg}");
program.ButtonClicked += msg => Console.WriteLine($"Subscriber 2 received: {msg}");
- 使用 Lambda 表达式 订阅事件:
- 第一个订阅者输出 Subscriber 1 received: {msg}。
- 第二个订阅者输出 Subscriber 2 received: {msg}。
- += 用于将方法添加到 ButtonClicked 的订阅列表中。
- 订阅者的意义:
- 这些方法在事件触发时会依次被调用。
4. 触发事件
public void OnButtonClick(string message)
{
ButtonClicked?.Invoke(message); // 触发事件,调用所有订阅者
}
- 触发逻辑:
1.ButtonClicked 是一个事件,多播委托的封装。
2.ButtonClicked?.Invoke(message) 检查事件是否有订阅者(避免空引用异常)。
3.如果有订阅者,依次调用每个订阅的方法,参数 message 被传递给订阅方法。 - 事件触发者的意义:
- 在类的内部,事件发布者负责调用事件,通知所有订阅者。
- 在这个例子中,事件通过 OnButtonClick 模拟按钮点击来触发。
5. 模拟事件触发
program.OnButtonClick("Button was clicked!");
- 调用 OnButtonClick 方法触发事件。
- 所有订阅了 ButtonClicked 的方法会依次被调用。
- 输出结果:
Subscriber 1 received: Button was clicked!
Subscriber 2 received: Button was clicked!
执行流程总结
- 定义了一个事件 ButtonClicked,基于 ClickHandler 委托。
- 外部(订阅者)通过 += 订阅了事件。
- 当 OnButtonClick 方法被调用时,事件 ButtonClicked 被触发,通知所有订阅者。
- 所有订阅方法被依次调用,处理事件的消息。
事件的作用和特点
1.松耦合:
- 事件机制实现了发布者和订阅者之间的松耦合,发布者无需了解订阅者的具体实现。
2.动态扩展:
- 可以随时通过 += 添加新的订阅者,或通过 -= 移除订阅者。
3.线程安全:
- 使用事件的 Invoke 调用方式,线程安全(如果有空值检查 ?.)。
4.实际应用场景:
- 按钮点击、鼠标移动、数据加载完成等 GUI 应用程序中的典型事件处理场景。
3. 事件的发布-订阅模型
发布者和订阅者
- 发布者:声明事件,并在特定条件下触发事件(如按钮点击)。
- 订阅者:注册对事件的响应逻辑。
示例:多个订阅者响应一个事件
using System;
class Program
{
public delegate void Notify(string message);
public event Notify OnEventHappened;
static void Main(string[] args)
{
var program = new Program();
// 订阅者1
program.OnEventHappened += msg => Console.WriteLine($"Logger: {msg}");
// 订阅者2
program.OnEventHappened += msg => Console.WriteLine($"Notifier: {msg}");
// 发布者触发事件
program.TriggerEvent("Something happened!");
}
public void TriggerEvent(string message)
{
Console.WriteLine("Event is about to trigger...");
OnEventHappened?.Invoke(message); // 触发事件
}
}
输出:
Event is about to trigger...
Logger: Something happened!
Notifier: Something happened!
4. 事件的语法简化
从 C# 2.0 开始,可以使用内置委托 Action 或 Func,简化事件的声明和使用。
简化代码:
using System;
class Program
{
// 使用内置委托声明事件
public event Action<string> OnMessage;
static void Main(string[] args)
{
var program = new Program();
// 订阅事件
program.OnMessage += msg => Console.WriteLine($"Received: {msg}");
// 触发事件
program.TriggerEvent("Hello, simplified events!");
}
public void TriggerEvent(string message)
{
OnMessage?.Invoke(message);
}
}
5. 常见应用场景
1. GUI 编程
- 例如,按钮点击事件(Click)、文本改变事件(TextChanged)。
示例:WPF 中的事件
<Button Content="Click Me" Click="Button_Click" />
对应的 C# 代码:
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button was clicked!");
}
2. 数据变化通知
- 例如 INotifyPropertyChanged 接口,用于通知 UI 数据的变化(MVVM 模式中常见)。
示例:数据绑定和事件
using System;
using System.ComponentModel;
class ViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
6. 事件的优缺点
优点
1.解耦:发布者和订阅者之间没有强依赖关系。
2.灵活:可以动态添加或移除订阅者。
3.可扩展性强:多个订阅者可同时监听一个事件。
缺点
1.调试复杂:特别是多播事件,订阅者过多时不容易追踪调用链。
2.内存泄漏风险:事件订阅者没有正确移除订阅,可能导致内存泄漏。
总结
事件是委托的高级封装,核心在于解耦和发布-订阅模式。通过事件机制,程序的逻辑更加清晰、模块化,可以轻松应对动态变化的需求