21

.NET 中的事件有一个标准模式——它们使用一种delegate类型,该类型采用一个名为 sender 的普通对象,然后是第二个参数中的实际“有效负载”,该参数应从EventArgs.

派生第二个参数的理由EventArgs似乎很清楚(请参阅.NET Framework Standard Library Annotated Reference)。随着软件的发展,它旨在确保事件接收器和源之间的二进制兼容性。对于每个事件,即使它只有一个参数,我们也会派生一个自定义事件参数类,该类具有包含该参数的单个属性,因此我们保留了在未来版本中向有效负载添加更多属性而不破坏现有客户端代码的能力. 在独立开发组件的生态系统中非常重要。

但我发现零参数也是如此。这意味着,如果我的第一个版本中有一个没有参数的事件,我会写:

public event EventHandler Click;

...那我做错了。如果我将来将委托类型更改为新类作为其有效负载:

public class ClickEventArgs : EventArgs { ...

...我将打破与我的客户的二进制兼容性。客户端最终绑定到一个内部方法的特定重载add_ClickEventHandler如果我更改委托类型,那么他们就找不到该重载,所以有一个MissingMethodException.

好的,那么如果我使用方便的通用版本呢?

public EventHandler<EventArgs> Click;

不,仍然是错误的,因为 anEventHandler<ClickEventArgs>不是EventHandler<EventArgs>.

因此,要获得 的好处EventArgs,您必须从中派生,而不是按原样直接使用它。如果你不这样做,你也可能不使用它(在我看来)。

然后是第一个论点,sender。在我看来,这似乎是邪恶耦合的秘诀。事件触发本质上是一个函数调用。一般来说,该函数是否应该有能力从堆栈中挖掘并找出调用者是谁,并相应地调整其行为?我们是否应该强制要求接口看起来像这样?

public interface IFoo
{
    void Bar(object caller, int actualArg1, ...);
}

毕竟,实现者Bar可能想知道是谁caller,所以他们可以查询更多信息!我希望你现在正在呕吐。为什么事件应该有所不同?

因此,即使我准备为EventArgs我声明的每个事件创建一个独立的派生类,只是为了让它值得我使用EventArgs,我肯定更愿意放弃 object sender 参数。

Visual Studio 的自动完成功能似乎并不关心您为事件使用的委托 - 您可以键入+= [hit Space, Return]并为您编写一个与它碰巧匹配的处理程序方法。

那么偏离标准模式会失去什么价值呢?

作为一个额外的问题,C#/CLR 4.0 会做些什么来改变这一点,也许是通过代表的逆变?我试图对此进行调查,但遇到了另一个问题。我最初将问题的这一方面包含在另一个问题中,但它在那里引起了混乱。把它分成三个问题似乎有点多……

更新:

事实证明,我想知道逆变对整个问题的影响是正确的!

正如其他地方所指出的,新的编译器规则在类型系统中留下了一个在运行时爆炸的漏洞。EventHandler<T>通过对 的不同定义,该孔已被有效地堵塞Action<T>

所以对于事件,为了避免你不应该使用的那种类型的洞Action<T>。这并不意味着您必须使用EventHandler<TEventArgs>; 这只是意味着如果您使用通用委托类型,请不要选择启用逆变的类型。

4

2 回答 2

7

什么都没有,你什么都不会失去。自从 .NET 3.5 出现以来,我一直在使用Action<>它,它更加自然且易于编程。

我什至不再处理EventHandler生成的事件处理程序的类型,只需编写您想要的方法签名并将其与 lambda 连接起来:

btnCompleteOrder.OnClick += (o,e) => _presenter.CompleteOrder();
于 2009-07-13T16:52:30.650 回答
2

我也不喜欢事件处理程序模式。在我看来,Sender 对象并没有那么有用。如果事件表明某个对象发生了某些事情(例如更改通知),那么在 EventArgs 中包含信息会更有帮助。我可以看到 Sender 的唯一用途是取消订阅一个事件,但并不总是清楚应该取消订阅什么事件。

顺便说一句,如果我有我的 druthers,Event 就不会是 AddHandler 方法和 RemoveHandler 方法。它只是一个 AddHandler 方法,它将返回一个 MethodInvoker 可用于取消订阅。而不是 Sender 参数,我将第一个参数是取消订阅所需的 MethodInvoker 的副本(以防对象发现自己接收到取消订阅调用程序已丢失的事件)。标准 MulticastDelegate 不适合调度事件(因为每个订阅者都应该收到不同的取消订阅委托),但取消订阅事件不需要通过调用列表进行线性搜索。

于 2010-12-18T03:39:56.480 回答