事件动作<> vs event EventHandler<>

时间:2009-09-16 06:50:32

标签: c#

声明event Action<>event EventHandler<>之间是否有任何不同。

假设实际引发事件的对象无关紧要。

例如:

public event Action<bool, int, Blah> DiagnosticsEvent;

VS

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}

两种情况下的使用情况几乎相同:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

我对event EventHandler<>模式有几件我不喜欢的事情:

  • 派生自的额外类型声明 EventArgs的
  • 强制传递对象来源 - 经常没有人关心

更多代码意味着需要维护更多代码而没有任何明显优势。

因此,我更喜欢event Action<>

但是,只有在Action&lt;&gt;中有太多类型参数时,才需要额外的类。

7 个答案:

答案 0 :(得分:81)

根据之前的一些答案,我将把我的答案分解为三个方面。

首先,使用Action<T1, T2, T2... >与使用派生类EventArgs的物理限制。有三个:首先,如果您更改参数的数量或类型,则必须更改每个订阅的方法以符合新模式。如果这是第三方程序集将要使用的面向公众的事件,并且事件args有可能发生变化,那么这将是使用从事件args派生的自定义类的一致性(记住,你仍然可以)使用Action<MyCustomClass>)其次,使用Action<T1, T2, T2... >将阻止您将反馈BACK传递给调用方法,除非您有一个与Action一起传递的某种对象(例如,具有Handled属性) 。第三,你没有得到命名参数,所以如果你传递3 boolint,两个stringDateTime,你有不知道这些价值观是什么意思。 作为旁注,您仍然可以使用“{1}}”来“安全地触发此事件”。

其次,一致性影响。如果你有一个大型系统,你已经在使用,除了你有一个很好的理由之外,按照设计其余部分的方式几乎总是更好。如果您公开面对需要维护的事件,则替换派生类的能力可能很重要。记住这一点。

第三,在现实生活中,我个人发现,我倾向于为需要与之交互的属性更改等事物创建大量的一次性事件(特别是在使用视图模型进行MVVM时彼此交互)或者该事件只有一个参数。大多数情况下,这些事件采用Action<T1, T2, T2... >public event Action<[classtype], bool> [PropertyName]Changed;的形式。在这些情况下,有两个好处。首先,我得到了一个发行类的类型。如果public event Action SomethingHappened;声明并且是唯一触发事件的类,我会在事件处理程序中获得一个MyClass的显式实例。其次,对于诸如属性更改事件之类的简单事件,参数的含义是显而易见的,并在事件处理程序的名称中声明,我不必为这些类型的事件创建无数的类。

答案 1 :(得分:66)

主要区别在于,如果您使用Action<>,您的活动将不会遵循系统中几乎任何其他事件的设计模式,我认为这是一个缺点。

主导设计模式(除了相同的力量之外)的一个优点是,您可以使用新属性扩展EventArgs对象,而无需更改事件的签名。如果您使用Action<SomeClassWithProperties>,这仍然是可能的,但在这种情况下我没有真正看到不使用常规方法的观点。

答案 2 :(得分:15)

在大多数情况下,我会说遵循这种模式。我偏离它,但很少,并且由于特定原因。在这种情况下,我遇到的最大问题是我可能仍然使用Action<SomeObjectType>,允许我稍后添加额外的属性,并使用偶尔的双向属性(想想{{1订阅者需要设置事件对象上的属性的其他反馈事件)。一旦你开始关注这一行,你也可以使用Handled代替某些EventHandler<T>

答案 3 :(得分:14)

当您的代码位于300,000行项目中时,会出现wordier方法的优势。

使用动作,就像你一样,没有办法告诉我bool,int和Blah是什么。如果你的动作传递了一个定义参数的对象,那么确定。

使用需要EventArgs的EventHandler,如果您要使用针对其目的评论的属性的getter来完成DiagnosticsArgs示例,那么您的应用程序将更容易理解。另外,请在DiagnosticsArgs构造函数中注释或完全命名参数。

答案 4 :(得分:6)

如果您遵循标准事件模式,则可以添加扩展方法以使事件触发检查更安全/更容易。 (即,下面的代码添加了一个名为SafeFire()的扩展方法,它执行空检查,以及(显然)将事件复制到一个单独的变量中,以防止可能影响事件的常见空竞争条件。)

(虽然我是否有两种想法,你是否应该在null对象上使用扩展方法......)

public static class EventFirer
{
    public static void SafeFire<TEventArgs>(this EventHandler<TEventArgs> theEvent, object obj, TEventArgs theEventArgs)
        where TEventArgs : EventArgs
    {
        if (theEvent != null)
            theEvent(obj, theEventArgs);
    }
}

class MyEventArgs : EventArgs
{
    // Blah, blah, blah...
}

class UseSafeEventFirer
{
    event EventHandler<MyEventArgs> MyEvent;

    void DemoSafeFire()
    {
        MyEvent.SafeFire(this, new MyEventArgs());
    }

    static void Main(string[] args)
    {
        var x = new UseSafeEventFirer();

        Console.WriteLine("Null:");
        x.DemoSafeFire();

        Console.WriteLine();

        x.MyEvent += delegate { Console.WriteLine("Hello, World!"); };
        Console.WriteLine("Not null:");
        x.DemoSafeFire();
    }
}

答案 5 :(得分:6)

我意识到这个问题已经有10多年的历史了,但在我看来,不仅最明显的答案没有得到解决,而且从这个问题中似乎并不能真正清楚地了解问题的根源。盖子。此外,还有其他有关延迟绑定的问题,以及对委托和lambda的含义(稍后会详细介绍)。

首先要解决房间中800磅重的大象/大猩猩的问题,何时选择eventAction<T> / Func<T>

  • 使用lambda执行一个语句或方法。当您使用event时 想要更多的具有多个发布/订阅模型 将执行的语句/ lambda /功能(这是主要 马上就能实现差异。
  • 当您要将语句/函数编译为表达式树时,请使用lambda。当您想参与更传统的后期绑定(例如在反射和COM互操作中使用)时,请使用委托/事件。

作为事件的示例,让我们使用一个小型控制台应用程序连接一组简单的“标准”事件,如下所示:

public delegate void FireEvent(int num);

public delegate void FireNiceEvent(object sender, SomeStandardArgs args);

public class SomeStandardArgs : EventArgs
{
    public SomeStandardArgs(string id)
    {
        ID = id;
    }

    public string ID { get; set; }
}

class Program
{
    public static event FireEvent OnFireEvent;

    public static event FireNiceEvent OnFireNiceEvent;


    static void Main(string[] args)
    {
        OnFireEvent += SomeSimpleEvent1;
        OnFireEvent += SomeSimpleEvent2;

        OnFireNiceEvent += SomeStandardEvent1;
        OnFireNiceEvent += SomeStandardEvent2;


        Console.WriteLine("Firing events.....");
        OnFireEvent?.Invoke(3);
        OnFireNiceEvent?.Invoke(null, new SomeStandardArgs("Fred"));

        //Console.WriteLine($"{HeightSensorTypes.Keyence_IL030}:{(int)HeightSensorTypes.Keyence_IL030}");
        Console.ReadLine();
    }

    private static void SomeSimpleEvent1(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent1)}:{num}");
    }
    private static void SomeSimpleEvent2(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent2)}:{num}");
    }

    private static void SomeStandardEvent1(object sender, SomeStandardArgs args)
    {

        Console.WriteLine($"{nameof(SomeStandardEvent1)}:{args.ID}");
    }
    private static void SomeStandardEvent2(object sender, SomeStandardArgs args)
    {
        Console.WriteLine($"{nameof(SomeStandardEvent2)}:{args.ID}");
    }
}

