在深入的 C#(迄今为止的一本优秀书籍)中,Skeet 解释了事件不是字段。我多次阅读本节,但我不明白为什么这种区别有任何区别。
我是那些混淆事件和委托实例的开发人员之一。在我看来,它们是一样的。两者都不只是一种间接形式吗?我们可以同时多播。一个事件被设置为一个字段作为速记......当然。但是,我们正在添加或删除处理程序。将它们堆叠起来以在事件触发时调用。我们不是对委托做同样的事情,将它们堆叠起来并调用调用吗?
其他答案基本上是正确的,但这是另一种看待它的方式:
我是那些混淆事件和委托实例的开发人员之一。在我看来,它们是一样的。
想起一句老话,见树不见林。我所做的区别是事件比委托实例的字段处于更高的“语义级别”。一个事件告诉该类型的消费者“嗨,我是一个喜欢在发生某事时告诉你的类型”。类型来源一个事件;这是其公共合同的一部分。
作为一个实现细节,该类如何选择跟踪谁有兴趣收听该事件,以及告诉订阅者事件正在发生的内容和时间,是该类的工作。它通常使用多播委托来执行此操作,但这是一个实现细节。这是一个非常常见的实现细节,将两者混淆是合理的,但我们确实有两个不同的东西:公共表面和私有实现细节。
类似地,属性描述了一个对象的语义:一个客户有一个名字,所以一个客户类有一个 Name 属性。您可能会说“他们的名字”是客户的属性,但您永远不会说“他们的名字”是客户的字段;这是特定类的实现细节,而不是关于业务语义的事实。属性通常实现为字段是类机制的私有细节。
属性也不是字段,尽管它们感觉像它们。它们实际上是一对具有特殊语法的方法(getter 和 setter)。
事件类似地是一对具有特殊语法的方法(订阅和取消订阅)。
在这两种情况下,您的类中通常都有一个私有的“支持字段”,它保存由 getter/setter/subscribe/unsubscribe 方法操作的值。并且编译器为您生成支持字段和访问器方法的属性和事件都有一个自动实现的语法。
目的也是相同的:属性提供对字段的受限访问,其中一些验证逻辑在存储新值之前运行。并且事件提供对委托字段的受限访问,其中消费者只能订阅或取消订阅,不能读取订阅者列表,也不能一次替换整个列表。
让我们考虑两种声明事件的方式。
要么使用显式add
/remove
方法声明事件,要么声明不使用此类方法的事件。
换句话说,您像这样声明事件:
public event EventHandlerType EventName
{
add
{
// some code here
}
remove
{
// some code here
}
}
或者你这样声明:
public event EventHandlerType EventName;
问题是,在某些方面它们是同一件事,而在其他方面,它们是完全不同的。
从外部代码的角度来看,即......发布事件的类之外的代码,它们是完全相同的东西。要订阅事件,请调用方法。要取消订阅,您需要调用不同的方法。
不同之处在于,在上面的第二个示例代码中,这些方法将由编译器为您提供,但是,它仍然是这样的。要订阅事件,您需要调用一个方法。
但是,在 C# 中执行此操作的语法是相同的,您可以执行以下任一操作:
objectInstance.EventName += ...;
或者:
objectInstance.EventName -= ...;
所以从“外部角度”来看,这两种方式并没有什么不同。
但是,在班级内部,却有所不同。
如果您尝试访问类中的EventName
标识符,您实际上指的field
是支持该属性的 ,但前提是您使用未显式声明add
/remove
方法的语法。
一个典型的模式是这样的:
public event EventHandlerType EventName;
protected void OnEventName()
{
var evt = EventName;
if (evt != null)
evt(this, EventArgs.Empty);
}
在这种情况下,当您引用 时EventName
,您实际上是在引用保存类型委托EventHandlerType
的字段。
但是,如果您已显式声明add
/remove
方法,则在类内部引用EventName
标识符就像在类外部一样,因为编译器不能保证它知道存储订阅的字段或任何其他机制.
事件是委托的访问器。就像属性是字段的访问器一样。使用完全相同的实用程序,它可以防止代码与委托对象混淆。就像属性有 get 和 set 访问器一样,事件也有 add 和 remove 访问器。
它的行为确实与属性有些不同,如果您不自己编写 add 和 remove 访问器,那么编译器会自动生成它们。包括一个存储委托对象的私有支持字段。类似于自动属性。
你不经常这样做,但这肯定不是不寻常的。.NET 框架通常这样做,例如 Winforms 控件的事件存储在EventHandlerList中,添加/删除访问器通过其 AddHandler() 和 RemoveHandler() 方法操作该列表。优点是所有事件(有很多)只需要类中的一个字段。
我可以添加到以前的答案中,委托可以在命名空间范围内(类外)声明,事件只能在类内声明。这是因为委托是一个类!
另一个区别是,对于事件,包含类是唯一可以触发它的类。您可以通过包含类订阅/取消订阅它,但不能触发它(与委托相反)。因此,也许您现在可以理解为什么约定是将其包装在protected virtual OnSomething(object sender, EventArgs e)
. 后代能够覆盖触发的实现。