事件不是字段 - 我不理解

52

C#深入浅出(到目前为止非常好的一本书)中,Skeet解释了事件不是字段。我读了这一部分很多次,但我不明白为什么区别会有任何影响。

我是那些混淆事件和委托实例的开发人员之一。在我看来,它们是相同的。难道它们不都是一种间接形式吗?我们可以广播两者。事件被设置为一个字段作为简写...当然。但是,我们正在添加或删除处理程序。堆叠它们以在事件触发时调用。我们不是也用委托做同样的事情,将它们堆叠起来并调用invoke吗?


2
哇,你吸引了很多“重量”来回答你的问题!;) - Andrew Barber
2
我非常欣赏每位高技能专家的观点。 - P.Brian.Mackey
1
请注意,在查阅勘误表后,我发现Jon在这个主题上有更详细的阐述(书中给出的链接是错误的):http://csharpindepth.com/Articles/Chapter2/Events.aspx - P.Brian.Mackey
5个回答

48
其他答案基本上是正确的,但这里有另一种看待它的方式:
我是那些混淆事件和委托实例的开发人员之一。在我的脑海中,它们是相同的。
关于看不到森林而只看到树木的古老说法涌上了心头。我区分它们的原因在于事件比委托实例更高层次的“语义级别”。事件向类型的消费者传递信息:“你好,我是一个类型,喜欢在某些情况下告诉你发生了什么事情。” 类型提供了一个事件;这是其公共契约的一部分。
至于这个类如何选择跟踪谁对该事件感兴趣,以及何时和如何告诉订阅者事件已经发生,这是该类的业务。通常使用多路广播委托来做到这一点,但这是实现细节。由于它是一个常见的实现细节,所以很容易混淆两者,但实际上我们确实有两个不同的东西:公共接口和私有实现细节。
同样,属性描述对象的语义:客户有姓名,所以Customer类具有Name属性。您可能会说“他们的名字”是客户的属性,但您永远不会说“他们的名字”是客户的“字段”。那只是特定类的实现细节,而不是业务语义的事实。属性通常实现为字段是该类机制的私有细节。

1
  1. 委托(delegate)无法做到但事件(events)可以的是什么?反之呢?
  2. 事件应该做而委托不应该做的是什么?这可能会使差异更加清晰。
- Dhananjay
@Dhananjay 事件必须以某种方式使用委托,因为它们是由需要委托作为参数的一对方法(添加和删除)实现的。但是,它们不需要直接映射到委托字段。相反,它们可以以其他方式存储委托,或者创建包装器委托并将其存储在其他方式中等等。类的用户不应该能够区分使用委托后备字段的自动事件和自定义编码事件之间的区别,因此合理的实现选项有些有限。 - Kevin Cathcart
5
如果你可以从头开始重新设计C#和.NET,你会用IObservable<T>替换事件/委托吗? - Judah Gabriel Himango
你可以使用委托代替事件,但是事件提供更多的安全性,例如保护委托不被定义类外部设置为空。我认为这是最大的区别。 - Tarik

46

属性和字段不同,尽管它们感觉相似。实际上,它们是一对带有特殊语法的方法(getter和setter)。

事件类似于一对带有特殊语法的方法(subscribe和unsubscribe)。

在这两种情况下,通常你的类中会有一个私有的“后备字段”,它保存着由getter/setter/subscribe/unsubscribe方法操作的值。并且,对于属性和事件都有自动实现的语法,编译器会为你生成后备字段和访问器方法。

其目的也是相同的:属性提供对字段的受限访问,在存储新值之前运行一些验证逻辑。事件提供对委托字段的受限访问,其中消费者只能订阅或取消订阅,不能读取订阅者列表,也不能一次替换整个列表。


