事件
-
使用委托时,一般会出现两种角色:广播者(Broadcaster)和订阅者(Subscriber)。
- 广播者:包含委托字段的类型,它通过调用委托决定何时进行广播。
- 订阅者:目标方法的接受者。订阅者通过在广播者的委托上调用
+=
和-=
来决定何时开始监听而何时结束监听。订阅者不知道也不会干涉其他订阅者。
-
事件(Events)
- 事件就是将广播者/订阅者这一模式正式化的语言功能。
- 事件是一种使用有限的委托功能实现广播者/订阅者模型的结构,只暴露了所需的委托特性的部分子集。
- 使用事件的主要目的在于防止订阅者之间相互干扰。
-
声明事件的最简单方法是在委托成员的前面加上
event
关键字:// Delegate definition public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice); public class Broadcaster { // Event declaration public event PriceChangedHandler PriceChanged; }
Broadcaster
类型中的代码对PriceChanged
有完全的访问权限,并可以将其视为委托;Broadcaster
类型之外的代码仅可以在PriceChanged
事件上执行+=
和-=
操作(不能将PriceChanged
作为委托对其执行+
和-
以及赋值为null
等操作)。
事件的工作机制 🌴
当声明如下的事件时,在内部发生了三件事:
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
访问器(应用于事件时,+=
和-=
是唯一的,而不像其他情况下是+
和-
运算符和赋值运算符的简写)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fXnkY44z-1602725273694)(_v_images/20200818152623023_17736.png =108x)]
在 Stock
类中,每当 Stock
的 Price
发生变化,PriceChanged
事件就会触发。
public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
public class Stock
{
decimal price;
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get { return price; }
set
{
if (price == value) return; // Exit if nothing has changed
decimal oldPrice = price;
price = value;
if (PriceChanged != null) // If invocation list not
PriceChanged(oldPrice, price); // empty, fire event.
}
}
}
class Test
{
static void Main(string[] args)
{
Stock st = new Stock();
st.PriceChanged += PrintChangedPrice;
st.Price = 3; // oldPrice:0 newPrice:3
}
static void PrintChangedPrice(decimal oldPrice, decimal newPrice)
=> Console.WriteLine($"oldPrice:{oldPrice} newPrice:{newPrice}");
}
本例中,如果将 event
关键字去掉, PriceChanged
就变成了普通的委托字段,虽然运行结果是不变的,但 Stock
类就没有原来健壮了。因为这个时候订阅者就可以通过以下方式相互影响:
- 通过重新指派
PriceChanged
替换其他的订阅者(不用+=
运算符)。 - 清除所有的订阅者(将
PriceChanged
设置为null
)。 - 通过调用其委托广播到其他的订阅者。
Stock st = new Stock();
st.PriceChanged += PrintChangedPrice;
st.PriceChanged = null; // Compile-time error: 事件“Stock.PriceChanged”只能出现在 += 或 -= 的左边(从类型“Stock”中使用时除外)
1 标准事件模式
.Net Framework 为事件编程定义了一个标准模式,它的目的就是保持框架和用户代码的一致性(为了让你秃头 💀)。
-
标准事件模式的核心是
System.EventArgs
类。-
预定义的框架类,除了静态的
Empty
属性外没有其他成员。 -
EventArgs
是为事件传递信息的基类。
在Stock
类中,可以继承EventArgs
以便在PriceChanged
事件触发事传递新的和旧的Price
值。public class PriceChangedEventArgs : System.EventArgs { public readonly decimal lastPrice; public readonly decimal newPrice; public PriceChangedEventArgs(decimal lastPrice, decimal newPrice) { this.lastPrice = lastPrice; this.newPrice = newPrice; } }
-
考虑到复用性,
EventArgs
子类应当根据它包含的信息来命名(而非根据使用它的事件命名)。 -
EventArgs
子类一般将数据以属性或只读字段的方式暴露给外界。
-
-
EventArgs
子类就位后,下一步就是选择或者定义事件的委托了,这一步需要遵循三个原则:- 委托必须以
void
作为返回值。 - 委托必须接受两个参数,第一个参数是
object
类型,第二个参数则是EventArgs
的子类。第一个参数表明了事件的广播者,第二个参数则包含了需要传递的额外信息。 - 委托的名称必须以 EventHandler 结尾。
框架定义了一个名为
System.EventHandler<>
的泛型委托,该委托满足以上三个条件:
public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;
- 委托必须以
-
接下来就是定义选定的委托类型的事件了。使用泛型的
EventHandler
委托:public class Stock { ... public event EventHandler<PriceChangedEventArgs> PriceChanged; }
-
最后,该模式需要编写一个
protected virtual
方法来触发事件。方法名必须和事件名称一致,以 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);
我们可以使用 csharp 6 的
null
条件运算符来避免临时变量的声明:PriceChanged?.Invoke(this, e);
这种方式既线程安全又书写简明,是现阶段最好的事件触发方式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i7ds23VT-1602725273703)(_v_images/20200818152636368_25502.png =108x)]
/// <summary>
/// PriceChangedEventArgs 继承 EventArgs 类,用于传递事件信息
/// </summary>
public class PriceChangedEventArgs : EventArgs
{
public readonly decimal lastPrice, newPrice;
public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
{
this.lastPrice = lastPrice;
this.newPrice = newPrice;
}
}
public class Stock
{
/// <summary>
/// 构造函数
/// </summary>
string symbol;
public Stock(string symbol)
{
this.symbol = symbol;
}
/// <summary>
/// 定义事件
/// </summary>
public event EventHandler<PriceChangedEventArgs> PriceChanged;
/// <summary>
/// 定义方法触发事件
/// </summary>
/// <param name="e">事件传递的信息</param>
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e); // 事件调用目标方法
}
/// <summary>
/// 属性
/// </summary>
decimal price;
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged( new PriceChangedEventArgs(oldPrice, price) ); // 触发事件
}
}
}
class Test
{
static void Main()
{
Stock stock = new Stock("THPW");
stock.Price = 27.10M;
// Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;
}
/// <summary>
/// 定义事件目标方法,触发事件后的处理方法
/// </summary>
/// <param name="sender"></param>
/// <param name="e">事件传递的信息</param>
static void stock_PriceChanged(object sender, PriceChangedEventArgs e)
{
if ( (e.NewPrice - e.lastPrice) / e.lastPrice > 0.1M )
Console.WriteLine("Alert, 10% stock price increase!");
}
}
如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托 EventHandler
。本例中,我们重写 Stock
类,当 Price
属性发生变化时,触发 PriceChanged
事件,事件出了传达已发生的消息之外没有必须包含的信息。为了避免创建 EventArgs
实例,我们使用了 EventArgs.Empty
属性:
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 { return price; }
set
{
if (price == value) return;
price = value;
OnPriceChanged(EventArgs.Empty);
}
}
}
2 事件访问器
事件访问器是对事件的 +=
和 -=
功能的实现。默认情况下,访问器由编译器隐式实现。考虑如下的声明:
public event EventHandler PriceChanged;
编译器将其转化为:
- 一个私有委托字段。
- 一对公有的事件访问器函数(
add_PriceChanged
和remove_PriceChanged
),它们将+=
和-=
操作转向了私有字段。
-
我们可以显式定义事件访问器来替代上述过程。以下是
PriceChanged
事件的手动实现:private EventHandler priceChanged; // Declare a private delegate public event EventHandler PriceChanged { add { priceChanged += value; } remove { priceChanged -= value; } }
本例从功能上和 csharp 的默认访问器实现是等价的(但是 csharp 还使用了无锁的比较并交换算法,保证了在更新委托时的线程安全性)。有了自定义事件访问器,csharp 就不会生成默认的字段和访问器逻辑。
-
显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。主要用于:
-
当前事件访问器仅仅是广播事件的类的中继器。
-
当类定义了大量的事件,而大部分事件有很少的订阅者。
-
当显式实现声明事件的接口时,如下:
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
方法。 -
3 事件的修饰符
和方法类似,事件可以是虚的(virtual
)、可以重写(overridden
)、可以是抽象的(abstract
)或者密封的(sealed
),也可以是静态的(static
)。
public class Foo
{
public static event EventHandler<EventArgs> StaticEvent;
public virtual event EventHandler<EventArgs> VirtualEvent;
}