输出将如下所示:

enter image description here

如果您对Action<int>Action<object, SomeStandardArgs>进行了同样的操作,则只会看到SomeSimpleEvent2SomeStandardEvent2

那么event内部发生了什么事?

如果我们扩展FireNiceEvent,则编译器实际上正在生成以下内容(与该讨论无关的线程同步,我已经省略了一些细节):

   private EventHandler<SomeStandardArgs> _OnFireNiceEvent;

    public void add_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Combine(_OnFireNiceEvent, handler);
    }

    public void remove_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Remove(_OnFireNiceEvent, handler);
    }

    public event EventHandler<SomeStandardArgs> OnFireNiceEvent
    {
        add
        {
            add_OnFireNiceEvent(value)
        }
        remove
        {
            remove_OnFireNiceEvent(value)

        }
    }

编译器会生成一个私有委托变量,该私有委托变量对于生成它的类名称空间是不可见的。该委托用于订阅管理和后期绑定参与,面向公众的界面是我们都熟悉并喜欢的熟悉的+=-=运营商:)

您可以通过将FireNiceEvent委托的范围更改为protected来自定义添加/删除处理程序的代码。现在,这使开发人员可以向挂钩添加自定义挂钩,例如日志记录或安全挂钩。这确实提供了一些非常强大的功能,这些功能现在允许根据用户角色等对订阅进行自定义访问。您可以使用lambda做到这一点吗? (实际上,您可以通过自定义编译表达式树来进行设置,但这超出了此响应的范围。)

要从此处的一些回应中解决几点:

  • 改变之间的“脆性”确实没有区别 Action<T>中的args列表并更改类中的属性 源自EventArgs。要么不仅需要编译 更改,它们都将更改公共界面,并且需要 版本控制。没什么。

  • 关于哪个是行业标准,取决于哪个地方 这正在被使用,为什么。 Action<T>,这种情况经常在IoC中使用 和DI,而event通常用于消息路由,例如GUI和 MQ类型框架。请注意,我经常说 ,而不是总是

  • 与lambda相比,代表的生存期不同。一个还必须是 意识到捕获...不仅有闭包,而且还有概念 看看猫拖了什么。这确实会影响记忆 足迹/生命周期以及管理泄漏。

还有一件事,我之前提到过……延迟绑定的概念。使用lambda之类的框架时,关于lambda何时变为“活动”状态,您经常会看到这种情况。这与委托的后期绑定非常不同,后者可能发生不止一次(即lambda始终存在,但是绑定根据需要经常发生在需要时),而不是lambda,后者一旦发生就完成了-魔术消失了,方法/属性将始终绑定。注意事项。

答案 6 :(得分:2)

查看Standard .NET event patterns我们找到了

  

.NET事件委托的标准签名是:

     

void OnEventRaised(object sender, EventArgs args);

     

[...]

     

参数列表包含两个参数:发件人和事件参数。发送方的编译时类型是System.Object,即使您可能知道更多派生类型始终是正确的。按照惯例,使用对象

下面在同一页面上,我们找到典型事件定义的示例,类似于

public event EventHandler<EventArgs> EventName;

我们定义了

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}

处理程序可能已经

void OnEventRaised(MyClass sender, EventArgs args);

其中sender具有正确的(更多派生)类型。