1
帮助理解的经典例子是在使用诸如winforms之类的东西时 - 有一个EventHandlerList,它是大多数事件的“后台支持者”(并且可以通过.Events提供给子类使用)。只有1个字段 - 很多很多事件。 - Marc Gravell
我相信这是通过重写事件上的addremove方法来完成的 - 例如http://msdn.microsoft.com/en-us/library/ak9w5846.aspx的第二个示例。 - dsolimano
4
“overriding”可能是在这里使用的一个令人困惑的术语,因为它通常与多态有关;提供定制的add/remove方法,当然可以 - 有点像自动实现属性和常规属性之间的区别(类似于“自动实现属性”的事件称为“类似字段的事件”)。 - Marc Gravell
这是一个很棒的答案 +1! - nawfal

26

让我们考虑两种声明事件的方式。

一种是使用显式的add/remove方法声明事件,另一种是不使用这些方法声明事件。

换句话说,你可以像这样声明事件:

public event EventHandlerType EventName
{
    add
    {
        // some code here
    }
    remove
    {
        // some code here
    }
}

或者你可以这样声明:

public event EventHandlerType EventName;
事实上,它们在某些方面是相同的,但在其他方面则完全不同。
从外部代码的角度来看...也就是类发布事件之外的代码,它们是完全相同的。要订阅事件,您调用一个方法。要取消订阅,则需要调用另一种方法。
不同之处在于,在上述第二个示例代码中,这些方法将由编译器为您提供,但仍然是这样。要订阅事件,您需要调用一个方法。
然而,在C#中执行此操作的语法是相同的,您可以执行以下任何一种方式:
objectInstance.EventName += ...;

或者:

objectInstance.EventName -= ...;

从“外部的角度”看,这两种方法没有任何区别。

然而,在类内部有所区别。

如果您尝试在类内部访问EventName标识符,您实际上是在引用支持属性的字段,但仅当您使用不显式声明add/remove方法的语法时才是如此

一种典型的模式如下:

public event EventHandlerType EventName;

protected void OnEventName()
{
    var evt = EventName;
    if (evt != null)
        evt(this, EventArgs.Empty);
}

在这种情况下,当您引用 EventName 时,实际上是在引用一个持有类型为 EventHandlerType委托 的字段。

然而,如果您显式声明了 add/remove 方法,并且在类内部引用 EventName 标识符,那么就像在类外部一样,编译器无法保证它知道您存储订阅的字段或任何其他机制。


10
一个事件是委托的访问器。就像属性是字段的访问器一样,它具有完全相同的实用性,可以防止代码干扰委托对象。与属性具有get和set访问器一样,事件具有add和remove访问器。如果您没有自己编写add和remove访问器,则它会与属性有些不同,编译器会自动生成它们,包括存储委托对象的私有后备字段,类似于自动属性。虽然这并不常见,但也不算罕见。.NET框架通常这样做,例如Winforms控件的事件存储在EventHandlerList中,add/remove访问器通过其AddHandler()和RemoveHandler()方法操作该列表。优点是,所有事件(有很多)仅需要类中的单个字段。

事件是一个访问器,用于访问其类型为委托类型的字段。 - Ben Voigt
阅读答案以查看情况并非如此。最后一段。 - Hans Passant
3
这句话的意思是与你回答中的第二句话一样正确。 - Ben Voigt
嗯,不是的,这是不同的事实。让编译器生成带有后备字段的默认实现的重点在于您不需要或不想要后备字段。属性的完全相同的推理。这是Skeet的观点,事件不是字段 - Hans Passant
4
Skeet的观点是,事件是提供对值访问的方法,而不是值本身。就像属性一样,但具有不同的操作。该值通常存储在字段中,但并非总是如此。与属性类似,字段可以是事件的背景之一。如果说一个事件可能没有后备字段,那么这种严谨的程度也排除了一般性的声明,即属性是字段的访问器。 - Ben Voigt
1
哎,你糟糕透了。没有字节。 - Hans Passant

0

我可以补充前面的回答,委托可以在命名空间范围内(类外)声明,而事件只能在类内声明。 这是因为委托是一个类!

另一个区别是,对于事件,包含它的类是唯一可以触发它的类。你可以通过包含它的类来订阅/取消订阅它,但不能触发它(与委托相比)。因此,现在也许你可以理解为什么惯例是将其包装在protected virtual OnSomething(object sender, EventArgs e)中了。这是为了让后代能够覆盖触发的实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接