首页 文章

.NET中的事件签名 - 使用强类型'Sender'?

提问于
浏览
104

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

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

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

我正在考虑使用一个使用强类型“发送者”的事件签名,而不是将其键入“对象”,这是当前的.NET设计模式 . 也就是说,而不是使用如下所示的标准事件签名:

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

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

首先,定义一个“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(...));
        }
    }
}

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

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%运行(我已经测试过),但我相信到2005年的旧版Visual Basic .NET没有委托协方差和逆变 . [编辑:我已经对此进行了测试,并且确认:VB.NET 2005及以下版本无法处理此问题,但VB.NET 2008是100%罚款 . 请参阅下面的"Edit #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]

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

提前致谢,

麦克风

Edit #1: 这是对Tommy Carlier's post的回应:

这是一个完整的工作示例,它表明强类型事件处理程序和使用“对象发送方”参数的当前标准事件处理程序可以与此方法共存 . 您可以在代码中复制粘贴并为其运行:

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();
        }
    }
}

Edit #2: 这是对Andrew Hare's statement关于协方差和逆变以及它如何应用的回应 . 使用C#语言的代表已经有了协方差和逆变,只是感觉"intrinsic",但它知道了,但Visual Basic .NET在.NET Framework 3.0之前没有为其代表提供协方差和逆变能力(VB.NET 2008) . 因此,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(sender As Object,e as vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)'与委托'委托Sub StrongTypedEventHandler(Of TSender,TEventArgs As System.EventArgs)没有相同的签名(发件人为Publisher,e为PublisherEventArgs) )”

基本上,代表在VB.NET版本2005及更低版本中是不变的 . 几年前我实际上想到了这个想法,但是VB.NET无法处理这个问题困扰着我...但我现在已经坚定地转向C#了,VB.NET现在可以处理它,所以,好吧,因此这个帖子 .

Edit: Update #3

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

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

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

它确实超过了FxCop规则CA1009,它规定:

“按照惯例,.NET事件有两个参数指定事件发送者和事件数据 . 事件处理程序签名应遵循以下形式:void MyEventHandler(object sender,EventArgs e) . 'sender'参数始终是System.Object类型,即使可以使用更具体的类型 . 'e'参数始终为System.EventArgs类型 . 不提供事件数据的事件应使用System.EventHandler委托类型 . 事件处理程序返回void,以便它们可以将每个事件发送到多个目标方法 . 第一次通话后,目标返回的任何值都将丢失 . “

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

所以使用 SuppressMessageAttribute 就可以了:

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

我希望这种方法在未来的某个时刻成为标准 . 它真的非常好用 .

感谢您的所有意见,我真的很感激...

麦克风

11 回答

  • 2

    我认为这是一个好主意,MS可能根本没有时间或兴趣投入使其更好,例如当他们从ArrayList转移到基于通用的列表时 .

  • 5

    似乎微软已经开始接受这一点,因为类似的例子现在在MSDN上:

    Generic Delegates

  • 13

    你提出的建议实际上有很多意义,我只是想知道这是否就是那种简单的事情之一,因为它最初是在仿制药之前设计的,或者是否有真正的原因 .

  • 25

    Windows运行时(WinRT)引入了 TypedEventHandler<TSender, TResult> 委托,它完全按照 StrongTypedEventHandler<TSender, TResult> 的规定执行,但显然没有 TResult 类型参数的约束:

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

    MSDN文档是here .

  • 1

    我对以下陈述提出异议:

    我相信到2005年的旧版Visual Basic .NET没有委托协方差和逆变 . 我完全意识到这接近亵渎 .

    首先,你在这里所做的一切与协方差或逆变无关 . ( Edit: 前面的说法是错误的,有关更多信息,请参阅Covariance and Contravariance in Delegates)此解决方案在所有CLR版本2.0及更高版本中都能正常工作(显然这在CLR 1.0应用程序中不起作用,因为它使用泛型) .

    其次,我强烈反对你的想法接近“亵渎”,因为这是一个很好的主意 .

  • 5

    我看了一下如何使用新的WinRT处理这个问题,并根据其他意见,并最终决定这样做:

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

    考虑到在WinRT中使用名称TypedEventHandler,这似乎是最好的方法 .

  • 12

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

    由于对象可能代表另一个对象发送事件,因此我可以看到对于具有Object类型的“sender”字段以及使EventArgs派生字段包含对该对象的引用的一些潜在用处 . 被采取行动 . 然而,“发送者”字段的使用可能受到以下事实的限制:对象无法从未知发件人取消订阅 .

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

  • 2

    回顾亵渎是使发送者成为对象类型的唯一原因(如果忽略VB 2005代码中的逆转问题,这是微软的错误恕我直言),任何人都可以建议至少理论动机将第二个参数钉入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;
    }
    
  • 1

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

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

    如果sender是通用的,则click-event的目标不是Button或Label类型,而是Control类型(因为事件是在Control上定义的) . 因此,Button类上的某些事件将具有Control类型的目标,其他事件将具有其他目标类型 .

  • 2

    我不想对你想做的事情有任何不妥 . 在大多数情况下,我怀疑 object sender 参数仍然是为了继续支持2.0之前的代码 .

    如果您确实希望对公共API进行此更改,则可能需要考虑创建自己的基础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)
    

    仍然可以订阅 .

    EDIT

    最后一行让我思考了一点......你应该能够在不破坏任何外部功能的情况下实现你提出的建议,因为运行时没有问题向下转换参数 . 我仍然倾向于 DataEventArgs 解决方案(个人) . 我会这样做,但是知道它是多余的,因为发送者存储在第一个参数中并作为事件args的属性 .

    坚持 DataEventArgs 的一个好处是,您可以链接事件,更改发件人(代表最后一个发件人),而EventArgs保留原始发件人 .

  • 1

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

    public event Action<MyEventType> EventName
    

    其中 MyEventType 不从 EventArgs 继承 . 如果我从不打算使用EventArgs的任何成员,为什么还要费心 .

相关问题