代码之家  ›  专栏  ›  技术社区  ›  Mike Rosenblum

.NET中的事件签名-是否使用强类型的“sender”?

  •  104
  • Mike Rosenblum  · 技术社区  · 16 年前

    我完全意识到我所提议的并不遵循.NET准则,因此,仅凭这个原因,可能是一个糟糕的想法。但是,我想从两个可能的角度来考虑这一点:

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

    (2)这是框架设计者可以考虑更改或更新的概念吗?

    我正在考虑使用一个使用强类型“sender”的事件签名,而不是将其作为当前.NET设计模式的“object”键入。也就是说,不要使用像这样的标准事件签名:

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

    我正在考虑使用一个使用强类型“sender”参数的事件签名,如下所示:

    首先,定义一个“strongtypeventhandler”:

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

    这与操作<tsenser、teventargs>没有什么不同,而是通过使用 StrongTypedEventHandler ,我们强制teventargs来自 System.EventArgs .

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

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

    上述安排将使订阅服务器能够使用不需要强制转换的强类型事件处理程序:

    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")
            {
                // ...
            }
        }
    }
    

    也就是说,如果事件处理程序需要订阅来自不同(或可能未知)对象类型的事件,则该处理程序可以将“sender”参数键入为“object”,以便处理潜在发送者对象的完整范围。

    除了打破惯例(相信我,这是我不能轻视的事情)之外,我不认为这有任何不利之处。

    这里可能存在一些符合CLS的问题。这确实在Visual Basic.NET 2008中运行100%良好(我已经测试过),但我相信Visual Basic.NET到2005的旧版本没有委托协方差和反向方差。 [编辑:我已经对此进行了测试,并确认:vb.net 2005及以下版本无法处理此问题,但vb.net 2008完全可以。请参阅下面的“编辑2”。] 可能还有其他的.NET语言对此也有问题,我不能确定。

    但我不认为自己正在为C或Visual Basic.NET以外的任何语言开发,我不介意将其限制为C和VB.NET for.NET Framework 3.0及更高版本。(老实说,我无法想象回到2.0版本。)

    其他人能想到这个问题吗?或者这仅仅是违背了惯例,以至于让人们的胃口翻滚?

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

    (1) Event Design Guidelines [MSDN 3.5]

    (2) C# simple Event Raising - using “sender” vs. custom EventArgs [StackOverflow 2009]

    (3) Event signature pattern in .net [StackOverflow 2008]

    我对任何人和每个人对此的看法都感兴趣…

    事先谢谢,

    迈克

    编辑第1页: 这是对 Tommy Carlier's post :

    下面是一个完整的工作示例,说明强类型事件处理程序和当前使用“object sender”参数的标准事件处理程序都可以与此方法共存。您可以复制粘贴代码并运行它:

    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页: 这是对 Andrew Hare's statement 关于协方差和反方差以及它在这里的应用。C语言的代表有协方差和反方差的时间太长了,以至于感觉它是“内在的”,但事实并非如此。我不知道,它甚至可能是在clr中启用的,但在.NET Framework 3.0(vb.net 2008)之前,Visual Basic.NET没有为其委托获得协方差和逆变功能。因此,Visual Basic.NET for.NET 2.0及更低版本将无法使用此方法。

    例如,上面的示例可以按如下方式转换为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上对它进行了测试,只是为了确保这一点,它不会编译,声明:

    方法“public sub” SomeEventHandler(作为对象的发送者,E AS vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)' 签名与 委托人的委托人 strongtypeventhandler(属于tsender, teventargs as system.eventargs)(发送方 作为出版商,e as publishereventargs)'

    基本上,委托在VB.NET 2005及以下版本中是不变的。几年前我就想到了这个主意,但是vb.net的处理能力让我很困扰…但我现在已经坚定地转向了C,vb.net现在可以处理它了,所以,好吧,这篇文章就是由此而来的。

    编辑:更新第3版

    好吧,我用这个已经很成功了一段时间了。这真是一个很好的系统。我决定将我的“strongtypeventhandler”命名为“genericEventhandler”,定义如下:

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

    除了这个重命名,我按照上面讨论的方式实现了它。

    它确实跳过了fxcop规则ca1009,该规则规定:

    按照惯例,.NET事件有两个 指定事件的参数 发送方和事件数据。事件处理程序 签名应遵循以下格式: void MyEventHandler(对象发送器, EvestARGS E)。“sender”参数 始终为System.Object类型,偶数 如果可以雇佣更多的 特定类型。“e”参数是 始终为System.EventArgs类型。 不提供事件数据的事件 应使用System.EventHandler 委托类型。事件处理程序返回 无效,以便他们可以发送每个事件 多个目标方法。任何价值 目标返回将丢失 第一次通话后。”

    当然,我们知道这一切,而且无论如何都违反了规则。(如果在任何情况下首选,所有事件处理程序都可以在其签名中使用标准的“对象发送者”——这是一个不间断的更改。)

    所以使用A SuppressMessageAttribute 诀窍:

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

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

    谢谢你们的意见,伙计们,我真的很感激…

    迈克

    11 回复  |  直到 16 年前
        1
  •  25
  •   Bas    15 年前

    微软似乎已经接受了这一点,因为现在在msdn上有一个类似的例子:

    Generic Delegates

        2
  •  13
  •   BFree    16 年前

    实际上,你的提议很有意义,我只是想知道这是不是一个简单的方法,因为它最初是在仿制药之前设计的,或者是否有真正的原因。

        3
  •  12
  •   Pierre Arnaud    14 年前

    Windows运行时(WinRT)引入了 TypedEventHandler<TSender, TResult> 代表,这正是你 StrongTypedEventHandler<TSender, TResult> 是的,但显然没有 TResult 类型参数:

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

    MSDN文档是 here .

        4
  •  5
  •   Andrew Hare    16 年前

    我不同意以下说法:

    • 我相信到2005年,旧版本的VisualBasic.NET没有委托协方差和反向方差。
    • 我完全意识到这近乎于亵渎。

    首先,你在这里所做的一切都与协方差或反方差无关。 ( 编辑: 上一个语句错误,有关详细信息,请参阅 Covariance and Contravariance in Delegates )这个解决方案在所有的clr版本2.0及更高版本中都可以正常工作(显然这将 在使用泛型的clr 1.0应用程序中工作)。

    其次,我强烈反对你的想法近乎“亵渎神明”,因为这是一个很好的想法。

        5
  •  5
  •   Inverness    13 年前

    我看了一眼新Winrt是如何处理这一问题的,并基于这里的其他观点,最后决定这样做:

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

    考虑到在WinRT中使用typeventhandler这个名称,这似乎是向前迈进的最佳方式。

        6
  •  2
  •   Otávio Décio    16 年前

    我认为这是一个很好的想法,微软可能根本没有时间或兴趣投资于改善这一点,例如当他们从ArrayList转移到基于泛型的列表时。

        7
  •  2
  •   supercat    15 年前

    据我所知,“sender”字段总是指保存事件订阅的对象。如果我有自己的druchers,也会有一个字段保存足够的信息,以便在需要时取消订阅事件(*)(例如,考虑订阅“Collection Changed”事件的更改记录器;它包含两个部分,其中一个部分执行实际工作并保存实际数据,另一个部分提供公共接口包装器,主要部分可能包含对包装器部分的弱引用。如果包装器部分被垃圾收集,这意味着不再有任何人对正在收集的数据感兴趣,因此更改记录器应该取消订阅它接收到的任何事件)。

    因为一个对象可能代表另一个对象发送事件,所以我可以看到一些潜在的有用性,比如有一个“sender”字段是对象类型的,并且有一个eventargs派生字段包含对应该对其执行操作的对象的引用。然而,“sender”字段的有效性可能受到这样一个事实的限制:对象无法从未知的发送者取消订阅。

    (*)实际上,处理取消订阅的一种更简单的方法是为返回布尔值的函数提供一个多播委托类型;如果此类委托调用的函数返回true,则该委托将被修补以删除该对象。这意味着委托将不再是真正不可变的,但应该可以以线程安全的方式进行这种更改(例如,通过取消对象引用并让多播委托代码忽略任何嵌入的空对象引用)。在这种情况下,无论事件来自何处,发布和事件到已释放对象的尝试都可以非常干净地处理。

        8
  •  2
  •   Lu4    15 年前

    回想一下亵渎神明是使发送者成为对象类型的唯一原因(如果在VB 2005代码中省略了与之相反的问题,这是微软的一个错误imho),有人能提出将第二个参数固定为eventargs类型的理论动机吗?更进一步,在这种情况下,是否有充分的理由遵守微软的指导方针和惯例?

    需要为我们想要在事件处理程序中传递的另一个数据开发另一个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;
    }
    
        9
  •  1
  •   Tommy Carlier    16 年前

    在当前情况下(发送者是对象),您可以轻松地将方法附加到多个事件:

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

    如果发送方是通用的,则click事件的目标将不是button或label类型,而是control类型(因为该事件是在control上定义的)。因此,button类上的某些事件将具有类型控制的目标,其他事件将具有其他目标类型。

        10
  •  1
  •   Michael Meadows    16 年前

    我不认为你想做的有什么问题。在很大程度上,我怀疑 object sender 参数保持不变,以便继续支持2.0之前的代码。

    如果您真的想对公共API进行这种更改,那么您可能需要考虑创建自己的base 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)
    

    仍然可以订阅。

    编辑

    最后一句话让我有点想…实际上,您应该能够在不破坏任何外部功能的情况下实现您建议的内容,因为运行时向下转换参数没有问题。我还是倾向于 DataEventArgs 解决方案(个人)。但是,我知道它是冗余的,因为发送者存储在第一个参数中,并且作为事件参数的一个属性。

    坚持的一个好处是 数据事件 您可以链接事件,在事件参数保留原始发件人的同时更改发件人(以表示最后一个发件人)。

        11
  •  1
  •   Scott Weinstein    16 年前

    去争取它。对于非基于组件的代码,我经常将事件签名简化为

    public event Action<MyEventType> EventName
    

    哪里 MyEventType 不从继承 EventArgs . 如果我从未打算使用Eventargs的任何成员,那何必麻烦呢?