事件Action<>与事件EventHandler<>的区别

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


4
加一(我刚刚打破了这个系统)代表“没人关心”。 - hyankov
1
@plusOne:我实际上需要知道发送者!比如发生了什么事情,你想知道是谁做的。这就是你需要“对象源”(又称发送者)的地方。 - Kamran Bigdely
发送者可以是事件负载中的属性。 - Thanasis Ioannidis
7个回答

120

根据之前的回答,我将我的回答分为三个方面。

首先讨论使用 Action<T1, T2, T2... > 与使用派生自EventArgs的类的物理限制。有三个问题: 第一,如果你改变参数的数量或类型,那么每个订阅该事件的方法都必须更改以符合新的模式。如果这是一个公共的事件,第三方程序集将使用它,并且可能会更改事件参数,为了一致性起见,使用派生自事件参数的自定义类是必要的。(记住,你仍然可以使用Action<MyCustomClass>)。第二,使用 Action<T1, T2, T2... > 将防止你将反馈传递回调用方法,除非你有某种对象(例如带有Handled属性的对象)与Action一起传递。第三,你没有命名参数,因此如果你传递3个bool、一个int、两个string和一个DateTime,你不知道这些值的含义。作为一个附注,你仍然可以使用 Action<T1, T2, T2... >的情况下,“安全地触发此事件”方法。

其次,要考虑一致性的影响。如果你已经有一个大型系统正在运行,除非你有非常好的理由,否则跟随系统设计的方式通常是更好的选择。如果你有需要维护的公共事件,替代派生类的能力可能很重要,记住这一点。

第三,实际操作中,我个人发现我往往会为需要交互的属性更改等事情创建许多一次性事件(尤其是在使用相互交互的视图模型进行MVVM时)或者事件只有一个参数。大多数情况下,这些事件采用以下形式:public event Action<[classtype], bool> [PropertyName]Changed;public event Action SomethingHappened;。在这些情况下,有两个好处。首先,我可以获得发布类的类型。如果MyClass声明并且是唯一触发事件的类,我将在事件处理程序中获得一个明确的MyClass实例以便处理。其次,对于简单的事件,如属性更改事件,参数的含义显而易见,并在事件处理程序的名称中说明,因此我不必为这些类型的事件创建大量的类。


2
非常棒的博客文章。如果你正在阅读这个帖子,一定值得一读! - Vexir
2
详细而深思熟虑的答案,解释了结论背后的推理。 - MikeT
2
你可以使用 Func<T1-16, TR> 来代替 Action<T1-16>,其中 TR 是返回类型。 - ChrisBeamond

81

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

除了具有相同性质的优势之外,支配设计模式的一大优点是,你可以通过添加新属性来扩展 EventArgs 对象而不改变事件的签名。如果你使用 Action<SomeClassWithProperties>,这仍然是可能的,但我不认为在那种情况下不使用常规方法有什么意义。


使用 Action<> 会导致内存泄漏吗?EventHandler 设计模式的一个缺点是可能会出现内存泄漏。同时需要指出,可以有多个事件处理程序,但只能有一个 Action - Luke T O'Brien
6
@LukeTO'Brien说,事件本质上是代表,因此使用Action<T>也存在相同的内存泄漏可能性。另外,一个Action<T>可以引用多个方法。以下是一个演示这一点的要点:https://gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434。 - Fredrik Mörk

22

较为冗长的方法的优势在于当您的代码位于一个有300,000行的项目中时,它会更加有用。

使用像您这样的操作,无法告诉我bool、int和Blah是什么。如果您的操作传递了定义参数的对象,则可以。

使用想要EventArgs的EventHandler,如果您能用getter来完成DiagnosticsArgs示例中的属性,并注释其目的,则您的应用程序将更加易于理解。另外,请注释或完全命名DiagnosticsArgs构造函数中的参数。


