.NET中的事件签名-使用强类型的“发件人”吗?


106

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

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

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

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

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

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

首先,定义一个“ 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%罚款。请参阅下面的“编辑#2”。]可能还有其他.NET语言对此也有问题,我不确定。

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

有人能想到这个问题吗?还是这仅仅是与惯例大相径庭,以至于让人胃口大开?

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

(1)事件设计准则[MSDN 3.5]

(2)C#简单事件引发-使用“发送者”与自定义EventArgs [StackOverflow 2009]

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

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

提前致谢,

麦克风

编辑1:这是对Tommy Carlier的帖子的回应:

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

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)关于协方差和反方差及其在此处如何应用的陈述的回应。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上对其进行了测试,并且没有编译,并指出:

方法“公共Sub SomeEventHandler(作为对象发送,作为vbGenericEventHandling.GenericEventHandling.PublisherEventArgs)”与委托“ Delegate Sub StrongTypedEventHandler(Of TSender,TEventArgs作为System.EventArgs)(作为发布者,作为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事件具有两个用于指定事件发送者和事件数据的参数。事件处理程序签名应遵循以下格式:void MyEventHandler(object sender,EventArgs e)。“ sender”参数始终为System.Object类型,即使可能使用更特定的类型,“ e”参数始终为System.EventArgs类型。不提供事件数据的事件应使用System.EventHandler委托类型。事件处理程序返回void,以便它们可以发送每个事件传递给多个目标方法。目标返回的任何值在第一次调用后都会丢失。”

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

因此,使用a SuppressMessageAttribute可以解决问题:

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

我希望这种方法将来会成为标准。它确实非常好用。

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

麦克风


6
做吧 (不要以为这是一个合理的答案。)
Konrad Rudolph,2009年

1
我的观点并没有真正针对您:当然,您应该在自己的项目中执行此操作。他们争论为什么它可能在BCL中不起作用。
汤米·卡利尔

3
伙计,我希望我的项目从一开始就做到这一点,我讨厌铸造发送者。
Matt H

7
现在,是一个问题。看到,伙计们?这些不是推文大小的oh hi this my hom work solve it plz :code dump:问题,而是我们从中学到的问题。
卡米洛·马丁

3
另一个建议,就是给它EventHandler<,>起个名字GenericEventHandler<,>EventHandler<>BCL中已经有一个泛型,仅被命名为EventHandler。因此EventHandler是一个更通用的名称,并且委托支持类型重载
nawfal 2013年

Answers:


25

微软似乎已经选择了此方法,因为现在MSDN上也有类似的示例:

普通代表


2
+1啊,太好了 他们确实对此有所了解。很好 我希望,虽然,他们使这个VS IDE内公认的模式,因为,因为它是现在,它更尴尬的智能感知等方面使用这种模式
麦克布鲁姆

13

您所提议的内容实际上确实有很多道理,我只是想知道这是否就是其中的一种,因为它最初是在泛型之前设计的,或者是否有真正的原因。


1
我敢肯定这就是原因。但是,由于该语言的较新版本在处理此问题时具有矛盾性,因此看起来它们应该能够以向后兼容的方式处理此问题。以前使用“发送者对象”的处理程序不会中断。但是我不确定这对于较旧的语言不是正确的,对于某些当前的.NET语言可能也不是正确的。
Mike Rosenblum

13

Windows运行时(WinRT)引入了一个TypedEventHandler<TSender, TResult>委托,该委托可以完全执行您的StrongTypedEventHandler<TSender, TResult>工作,但显然不受TResulttype参数的约束:

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

MSDN文档在这里


1
啊,很高兴看到有进展...我想知道为什么TResult不限于从'EventArgs'类继承。'EventArgs'基类基本上是空的;也许他们正在摆脱这一限制?
Mike Rosenblum 2012年

这可能是设计团队的疏忽。谁知道。
皮埃尔·阿诺

好吧,事件无需使用EventArgs就可以正常运行,这只是一个惯例而已
Sebastian 2012年

3
它的TypedEventHandler文件中明确规定,args将是null如果没有事件数据,所以也没有出现,他们所得到的使用基本上是空的对象默认了。我猜测最初的想法是具有第二个类型参数的方法EventArgs可以处理任何事件,因为这些类型始终是兼容的。他们现在可能已经意识到,能够用一种方法处理多个不同的事件并不是那么重要。
jmcilhinney 2014年

1
看起来不像是疏忽。该约束也已从System.EventHandler <TEventArgs>委托中删除。referencesource.microsoft.com/#mscorlib/system/…–
colton7909

5

我对以下陈述表示怀疑:

  • 我相信到2005年的Visual Basic .NET的较旧版本都没有委托协方差和相反方差。
  • 我完全意识到这是亵渎神灵。

首先,您在这里所做的任何事情都不与协方差或协方差有关。编辑: 以前的说法是错误的,以获取更多信息,请参见协变和逆变的代表),该解决方案将在所有CLR 2.0或更高版本的工作只是罚款(显然,这将不会在CLR 1.0应用程序的工作,因为它使用泛型)。

其次,我非常不同意您的想法是“亵渎”,因为这是一个好主意。


