C#: 带有显式add/remove的事件与典型事件不同?

47
我已经声明了一个通用的事件处理程序。
public delegate void EventHandler();

我添加了扩展方法“RaiseEvent”:

public static void RaiseEvent(this EventHandler self)        {
   if (self != null) self.Invoke();
}

当我使用典型的语法定义事件时,

public event EventHandler TypicalEvent;

那么我可以毫无问题地调用扩展方法:
TypicalEvent.RaiseEvent();

但是当我使用显式的add/remove语法定义事件时

private EventHandler _explicitEvent;
public event EventHandler ExplicitEvent {
   add { _explicitEvent += value; } 
   remove { _explicitEvent -= value; }
}

如果事件是使用显式的add/remove语法定义的,则该扩展方法在该事件上不存在。
ExplicitEvent.RaiseEvent(); //RaiseEvent() does not exist on the event for some reason

当我将鼠标悬停在事件上以查看原因时,它会显示:

事件“ExplicitEvent”只能出现在 += 或 -= 的左侧

为什么使用典型语法定义的事件与使用显式添加/删除语法定义的事件不同,并且为什么扩展方法不能在后者上工作?

编辑:我发现可以通过直接使用私有事件处理程序来解决这个问题:

_explicitEvent.RaiseEvent();

但是我仍然不明白为什么我不能像使用典型语法定义的事件那样直接使用该事件。也许有人可以给我启示。


你也不能在类的外部使用扩展方法。 - IS4
4个回答

53

当您创建“字段式”事件时,就像这样:

public event EventHandler Foo;
编译器生成一个字段和一个事件。在声明事件的类的源代码中,每次引用Foo时,编译器理解你引用的是字段。然而,该字段是私有的,因此从其他类引用Foo时,它会引用事件(因此是添加/删除代码)。
如果您声明自己的显式添加/删除代码,则不会获得自动生成的字段。因此,您只有一个事件,并且无法直接在C#中引发事件-只能调用委托实例。事件不是委托实例,它只是一个添加/删除对。现在,您的代码包含以下内容:
public EventHandler TypicalEvent;
这还有点不同 - 它没有声明一个 事件,而是声明了一个公共的委托类型 EventHandler 的字段。 任何人 都可以调用它,因为该值只是委托实例。了解字段和事件之间的区别很重要。您不应该编写此类代码,就像我确定您通常没有其他类型的公共字段(如string 和int )一样。不幸的是,这是一个容易犯的拼写错误,并且相对难以阻止。您只能通过注意到编译器允许您将值分配给另一个类或使用它来发现它。
有关事件和委托的更多信息,请参见我的文章

谢谢Jon,我早就读过你的文章了,只是当时我注意到这个扩展方法的“奇怪行为”时,它对我来说并不是立即清晰明了的。 - user65199
事件(event)的作用类似于属性。 - Alex78191
@Alex78191:有点类似,只不过是用添加/删除代替获取/设置。 - Jon Skeet
我也发现blimac在这里的回答(https://dev59.com/Cmgu5IYBdhLWcg3wpYgj)很有帮助... - mike

39

因为你可以这样做(这只是一个非真实世界的示例,但它“有效”):

private EventHandler _explicitEvent_A;
private EventHandler _explicitEvent_B;
private bool flag;
public event EventHandler ExplicitEvent {
   add {
         if ( flag = !flag ) { _explicitEvent_A += value; /* or do anything else */ }
         else { _explicitEvent_B += value; /* or do anything else */ }
   } 
   remove {
         if ( flag = !flag ) { _explicitEvent_A -= value; /* or do anything else */ }
         else { _explicitEvent_B -= value; /* or do anything else */ }
   }
}

编译器如何知道它应该如何处理"ExplicitEvent.RaiseEvent();"?


它不知道。


"ExplicitEvent.RaiseEvent();"只是语法糖,只有当事件隐式实现时才能预测。


8
您是如何知道编译器是“他”?开个玩笑,感谢您的答案和样例,这真的非常清楚易懂。我喜欢这个网站,因为它能够提供像这样的快速回答。 - user65199
+1 这个超快的东西。这就是让我一直回来的原因。还有...巨大的知识量。 - Olivier Tremblay

11

这是因为你没有正确地看待它。 逻辑与属性(Property)相同。 一旦你设置了添加/删除操作,它就不再是一个实际的事件,而是一个包装器,用于公开实际的事件(事件只能从类内部触发,因此您始终可以在本地访问真正的事件)。

private EventHandler _explicitEvent;
public event EventHandler ExplicitEvent {
   add { _explicitEvent += value; } 
   remove { _explicitEvent -= value; }
}

private double seconds; 
public double Hours
{
    get { return seconds / 3600; }
    set { seconds = value * 3600; }
}
在这两种情况下,拥有get/set或add/remove属性的成员并没有真正包含任何数据。您需要一个“真正”的私有成员来包含实际数据。 这些属性只是允许您在向外界公开成员时编写额外的逻辑。
你需要这样做的一个好例子是为了防止不必要的计算(没有人监听事件)。
例如,假设事件是由计时器触发的,如果没有人注册该事件,我们不希望计时器工作。
private System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
private EventHandler _explicitEvent;
public event EventHandler ExplicitEvent 
{
   add 
   { 
       if (_explicitEvent == null) timer.Start();
       _explicitEvent += value; 
   } 
   remove 
   { 
      _explicitEvent -= value; 
      if (_explicitEvent == null) timer.Stop();
   }
}

你可能希望使用一个对象来锁定添加/删除(作为一个事后的想法)...


1

"plain" 声明 TypicalEvent 进行了一些编译器的技巧。它创建了一个事件元数据条目、添加和删除方法以及一个后备字段。当您的代码引用 TypicalEvent 时,编译器将其转换为对后备字段的引用;当外部代码引用 TypicalEvent(使用 += 和 -=)时,编译器将其转换为对添加或删除方法的引用。

"explicit" 声明绕过了这种编译器技巧。您正在拼写出添加和删除方法以及后备字段:事实上,正如 TcKs 指出的那样,可能甚至没有一个后备字段(这是使用显式形式的常见原因,例如 System.Windows.Forms.Control 中的事件)。因此,如果您想要后备字段、实际的委托对象,您必须直接引用后备字段:

_explicitEvent.RaiseEvent()

2
不完全正确 - 在类内部,+= 和 -= 仍将引用该字段。只有在从类外部引用它时才会发生这种情况。 - Jon Skeet
谢谢你的回答。你解释得非常清楚。我已经自己找到了解决方法,但还是感谢你提供了如何在显式事件上调用RaisEvent()的示例。 - user65199
Jon,感谢您的见解。时刻记住使用事件的复杂性总是很好的。 - user65199

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