22
更新于9/2/23:在@Valmont和@Josh Sutterfield的指出下,我必须纠正一下。令我惊讶的是,也让我的一些同事感到惊讶的是,Action<T>Func<T>都继承自MulticastDelegate,因此它们的行为方式相同,即可以使用+=-=运算符添加/删除lambda表达式、委托等,并且可以按预期遍历调用列表。我使用DotPeek确认了这种继承关系,并在测试代码中验证了上述断言。
我觉得最好在更新中添加这个信息,而不是尝试进行编辑,可能会破坏帖子中剩余的部分。我承认错误。/更新
我意识到这个问题已经超过10年了,但是在我看来,不仅最明显的答案没有被解决,而且从问题中也不太清楚对底层的理解。此外,还有其他关于延迟绑定以及委托和Lambda表达式的问题(稍后再谈)。
首先要解决的问题是,在选择`event`还是`Action`/`Func`时:
- 当你想执行一个语句或方法时,使用Lambda表达式。当你希望使用多个语句/Lambda表达式/函数来执行时,使用`event`(这是一个非常重要的区别)。 - 当你想将语句/函数编译为表达式树时,使用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>中的参数列表和改变从EventArgs派生的类的属性之间确实没有"脆弱性"的区别。两者不仅需要进行编译更改,还会改变公共接口并需要进行版本控制。没有区别。
  • 至于哪个是行业标准,这取决于在何处以及为何使用。在IoC和DI中经常使用Action<T>等,而在消息路由(如GUI和MQ类型框架)中经常使用event。请注意,我说的是经常,而不是总是。
  • 委托与Lambda具有不同的生命周期。人们还必须注意捕获...不仅涉及闭包,还涉及所谓的"看看猫拖回来了什么"。这也会影响内存占用/生命周期以及管理(即泄漏)。

还有一件事,我之前提到过的东西...迟绑定的概念。当使用像LINQ这样的框架时,你经常会看到这种情况,关于lambda何时变得“活跃”。这与委托的迟绑定非常不同,后者可能发生多次(即lambda始终存在,但绑定根据需要随时发生),而lambda一旦发生,就完成了——魔法消失了,方法/属性将始终绑定。记住这一点。


这是一个很久之前的帖子 :) 我尝试过类似的东西。我没有看到任何区别。如果你还在的话,能否提供一下代码,以说明这段引用的差异:“如果你对Action<int>或Action<object, SomeStandardArgs>做同样的操作,你只会看到SomeSimpleEvent2和SomeStandardEvent2。”? - Valmont
这是一个很久之前的帖子 :) 我尝试过类似的东西。我没有看到任何区别。如果你还在的话,你能否提供一下代码,以说明这段引用的区别:“如果你对Action<int>或Action<object, SomeStandardArgs>做同样的操作,你只会看到SomeSimpleEvent2和SomeStandardEvent2。”? - undefined
是的 - 确认 @Valmont。.NET 使用 Action 来执行完全相同类型的操作。即使没有 "event" 关键字(令人惊讶地),我也可以通过在单个声明的 action 上使用 += 来链接各种事件。 - Josh Sutterfield
你们两个都是对的,我已经更新了帖子以反映这一点。 - undefined

20
在大多数情况下,我建议遵循这种模式。我确实有偏离它的情况,但非常少,并且是基于特定原因。在这个例子中,我最大的问题可能仍然会使用一个,允许我稍后添加额外的属性,并使用偶尔的双向属性(考虑Handled或其他反馈事件,其中订阅者需要在事件对象上设置属性)。一旦你开始沿着这条路走,你也可以为一些使用。

7

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

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

void OnEventRaised(object sender, EventArgs args);

[...]

参数列表包含两个参数:发送方和事件参数。即使您可能知道一个更具体化的类型,编译时发件人的类型仍然是System.Object。按照惯例,请使用object

在同一页底部,我们可以找到一个典型事件定义的示例,如下:

public event EventHandler<EventArgs> EventName;

如果我们定义了

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

处理程序可能已经被删除。
void OnEventRaised(MyClass sender, EventArgs args);

sender拥有正确(即更为派生)的类型时。


很抱歉没有注意到处理程序签名中的差异,这将受益于一个更精确类型化的“发送者”。 - user1832484

7
如果您遵循标准的事件模式,那么您可以添加一个扩展方法来使事件触发的检查更加安全/容易。 (即以下代码添加了一个名为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();
    }
}

5
你能不能对 Action<T> 执行相同的操作?SafeFire<T>(this Action<T> theEvent, T theEventArgs) 应该是可行的,而且不需要使用 "where"。 - Beachwalker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接