2
嗨,安德鲁,谢谢你的赞许!考虑到您的声誉水平,这对我确实意义重大。在协方差/矛盾方面:如果订户提供的委托与发布者事件的签名不完全匹配,则涉及协方差和矛盾。C#从一开始就具有委托协方差和自变量,因此感觉很内在,但是VB.NET直到.NET 3.0才具有委托协方差和自变量。因此,用于.NET 2.0及以下版本的VB.NET将无法使用此系统。(请参阅我在上面的“编辑#2”中添加的代码示例。)
Mike Rosenblum

@Mike-抱歉,您是100%正确的!我已修改答案以反映您的观点:)
Andrew Hare

4
啊,有趣!似乎委托协方差/协方差是CLR的一部分,但是(由于我不知道的原因)VB.NET在最新版本之前未公开它。这是Francesco Balena的一篇文章,展示了如何使用反射实现委托方差异,如果语言本身未启用的话,则可以实现:dotnet2themax.com/blogs/fbalena/…
Mike Rosenblum

1
@Mike-了解CLR支持的东西,但是.NET语言中不支持的东西,总是很有趣。
Andrew Hare

5

我偷看了如何使用新的WinRT以及如何根据此处的其他意见来处理此问题,最后决定这样做:

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

考虑到在WinRT中使用名称TypedEventHandler,这似乎是前进的最佳方法。


为什么要在TEventArgs上添加通用限制?它从EventHandler <>和TypedEventHandler <,>中删除,因为它实际上没有任何意义。
Mike Marynowski

2

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


您可能是对的...另一方面,我认为这只是“标准”,可能根本不是技术问题。也就是说,我不知道所有当前.NET语言都可能具有此功能。我确实知道C#和VB.NET可以处理此问题。但是,我不确定这在当前所有.NET语言中的适用范围如何...但是由于它在C#和VB.NET中有效,并且这里的每个人都非常支持,我认为我很可能会这样做。:-)
Mike Rosenblum

2

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

由于一个对象有可能代表另一个对象发送事件,因此我可以看到具有“对象”类型的“发送者”字段以及使EventArgs派生的字段包含对该对象的引用的一些潜在用途。采取行动。但是,“发件人”字段的有用之处可能受到以下事实的限制:对象没有从未知发件人退订的干净方法。

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


2

回顾亵渎是使发送方成为对象类型的唯一原因(如果要忽略VB 2005代码中的矛盾问题,这是微软的错误之处,恕我直言),任何人都可以至少提出理论上的动机来为EventArgs类型添加第二个参数。更进一步,在这种特殊情况下,是否有充分的理由遵守Microsoft的准则和约定?

需要为我们要在事件处理程序中传递的其他数据开发另一个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;
}

2
是的,创建一个单独的类继承自System.EventArgs似乎很不直观,并且确实代表了额外的工作,但这是有充分理由的。如果您永远不需要更改代码,那么您的方法很好。但是现实情况是,您可能需要在将来的版本中增加事件的功能,并向事件args添加属性。在您的方案中,您将必须在事件处理程序的签名中添加额外的重载或可选参数。这是在VBA和旧版VB 6.0中使用的方法,虽然可行,但在实践中有点难看。
Mike Rosenblum

1
但是,通过继承EventArgs,将来的版本可以继承较旧的事件参数类并对其进行扩充。通过对新事件参数类的基类进行操作,所有较旧的调用方仍可以按原样工作。很干净。您需要做更多的工作,但是对于依赖于您的资料库的所有呼叫者来说,清理工作都更为合理。
Mike Rosenblum

它甚至不需要继承它,您只需将其他功能直接添加到您的事件args类中,它将继续正常工作。就是说,取消了将args固定到eventargs的限制,因为它在很多情况下都没有多大意义。当您知道不再需要扩展特定事件的功能时,或者当您只需要对性能非常敏感的应用程序中的值类型为arg时,就可以了。
Mike Marynowski

1

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

button.Click += ClickHandler;
label.Click += ClickHandler;

void ClickHandler(object sender, EventArgs e) { ... }

如果发送方是通用的,则单击事件的目标将不是Button或Label类型,而是Control类型(因为该事件是在Control上定义的)。因此,Button类上的某些事件将具有Control类型的目标,而其他事件将具有其他目标类型。


2
汤米,你可以做正是同样的事情与系统,我提议。您仍然可以使用具有“对象发送者”参数的标准事件处理程序来处理这些强类型的事件。(请参阅我现在添加到原始帖子中的代码示例。)
Mike Rosenblum

是的,我同意,这是接受标准.NET事件的好东西!
2010年

1

我认为您要做的事情没有任何问题。在大多数情况下,我怀疑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)

仍然可以订阅。

编辑

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

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


嗨,迈克尔,这是一个非常整洁的选择。我喜欢。正如您所提到的,有效地传递两次“ sender”参数是多余的。这里讨论了一种类似的方法:stackoverflow.com/questions/809609/…,并且共识似乎是它太不标准了。这就是为什么我不愿在这里提出强类型的“发件人”想法的原因。(尽管似乎受到了好评,所以我很高兴。)
Mike Rosenblum

1

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

public event Action<MyEventType> EventName

MyEventType不从那里继承EventArgs。如果我从不打算使用EventArgs的任何成员,那就麻烦了。


1
同意!我们为什么要感到自己像猴子?
2010年

1
+ 1-ed,这也是我使用的。有时,简单胜出!甚至event Action<S, T>event Action<R, S, T>等等。我对Raise线程安全有一个扩展方法:)
nawfal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.