.NET中的事件签名 – 使用强types的“发件人”?

我完全意识到我所提议的并不遵循.NET的指导方针,因此,仅仅因为这个原因可能是一个糟糕的主意。 不过,我想从两个可能的angular度来考虑:

(1)我是否应该考虑将其用于我自己的开发工作,这是100%用于内部目的。

(2)这是一个框架devise者可以考虑改变或更新的概念吗?

我正在考虑使用事件签名,它使用强types的“发件人”,而不是将其作为当前的.NETdevise模式“对象”input。 也就是说,而不是使用如下所示的标准事件签名:

class Publisher { public event EventHandler<PublisherEventArgs> SomeEvent; } 

我正在考虑使用使用强types的“发件人”参数的事件签名,如下所示:

首先,定义一个“StrongTypedEventHandler”:

 [SerializableAttribute] public delegate void StrongTypedEventHandler<TSender, TEventArgs>( TSender sender, TEventArgs e ) where TEventArgs : EventArgs; 

这与Action <TSender,TEventArgs>并不完全相同,但是通过使用StrongTypedEventHandler ,我们强制TEventArgs从System.EventArgs派生。

接下来,作为一个例子,我们可以在发布类中使用StrongTypedEventHandler,如下所示:

 class Publisher { public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent; protected void OnSomeEvent() { if (SomeEvent != null) { SomeEvent(this, new PublisherEventArgs(...)); } } } 

