事件Action <>与事件EventHandler <>


144

声明event Action<>和之间有什么区别吗event EventHandler<>

假设什么对象实际引发事件都没有关系。

例如:

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

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 <>中的类型参数太多时,才需要一个额外的类。


2
plusOne(我刚刚击败了系统)表示“没有人在乎”
hyankov

@plusOne:我实际上需要知道发件人!假设发生了某些事情,而您想知道是谁做的。那就是您需要的“对象源”(又名发送者)。
Kamran Bigdely

发送者可以成为事件有效负载中的属性
Thanasis Ioannidis

Answers:


67

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

主导设计模式的一个好处(除了相同的功能之外)是,您可以在EventArgs不更改事件签名的情况下使用新属性扩展对象。如果您使用Action<SomeClassWithProperties>,这仍然有可能,但是在那种情况下不使用常规方法并没有真正意义。


使用会Action<>导致内存泄漏吗?EventHandler设计模式的缺点之一是内存泄漏。还应该指出,可以有多个事件处理程序,但是只有一个操作
Luke T O'Brien

4
@ LukeTO'Brien:事件本质上是委托,因此与存在相同的内存泄漏可能性Action<T>。另外,Action<T> 可以参考几种方法。这是一个演示要点的要点:gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
FredrikMörk17年

88

基于先前的一些答案,我将把我的答案分为三个区域。

首先,使用Action<T1, T2, T2... >和使用的派生类的物理限制EventArgs。有三个:首先,如果您更改参数的数量或类型,则必须更改每个预订的方法以符合新模式。如果这是第3方程序集将要使用的面向公众的事件,并且事件args可能会发生变化,那么这就是出于一致性考虑使用从事件args派生的自定义类的原因(请记住,您仍然可以使用Action<MyCustomClass>)二,使用Action<T1, T2, T2... >会阻止您传递反馈回调用方法,除非你有一些类型的对象(与一个已处理的属性为例)与该行动传承下去。第三,您不会获得命名参数,因此,如果要传递3 bool的an int,则两个string的和DateTime,您不知道这些值的含义。附带说明,您仍然可以使用Action<T1, T2, T2... >“ 在仍然使用时安全触发此事件的方法”。

其次,一致性的影响。如果您已经使用了大型系统,那么除非总有很好的理由,否则最好遵循其余系统的设计方法。如果您有需要维护的公开事件,那么替换派生类的能力就很重要。记在脑子里。

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


很棒的博客文章。如果您正在阅读此主题,绝对值得一读!
Vexir 2014年

1
经过深思熟虑的详细答案解释了结论的原因
MikeT 2015年

18

在大多数情况下,我会遵循模式。我已经偏离了它,但出于特定原因却很少。在这种情况下,我遇到的最大问题是,我可能仍会使用Action<SomeObjectType>,让我以后再添加额外的属性,并偶尔使用2向属性(如思考Handled或其他反馈事件,订阅者需要在事件对象上设置属性)。而且,一旦您开始使用这条线,就可以使用EventHandler<T>T


14

当您的代码位于300,000行项目中时,字法比较方便。

正如您所使用的那样,无法使用动作来告诉我bool,int和Blah是什么。如果您的操作传递了定义参数的对象,则确定。

使用一个需要EventArgs的EventHandler,如果您用getters来完成DiagnosticsArgs示例,以说明其目的的属性,那么您的应用程序将更容易理解。另外,请在DiagnosticsArgs构造函数中注释或完整命名参数。


6

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

(尽管我有两种想法,是否应该对空对象使用扩展方法...)

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

4
...对Action <T>不能做同样的事情吗?SafeFire <T>(此Action <T> theEvent,T theEventArgs)应该可以工作...并且无需使用“ where”
Beachwalker

6

我意识到这个问题已经有10多年的历史了,但在我看来,不仅没有解决最明显的答案,而且从这个问题中似乎并不能真正清楚地了解幕后的情况。此外,还有其他有关延迟绑定的问题,这对于委托人和lambda意味着什么(稍后会详细介绍)。

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

  • 使用lambda执行一个语句或方法。使用event时,您用多条语句/ lambda表达式/功能,将执行(这是一个想多pub / sub模型的主要 区别了蝙蝠的权利)。
  • 当您要将语句/函数编译为表达式树时,请使用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}");
    }
}

输出将如下所示:

在此处输入图片说明

如果您对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做到这一点吗?(实际上,您可以通过自定义编译表达式树来进行设置,但这超出了此响应的范围)。

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

  • 在更改args列表Action<T>和更改从派生的类中的属性之间的“脆性”确实没有区别EventArgs。要么不仅需要更改编译,还需要更改公共接口并需要版本控制。没有不同。

  • 关于哪个是行业标准,这取决于在哪里使用以及为什么使用。Action<T>这些通常用在IoC和DI中,并且event经常用在诸如GUI和MQ类型框架之类的消息路由中。请注意,我经常但不总是说

  • 代表的生命周期与lambdas不同。人们还必须意识到捕捉……不仅是封闭,而且还有“看猫所拖的东西”的概念。这确实会影响内存占用/寿命以及管理(也称为泄漏)。

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


4

查看标准.NET事件模式,我们发现

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

void OnEventRaised(object sender, EventArgs args);

[...]

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

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

public event EventHandler<EventArgs> EventName;

如果我们定义了

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

该处理程序本来可以

void OnEventRaised(MyClass sender, EventArgs args);

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


很抱歉,我们尚未指出处理程序签名之间的差异,这将有助于使用更精确的typed sender
user1832484
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.