事件Action <> vs event EventHandler <>

Bor*_*itz 132 c#

声明event Action<>和声明之间有什么不同吗event EventHandler<>

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

例如:

public event Action<bool, int, Blah> DiagnosticsEvent;
Run Code Online (Sandbox Code Playgroud)

VS

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

obj.DiagnosticsEvent += HandleDiagnosticsEvent;
Run Code Online (Sandbox Code Playgroud)

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

  • 从EventArgs派生的额外类型声明
  • 强制传递对象来源 - 通常无人问津

更多代码意味着维护更多代码而没有任何明显的优势.

结果,我更喜欢 event Action<>

但是,只有在Action <>中有太多类型参数时,才需要额外的类.

Pau*_*hde 85

基于以前的一些答案,我将把我的答案分解为三个方面.

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

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

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

  • 让我把链接添加到你的帖子中,更详细,更有条理的回答http://www.paulrohde.com/events-eventhandler-or-action/ (13认同)
  • 您可以使用 `Func&lt;T1-16, TR&gt;` 来代替 `Action&lt;T1-16&gt;` 返回,其中 TR 是返回类型。 (3认同)
  • 很棒的博客文章。如果您正在阅读这篇文章,绝对值得一读! (2认同)
  • 详细且经过深思熟虑的答案,解释了结论背后的推理 (2认同)

Fre*_*örk 66

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

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

  • @ LukeTO'Brien:事件本质上是委托,因此`Action <T>`存在相同的内存泄漏可能性.另外,`Action <T>`_can_指的是几种方法.这是一个证明:https://gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434的要点 (2认同)

Mar*_*ell 16

在大多数情况下,我会说遵循这种模式.我已经从中偏离,但很少,而对于具体原因.在这种情况下,我遇到的最大问题是我可能仍然使用Action<SomeObjectType>,允许我稍后添加额外的属性,并使用偶尔的双向属性(想想Handled,或其他反馈事件,其中订阅者需要在事件对象上设置属性.一旦你开始下线,你也可以使用EventHandler<T>一些T.


Pau*_*ich 14

当您的代码在300,000行项目中时,会出现wordier方法的优点.

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

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


Sta*_*itz 10

我意识到这个问题已经有 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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

输出将如下所示:

在此处输入图片说明

如果你对Action<int>or做同样的事情Action<object, SomeStandardArgs>,你只会看到SomeSimpleEvent2and SomeStandardEvent2

那么里面发生了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)

        }
    }
Run Code Online (Sandbox Code Playgroud)

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

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

从这里的一些回复中解决几点:

  • 更改 args 列表中的 args 列表Action<T>和更改从EventArgs. 要么不仅需要编译更改,它们都将更改公共接口并需要版本控制。没有不同。

  • 关于哪个是行业标准,这取决于它在何处使用以及为什么使用。Action<T>此类常用于 IoC 和 DI,event常用于消息路由,如 GUI 和 MQ 类型的框架。请注意,我经常说,并非总是如此

  • 委托与 lambda 的生命周期不同。人们还必须意识到捕获......不仅仅是关闭,而且还有“看看猫拖进来的东西”的概念。这确实会影响内存占用/生命周期以及管理(又名泄漏)。

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


Pau*_*ott 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • ...你不能用Action <T>做同样的事吗?SafeFire <T>(这个Action <T> theEvent,T theEventArgs)应该工作......而且不需要使用"where" (4认同)

use*_*484 5

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

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

void OnEventRaised(object sender, EventArgs args);

[...]

参数列表包含两个参数:sender和事件参数。sender 的编译时类型是 System.Object,即使您可能知道一个始终正确的派生类型。按照惯例,使用object

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

public event EventHandler<EventArgs> EventName;
Run Code Online (Sandbox Code Playgroud)

如果我们定义

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}
Run Code Online (Sandbox Code Playgroud)

处理程序可能是

void OnEventRaised(MyClass sender, EventArgs args);
Run Code Online (Sandbox Code Playgroud)

wheresender有正确的(更派生的)类型。