如果放弃 .NET 中的标准 EventHandler 模式,我会失去什么?
.NET 中的事件有一个标准模式 - 它们使用 delegate
类型,该类型采用名为 sender 的普通对象,然后是第二个参数中的实际“有效负载”,该参数应从 EventArgs 派生
。
从 EventArgs
派生的第二个参数的基本原理似乎非常清楚(请参阅 .NET Framework 标准库带注释的参考)。 其目的是随着软件的发展确保事件接收器和源之间的二进制兼容性。 对于每个事件,即使它只有一个参数,我们也会派生一个自定义事件参数类,该类具有包含该参数的单个属性,这样我们就可以保留在未来版本中向有效负载添加更多属性的能力,而不会破坏现有的客户端代码。 在独立开发组件的生态系统中非常重要。
但我发现零参数也是如此。 这意味着,如果我的第一个版本中有一个没有参数的事件,并且我写:
public event EventHandler Click;
...那么我就做错了。 如果我将来将委托类型更改为新类作为其有效负载:
public class ClickEventArgs : EventArgs { ...
...我将破坏与客户端的二进制兼容性。 客户端最终绑定到采用 EventHandler
的内部方法 add_Click
的特定重载,如果我更改委托类型,那么他们将无法找到该重载,因此有一个MissingMethodException
。
好吧,那如果我使用方便的通用版本呢?
public EventHandler<EventArgs> Click;
不,仍然错误,因为 EventHandler
不是 EventHandler
。
因此,要获得 EventArgs
的优势,您必须从它派生,而不是直接按原样使用它。 如果你不这样做,你可能也不会使用它(在我看来)。
然后是第一个参数,sender
。 在我看来,这就像是邪恶耦合的秘诀。 事件触发本质上是函数调用。 一般来说,函数是否应该有能力深入堆栈并找出调用者是谁,并相应地调整其行为? 我们是否应该强制要求接口应该像这样?
public interface IFoo
{
void Bar(object caller, int actualArg1, ...);
}
毕竟,Bar
的实现者可能想知道调用者
是谁,以便他们可以查询其他信息! 我希望你现在已经吐了。 为什么事件会有所不同?
因此,即使我准备好为我声明的每个事件创建一个独立的 EventArgs 派生类,只是为了让它值得我使用 EventArgs,我绝对更愿意放弃对象发送者参数。
Visual Studio 的自动完成功能似乎并不关心您为事件使用什么委托 - 您可以输入 +=
[hit Space, Return] ,它会为事件编写一个处理程序方法与恰好匹配的任何代表的您。
那么如果偏离标准模式我会损失什么价值呢?
作为一个额外的问题,C#/CLR 4.0 是否会采取任何措施来改变这一点,也许是通过委托中的逆变? 我试图对此进行调查,但遇到了另一个问题。 我最初将问题的这方面包含在另一个问题中,但它在那里引起了混乱。 将其分成总共三个问题似乎有点太多了...
更新:
事实证明,我对逆变对整个问题的影响的怀疑是正确的!
正如其他地方提到的,新的编译器规则在类型系统中留下了一个漏洞,该漏洞会在运行时爆炸。 通过定义与 Action
不同的 EventHandler
,该漏洞已被有效堵塞。
因此,对于事件,为了避免这种类型漏洞,您不应使用 Action
。 这并不意味着您必须使用 EventHandler
; 这只是意味着,如果您使用通用委托类型,请不要选择启用逆变的类型。
There's a standard pattern for events in .NET - they use a delegate
type that takes a plain object called sender and then the actual "payload" in a second parameter, which should be derived from EventArgs
.
The rationale for the second parameter being derived from EventArgs
seems pretty clear (see the .NET Framework Standard Library Annotated Reference). It is intended to ensure binary compatibility between event sinks and sources as the software evolves. For every event, even if it only has one argument, we derive a custom event arguments class that has a single property containing that argument, so that way we retain the ability to add more properties to the payload in future versions without breaking existing client code. Very important in an ecosystem of independently-developed components.
But I find that the same goes for zero arguments. This means that if I have an event that has no arguments in my first version, and I write:
public event EventHandler Click;
... then I'm doing it wrong. If I change the delegate type in the future to a new class as its payload:
public class ClickEventArgs : EventArgs { ...
... I will break binary compatibility with my clients. The client ends up bound to a specific overload of an internal method add_Click
that takes EventHandler
, and if I change the delegate type then they can't find that overload, so there's a MissingMethodException
.
Okay, so what if I use the handy generic version?
public EventHandler<EventArgs> Click;
No, still wrong, because an EventHandler<ClickEventArgs>
is not an EventHandler<EventArgs>
.
So to get the benefit of EventArgs
, you have to derive from it, rather than using it directly as is. If you don't, you may as well not be using it (it seems to me).
Then there's the first argument, sender
. It seems to me like a recipe for unholy coupling. An event firing is essentially a function call. Should the function, generally speaking, have the ability to dig back through the stack and find out who the caller was, and adjust its behaviour accordingly? Should we mandate that interfaces should look like this?
public interface IFoo
{
void Bar(object caller, int actualArg1, ...);
}
After all, the implementor of Bar
might want to know who the caller
was, so they can query for additional information! I hope you're puking by now. Why should it be any different for events?
So even if I am prepared to take the pain of making a standalone EventArgs
-derived class for every event I declare, just to make it worth my while using EventArgs
at all, I definitely would prefer to drop the object sender argument.
Visual Studio's autocompletion feature doesn't seem to care what delegate you use for an event - you can type +=
[hit Space, Return] and it writes a handler method for you that matches whatever delegate it happens to be.
So what value would I lose by deviating from the standard pattern?
As a bonus question, will C#/CLR 4.0 do anything to change this, perhaps via contravariance in delegates? I attempted to investigate this but hit another problem. I originally included this aspect of the question in that other question, but it caused confusion there. And it seems a bit much to split this up into a total of three questions...
Update:
Turns out I was right to wonder about the effects of contravariance on this whole issue!
As noted elsewhere, the new compiler rules leave a hole in the type system that blows up at runtime. The hole has effectively been plugged by defining EventHandler<T>
differently to Action<T>
.
So for events, to avoid that type hole you should not use Action<T>
. That doesn't mean you have to use EventHandler<TEventArgs>
; it just means that if you use a generic delegate type, don't pick one that is enabled for contravariance.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
什么都没有,你什么也没有失去。 自从 .NET 3.5 发布以来,我一直在使用
Action<>
,它更加自然且更易于编程。我什至不再处理生成的事件处理程序的
EventHandler
类型,只需编写所需的方法签名并将其与 lambda 连接起来:Nothing, you lose nothing. I've been using
Action<>
since .NET 3.5 came out and it is far more natural and easier to program against.I don't even deal with the
EventHandler
type for generated event handlers anymore, simply write the method signature you want and wire it up with a lambda:我也不喜欢事件处理程序模式。 在我看来,Sender 对象并不是真的那么有帮助。 如果事件表明某个对象发生了某些事情(例如更改通知),则在 EventArgs 中包含该信息会更有帮助。 我能看到的发送者的唯一用途是取消订阅事件,但并不总是清楚应该取消订阅哪个事件。
顺便说一句,如果我有自己的想法,Event 就不会是 AddHandler 方法和 RemoveHandler 方法;而是一个事件。 它只是一个 AddHandler 方法,它将返回一个可用于取消订阅的 MethodInvoker。 我希望第一个参数不是 Sender 参数,而是取消订阅所需的 MethodInvoker 的副本(以防对象发现自己正在接收取消订阅调用程序已丢失的事件)。 标准 MulticastDelegate 不适合分派事件(因为每个订阅者应该接收不同的取消订阅委托),但取消订阅事件不需要通过调用列表进行线性搜索。
I don't like the event-handler pattern either. To my mind, the Sender object isn't really all that helpful. In cases where an event is saying something happened to some object (e.g. a change notification) it would be more helpful to have the information in the EventArgs. The only use I could kinda-sorta see for Sender would be to unsubscribe from an event, but it's not always clear what event one should unsubscribe to.
Incidentally, if I had my druthers, an Event wouldn't be an AddHandler method and a RemoveHandler method; it would just be an AddHandler method which would return a MethodInvoker that could be used for unsubscription. Rather than a Sender argument, I'd have the first argument be a copy of the MethodInvoker required for unsubscription (in case an object finds itself receiving events to which the unsubscribe invoker has been lost). The standard MulticastDelegate wouldn't be suitable for dispatching events (since each subscriber should receive a different unsubscription delegate) but unsubscribing events wouldn't require a linear search through an invocation list.