C#事件用法详解(附带实例)
当使用委托时,一般会出现广播者(broadcaster)和订阅者(subscriber)两种角色。
广播者是包含委托字段的类型,它通过调用委托决定何时进行广播。
订阅者是方法的目标接收者。订阅者通过在广播者的委托上调用+=和-=来决定何时开始监听和何时监听结束。订阅者不知道也不会干涉其他订阅者。
事件就是正式定义这一模式的语言功能。事件是一种使用有限的委托功能实现广播者/订阅者模型的结构。使用事件的主要目的在于保证订阅者之间不互相影响。
声明事件最简单的方法是在委托成员的前面加上 event 关键字:
首先,编译器将事件的声明转换为如下形式:
而后,编译器在 Broadcaster 类里面找到除调用 += 和 -= 之外的 PriceChanged 引用点,并将它们重定向到内部的 priceChanged 委托字段。
最后,编译器对事件上的 += 和 -= 运算符操作相应地调用事件的 add 或 remove 访问器。有意思的是当应用于事件时,+= 和 -= 的行为是唯一的,而不像其他情况下是 + 和 - 运算符与赋值运算符的简写。
观察下面的例子,在 Stock 类中,每当 Stock 的 Price 发生变化时,就会触发 Price-Changed 事件:
标准事件模式的核心是 System.EventArgs 类,这是一个预定义的没有成员(但是有一个静态的 Empty 字段)的类。EventArgs 是为事件传递信息的基类。在 Stock 示例中,我们可以继承 EventArgs 以便在 PriceChanged 事件被触发时传递新的和旧的 Price 值:
EventArgs 子类就位后,下一步就是选择或者定义事件的委托了。这一步需要遵循三条规则:
.NET 定义了一个名为 System.EventHandler<> 的泛型委托来辅助实现标准事件模式:
在泛型出现之前(C# 2.0 之前),我们只能以如下方式书写自定义委托:
接下来就是定义选定委托类型的事件了。这里使用泛型的 EventHandler 委托:
最后,该模式需要编写一个 protected 的虚方法来触发事件。方法名称必须和事件名称一致,以 On 作为前缀,并接收唯一的 EventArgs 参数:
注意,为了在多线程下可靠地工作,在测试和调用委托之前,需要将它保存在一个临时变量中:
这样就提供了一个子类可以调用或重写事件的关键点(假设该类不是密封类)。
以下是完整的例子:
如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托 EventHandler。在本例中,我们重写 Stock 类,当 price 属性发生变化时,触发 PriceChanged 事件,事件除了传达已发生的消息之外没有必须包含的信息。为了避免创建不必要的 EventArgs 实例,我们使用了 EventArgs.Emtpy 属性:
我们也可以显式定义事件访问器来替代这个过程。以下是 PriceChanged 事件的手动实现:
显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。这主要有以下三种情形:
以下例子展示了第三种情形:
广播者是包含委托字段的类型,它通过调用委托决定何时进行广播。
订阅者是方法的目标接收者。订阅者通过在广播者的委托上调用+=和-=来决定何时开始监听和何时监听结束。订阅者不知道也不会干涉其他订阅者。
事件就是正式定义这一模式的语言功能。事件是一种使用有限的委托功能实现广播者/订阅者模型的结构。使用事件的主要目的在于保证订阅者之间不互相影响。
声明事件最简单的方法是在委托成员的前面加上 event 关键字:
// 委托定义 public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice); public class Broadcaster { // 事件声明 public event PriceChangedHandler PriceChanged; }Broadcaster 类型中的代码对 PriceChanged 有完全的访问权限,并可以将其视为委托。而 Broadcaster 类型之外的代码则仅可以在 PriceChanged 事件上执行+=和-=运算。
C#事件的工作机制
当声明如下委托时,在内部发生了三件事情:public class Broadcaster { public event PriceChangedHandler PriceChanged; }
首先,编译器将事件的声明转换为如下形式:
PriceChangedHandler priceChanged; // private delegate public event PriceChangedHandler PriceChanged { add { priceChanged += value; } remove { priceChanged -= value; } }add 和 remove 关键字明确了事件的访问器,就像属性的访问器那样。
而后,编译器在 Broadcaster 类里面找到除调用 += 和 -= 之外的 PriceChanged 引用点,并将它们重定向到内部的 priceChanged 委托字段。
最后,编译器对事件上的 += 和 -= 运算符操作相应地调用事件的 add 或 remove 访问器。有意思的是当应用于事件时,+= 和 -= 的行为是唯一的,而不像其他情况下是 + 和 - 运算符与赋值运算符的简写。
观察下面的例子,在 Stock 类中,每当 Stock 的 Price 发生变化时,就会触发 Price-Changed 事件:
public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice); public class Stock { string symbol; decimal price; public Stock(string symbol) => this.symbol = symbol; public event PriceChangedHandler PriceChanged; public decimal Price { get => price; set { if (price == value) return; // Exit if nothing has changed decimal oldPrice = price; price = value; if (PriceChanged != null) // 如果调用列表不为空 PriceChanged(oldPrice, price); } } }在上例中,如果将 event 关键字去掉,PriceChanged 就变成了普通的委托字段,虽然运行结果是不变的,但是 Stock 类就没有原来健壮了。因为这时订阅者可以通过以下方式相互影响:
- 通过重新指派 PriceChanged 替换其他订阅者(不用+=运算符);
- 清除所有订阅者(将 PriceChanged 设置为 null);
- 通过调用委托广播到其他订阅者。
C#标准事件模式
在 .NET 程序库中几乎所有和事件相关的定义中,都体现了一个标准模式。该模式保证了程序库和用户代码使用事件的一致性。标准事件模式的核心是 System.EventArgs 类,这是一个预定义的没有成员(但是有一个静态的 Empty 字段)的类。EventArgs 是为事件传递信息的基类。在 Stock 示例中,我们可以继承 EventArgs 以便在 PriceChanged 事件被触发时传递新的和旧的 Price 值:
public class PriceChangedEventArgs : EventArgs { public readonly decimal LastPrice; public readonly decimal NewPrice; public PriceChangedEventArgs(decimal lastPrice, decimal newPrice) { LastPrice = lastPrice; NewPrice = newPrice; } }考虑到复用性,EventArgs 子类应当根据它包含的信息来命名(而非根据使用它的事件命名)。它一般将数据以属性或只读字段的方式暴露给外界。
EventArgs 子类就位后,下一步就是选择或者定义事件的委托了。这一步需要遵循三条规则:
- 委托必须以 void 作为返回值;
- 委托必须接受两个参数,第一个参数是 object 类型,第二个参数是 EventArgs 的子类。第一个参数表明了事件的广播者,第二个参数则包含了需要传递的额外信息;
- 委托的名称必须以 EventHandler 结尾;
.NET 定义了一个名为 System.EventHandler<> 的泛型委托来辅助实现标准事件模式:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs e);
在泛型出现之前(C# 2.0 之前),我们只能以如下方式书写自定义委托:
public delegate void PriceChangedHandler(object sender, PriceChangedEventArgs e);出于历史原因,.NET 库中大部分事件使用的委托都是这样定义的。
接下来就是定义选定委托类型的事件了。这里使用泛型的 EventHandler 委托:
public class Stock { ... public event EventHandler<PriceChangedEventArgs> PriceChanged; }
最后,该模式需要编写一个 protected 的虚方法来触发事件。方法名称必须和事件名称一致,以 On 作为前缀,并接收唯一的 EventArgs 参数:
public class Stock { ... public event EventHandler<PriceChangedEventArgs> PriceChanged; protected virtual void OnPriceChanged(PriceChangedEventArgs e) { if (PriceChanged != null) PriceChanged(this, e); } }
注意,为了在多线程下可靠地工作,在测试和调用委托之前,需要将它保存在一个临时变量中:
var temp = PriceChanged; if (temp != null) temp(this, e);我们可以使用 null 条件运算符来避免声明临时变量:
PriceChanged?.Invoke(this, e);这种方式既线程安全又书写简明,是现阶段最好的事件触发方式。
这样就提供了一个子类可以调用或重写事件的关键点(假设该类不是密封类)。
以下是完整的例子:
using System; Stock stock = new Stock("THPW"); stock.Price = 27.10m; // 注册事件处理器 stock.PriceChanged += stock_PriceChanged; // 修改价格,触发事件 stock.Price = 31.59m; // 事件处理器实现 void stock_PriceChanged(object sender, PriceChangedEventArgs e) { if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1m) { Console.WriteLine("Alert, 10% stock price increase!"); } } // 事件参数类 public class PriceChangedEventArgs : EventArgs { public readonly decimal LastPrice; public readonly decimal NewPrice; public PriceChangedEventArgs(decimal lastPrice, decimal newPrice) { LastPrice = lastPrice; NewPrice = newPrice; } } // Stock 类 public class Stock { private readonly string symbol; private decimal price; public Stock(string symbol) => this.symbol = symbol; public event EventHandler<PriceChangedEventArgs> PriceChanged; protected virtual void OnPriceChanged(PriceChangedEventArgs e) { PriceChanged?.Invoke(this, e); } public decimal Price { get => price; set { if (price == value) return; decimal oldPrice = price; price = value; OnPriceChanged(new PriceChangedEventArgs(oldPrice, price)); } } }
如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托 EventHandler。在本例中,我们重写 Stock 类,当 price 属性发生变化时,触发 PriceChanged 事件,事件除了传达已发生的消息之外没有必须包含的信息。为了避免创建不必要的 EventArgs 实例,我们使用了 EventArgs.Emtpy 属性:
public class Stock { string symbol; decimal price; public Stock(string symbol) => this.symbol = symbol; public event EventHandler PriceChanged; protected virtual void OnPriceChanged(EventArgs e) { PriceChanged?.Invoke(this, e); } public decimal Price { get => price; set { if (price == value) return; price = value; OnPriceChanged(EventArgs.Empty); } } }
C#事件访问器
事件访问器是对事件的 += 和 -= 功能的实现。默认情况下,访问器由编译器隐式实现。考虑如下声明:private EventHandler priceChanged;编译器将其转化为:
- 一个私有的委托字段。
- 一对公有的事件访问器函数(add_PriceChanged 和 remove_PriceChanged),它们将 += 和 -= 操作转向了私有的委托字段。
我们也可以显式定义事件访问器来替代这个过程。以下是 PriceChanged 事件的手动实现:
public EventHandler PriceChanged { add { priceChanged += value; } remove { priceChanged -= value; } }本例在功能上和 C# 的默认访问器实现是等价的(C# 使用无锁的比较-交换算法来保证委托更新时的线程安全性)。若定义了自定义事件访问器,C# 就不会生成默认的字段和访问器逻辑了。
显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。这主要有以下三种情形:
- 当前事件访问器仅仅是广播事件的类的中继器;
- 当类定义了大量的事件,而大部分事件有很少的订阅者时,例如 Windows 控件。在这种情况下,最好在一个字典中存储订阅者的委托实例,因为字典会比大量的空委托字段引用更少的存储开销;
- 当显式实现声明事件的接口时。
以下例子展示了第三种情形:
public interface IFoo { event EventHandler Ev; } class Foo : IFoo { private EventHandler ev; event EventHandler IFoo.Ev { add { ev += value; } remove { ev -= value; } } }
注意,事件的 add 和 remove 部分会分别编译为 add_XXX 和 remove_XXX 方法。
C#事件修饰符
和方法类似,事件可以是虚的(virtual)、重写的(overridden)、抽象的(abstract)或者密封的(sealed),当然也可以是静态的(static):public class Foo { public static event EventHandler<EventArgs> StaticEvent; public virtual event EventHandler<EventArgs> VirtualEvent; }