上述安排将使用户能够使用不需要强制转换的强types事件处理程序:

 class Subscriber { void SomeEventHandler(Publisher sender, PublisherEventArgs e) { if (sender.Name == "John Smith") { // ... } } } 

我完全意识到,这违反了标准的.NET事件处理模式; 但是,请记住,如果需要,反转会使用户能够使用传统的事件处理签名:

 class Subscriber { void SomeEventHandler(object sender, PublisherEventArgs e) { if (((Publisher)sender).Name == "John Smith") { // ... } } } 

也就是说,如果一个事件处理程序需要订阅来自不同的(或可能是未知的)对象types的事件,处理程序可以将“发送者”参数键入为“对象”,以便处理潜在的发送者对象的全部范围。

除了打破常规(这是我不轻视的,相信我),我想不出任何缺点。

这里可能有一些CLS合规问题。 这确实运行在Visual Basic .NET 2008 100%(我testing过),但是我相信到2005年的Visual Basic .NET的老版本没有委托协方差和逆变。 [编辑:我已经testing了这一点,并确认:VB.NET 2005及以下不能处理这个,但VB.NET 2008是100%罚款。 请参阅下面的“编辑#2”。]也许有其他的.NET语言,也有这个问题,我不能确定。

但是我没有看到自己正在开发除C#或Visual Basic .NET之外的其他语言,我不介意将它限制在.NET Framework 3.0及更高版本的C#和VB.NET中。 (在这一点上,我无法想象回到2.0,说实话。)

任何人都可以想到这个问题吗? 还是这样做只是打破常规,使人们的胃口转向?

以下是我find的一些相关链接:

(1) 事件devise指南[MSDN 3.5]

(2) C#简单事件提升 – 使用“发件人”与自定义EventArgs [StackOverflow 2009]

(3) .net中的事件签名模式[StackOverflow 2008]

我对任何人和每个人的意见都很感兴趣

提前致谢,

麦克风

编辑#1:这是对Tommy Carlier的post的回应:

下面是一个完整的工作示例,它显示强types事件处理程序和使用“对象发送者”参数的当前标准事件处理程序可以与此方法共存。 你可以复制粘贴代码并运行:

 namespace csScrap.GenericEventHandling { class PublisherEventArgs : EventArgs { // ... } [SerializableAttribute] public delegate void StrongTypedEventHandler<TSender, TEventArgs>( TSender sender, TEventArgs e ) where TEventArgs : EventArgs; class Publisher { public event StrongTypedEventHandler<Publisher, PublisherEventArgs> SomeEvent; public void OnSomeEvent() { if (SomeEvent != null) { SomeEvent(this, new PublisherEventArgs()); } } } class StrongTypedSubscriber { public void SomeEventHandler(Publisher sender, PublisherEventArgs e) { MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called."); } } class TraditionalSubscriber { public void SomeEventHandler(object sender, PublisherEventArgs e) { MessageBox.Show("TraditionalSubscriber.SomeEventHandler called."); } } class Tester { public static void Main() { Publisher publisher = new Publisher(); StrongTypedSubscriber strongTypedSubscriber = new StrongTypedSubscriber(); TraditionalSubscriber traditionalSubscriber = new TraditionalSubscriber(); publisher.SomeEvent += strongTypedSubscriber.SomeEventHandler; publisher.SomeEvent += traditionalSubscriber.SomeEventHandler; publisher.OnSomeEvent(); } } } 

编辑#2:这是对安德鲁·海尔关于协变和逆变以及它如何应用于此的陈述的回应。 C#语言的代表长久以来一直具有协变性和相反性,它只是感觉“内在”,但事实并非如此。 它甚至可能是在CLR中启用的东西,我不知道,但Visual Basic .NET没有为其代表获得协变和反转能力,直到.NET Framework 3.0(VB.NET 2008)。 结果,.NET 2.0及以下版本的Visual Basic.NET将无法使用这种方法。

例如,上面的例子可以转换成VB.NET如下:

 Namespace GenericEventHandling Class PublisherEventArgs Inherits EventArgs ' ... ' ... End Class <SerializableAttribute()> _ Public Delegate Sub StrongTypedEventHandler(Of TSender, TEventArgs As EventArgs) _ (ByVal sender As TSender, ByVal e As TEventArgs) Class Publisher Public Event SomeEvent As StrongTypedEventHandler(Of Publisher, PublisherEventArgs) Public Sub OnSomeEvent() RaiseEvent SomeEvent(Me, New PublisherEventArgs) End Sub End Class Class StrongTypedSubscriber Public Sub SomeEventHandler(ByVal sender As Publisher, ByVal e As PublisherEventArgs) MessageBox.Show("StrongTypedSubscriber.SomeEventHandler called.") End Sub End Class Class TraditionalSubscriber Public Sub SomeEventHandler(ByVal sender As Object, ByVal e As PublisherEventArgs) MessageBox.Show("TraditionalSubscriber.SomeEventHandler called.") End Sub End Class Class Tester Public Shared Sub Main() Dim publisher As Publisher = New Publisher Dim strongTypedSubscriber As StrongTypedSubscriber = New StrongTypedSubscriber Dim traditionalSubscriber As TraditionalSubscriber = New TraditionalSubscriber AddHandler publisher.SomeEvent, AddressOf strongTypedSubscriber.SomeEventHandler AddHandler publisher.SomeEvent, AddressOf traditionalSubscriber.SomeEventHandler publisher.OnSomeEvent() End Sub End Class End Namespace 

VB.NET 2008可以100%正常运行。 但是我现在已经在VB.NET 2005上进行了testing,可以肯定的是,它不能编译,指出:

方法'Public Sub SomeEventHandler(sender As Object,e As vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)'与委托'Delegate Sub StrongTypedEventHandler(TSender,TEventArgs As System.EventArgs)(sender As Publisher,e As PublisherEventArgs) “

基本上,代表在VB.NET版本2005及以下版本是不变的。 实际上我在几年前想到了这个想法,但是VB.NET无法处理这个问题……但是现在我已经坚定地转向了C#,并且VB.NET现在可以处理它,所以,这篇文章。

编辑:更新#3

好的,我现在已经用了一段时间了。 这真的是一个很好的系统。 我决定将我的“StrongTypedEventHandler”命名为“GenericEventHandler”,定义如下:

 [SerializableAttribute] public delegate void GenericEventHandler<TSender, TEventArgs>( TSender sender, TEventArgs e ) where TEventArgs : EventArgs; 

除了这个重命名之外,我完全按照上面的讨论来实现它。

它通过FxCop规则CA1009,它说:

“按照惯例,.NET事件有两个参数来指定事件发送者和事件数据,事件处理器签名应该遵循以下forms:void MyEventHandler(object sender,EventArgs e)'sender'参数始终是System.Objecttypes,即使可以使用更具体的types,'e'参数总是System.EventArgstypes,不提供事件数据的事件应该使用System.EventHandler委托types,事件处理程序返回void,以便它们可以发送每个事件到多个目标方法,目标返回的任何值在第一次调用后都会丢失。

当然,我们知道这一切,而且正在违反规则。 (所有事件处理程序都可以在标记中使用标准的“对象发件人”,如果在任何情况下都是这样的话) – 这是一个不会改变的情况。

所以使用SuppressMessageAttribute就可以做到这一点:

 [SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Justification = "Using strong-typed GenericEventHandler<TSender, TEventArgs> event handler pattern.")] 

我希望这种方法在未来某个时候成为标准。 它真的很好用。

谢谢你们所有的意见家伙,我真的很感激…

麦克风

看来微软已经拿起了这个MSDN上类似的例子:

通用代表

你提出的build议确实有很多意义,我只是想知道这是否是其中之一,因为它最初是在generics之前devise的,或者是真正的原因。

Windows Runtime(WinRT)引入了一个TypedEventHandler<TSender, TResult>委托,它完全像你的StrongTypedEventHandler<TSender, TResult>所做的那样,但显然没有对TResulttypes参数的限制:

 public delegate void TypedEventHandler<TSender, TResult>(TSender sender, TResult args); 

MSDN文档在这里 。

我对以下陈述有所质疑:

  • 我相信通过2005年的旧版本的Visual Basic .NET不具有委托协方差和变换。
  • 我完全明白,这是亵渎之缘。

首先,你在这里所做的一切与协变或逆变有什么关系。 编辑: 以前的语句是错误的,更多的信息请参阅代表中的协变和逆变 )这个解决scheme在所有的CLR 2.0版本中都能正常工作(显然这在CLR 1.0应用程序中是无效的 ,因为它使用了generics)。

其次,我坚决不同意你的想法接近“亵渎”,因为这是一个好主意。

我看了一下这个新的WinRT是如何处理的,并且基于这里的其他观点,最后决定这样做:

 [Serializable] public delegate void TypedEventHandler<in TSender, in TEventArgs>( TSender sender, TEventArgs e ) where TEventArgs : EventArgs; 

这似乎是考虑在WinRT中使用名称TypedEventHandler的最佳方式。

我认为这是一个好主意,MS可能根本没有时间或兴趣去投资,例如当他们从ArrayList移动​​到基于generics的列表时。

据我所知,“发件人”字段总是应该引用持有事件订阅的对象。 如果我有我的干事,那么在有必要的时候,还会有一个信息足以取消订阅事件的信息(*)(例如,考虑订阅“收集改变”事件的更改logging器;它包含两个部分,其中一个做实际的工作并保存实际的数据,另一个提供一个公共的接口包装器,主要部分可以对包装器部分持有一个弱引用,如果包装器部分被垃圾收集,那就意味着不再有人对正在收集的数据感兴趣,并且变更logging器因此应该退出收到的任何事件)。

由于一个对象可能代表另一个对象发送事件,所以我可以看到一些潜在的有用性,这个对象具有一个“对象types”的“发送者”字段,并且让EventArgs派生的字段包含对应的对象的引用被采取行动。 然而,“发件人”字段的用处可能受限于一个事实,即没有干净的方式让对象从未知发件人取消订阅。

(*)实际上,处理取消订阅的更清晰的方法是为返回布尔值的函数设置多播委托types; 如果这样一个委托调用的函数返回True,委托将被修补以删除该对象。 这将意味着代表将不再是真正的不可变的,但是应该可以以线程安全的方式实现这种改变(例如,通过使对象引用无效并使多播委托代码忽略任何embedded的空对象引用)。 在这种情况下,无论事件来自哪里,试图发布和处理已处理对象的事件都可以非常干净地处理。

回顾亵渎是使发件人成为对象types的唯一原因(如果在VB 2005代码中这是一个微软的错误恕我直言,要省略反向变换的问题),任何人都可以提出至less理论动机来将第二个参数指向EventArgstypes。 再进一步,在这个特定的情况下是否有一个很好的理由来符合微软的指导方针和惯例?

需要开发另一个EventArgs封装器来处理另一个我们想要在事件处理程序中传递的数据,这似乎很奇怪,为什么不能直接在那里传递这些数据。 考虑下面的代码部分

[实施例1]

 public delegate void ConnectionEventHandler(Server sender, Connection connection); public partial class Server { protected virtual void OnClientConnected(Connection connection) { if (ClientConnected != null) ClientConnected(this, connection); } public event ConnectionEventHandler ClientConnected; } 

[例2]

 public delegate void ConnectionEventHandler(object sender, ConnectionEventArgs e); public class ConnectionEventArgs : EventArgs { public Connection Connection { get; private set; } public ConnectionEventArgs(Connection connection) { this.Connection = connection; } } public partial class Server { protected virtual void OnClientConnected(Connection connection) { if (ClientConnected != null) ClientConnected(this, new ConnectionEventArgs(connection)); } public event ConnectionEventHandler ClientConnected; } 

有了目前的情况(发件人是对象),您可以轻松地将方法附加到多个事件:

 button.Click += ClickHandler; label.Click += ClickHandler; void ClickHandler(object sender, EventArgs e) { ... } 

如果发件人是通用的,那么click事件的目标将不是Button或Labeltypes,而是typesControl(因为事件在Control上定义)。 因此,Button-class上的一些事件将具有Controltypes的目标,而其他types将具有其他目标types。

我不认为你想做什么是错的。 在大多数情况下,我怀疑object sender参数仍然是为了继续支持前2.0代码。

如果你真的想为公共API做这个改变,你可能要考虑创build你自己的基础EvenArgs类。 像这样的东西:

 public class DataEventArgs<TSender, TData> : EventArgs { private readonly TSender sender, TData data; public DataEventArgs(TSender sender, TData data) { this.sender = sender; this.data = data; } public TSender Sender { get { return sender; } } public TData Data { get { return data; } } } 

那么你可以像这样宣布你的事件

 public event EventHandler<DataEventArgs<MyClass, int>> SomeIndexSelected; 

和这样的方法:

 private void HandleSomething(object sender, EventArgs e) 

仍然可以订阅。

编辑

最后一行让我想起了一点…实际上你应该能够实现你的build议,而不会破坏任何外部function,因为运行时没有问题的向下转换参数。 我仍然倾向于DataEventArgs解决scheme(个人)。 我会这样做,但是知道它是多余的,因为发送者存储在第一个参数中,并作为事件参数的属性。

坚持使用DataEventArgs一个好处是,当EventArgs保留原始发件人时,您可以链接事件,更改发件人(代表最后一个发件人)。

去吧。 对于非基于组件的代码,我经常简化事件签名

 public event Action<MyEventType> EventName 

MyEventType不从EventArgsinheritance。 为什么打扰,如果我从来没有打算使用任何EventArgs的成员。