更新于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.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](https://istack.dev59.com/4y5mw.webp)
如果你对
Action<int>
或者
Action<object, SomeStandardArgs>
做同样的操作,你只会看到
SomeSimpleEvent2
和
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)
}
}
编译器生成一个私有的委托变量,该变量对于生成它的类命名空间是不可见的。这个委托用于订阅管理和延迟绑定参与,而公共接口则是我们所熟悉的“+=”和“-=”运算符,大家都很喜欢使用它们:)
您可以通过将
FireNiceEvent
委托的作用域更改为protected来自定义添加/移除处理程序的代码。这样一来,开发人员就可以向钩子添加自定义钩子,例如日志记录或安全钩子。这确实提供了一些非常强大的功能,现在可以根据用户角色等自定义访问订阅。使用lambda表达式能做到这一点吗?(实际上,通过自定义编译表达式树是可以的,但超出了本回答的范围)。
针对一些回答中提到的几个问题进行解答:
- 改变
Action<T>
中的参数列表和改变从EventArgs
派生的类的属性之间确实没有"脆弱性"的区别。两者不仅需要进行编译更改,还会改变公共接口并需要进行版本控制。没有区别。
- 至于哪个是行业标准,这取决于在何处以及为何使用。在IoC和DI中经常使用
Action<T>
等,而在消息路由(如GUI和MQ类型框架)中经常使用event
。请注意,我说的是经常,而不是总是。
- 委托与Lambda具有不同的生命周期。人们还必须注意捕获...不仅涉及闭包,还涉及所谓的"看看猫拖回来了什么"。这也会影响内存占用/生命周期以及管理(即泄漏)。
还有一件事,我之前提到过的东西...迟绑定的概念。当使用像LINQ这样的框架时,你经常会看到这种情况,关于lambda何时变得“活跃”。这与委托的迟绑定非常不同,后者可能发生多次(即lambda始终存在,但绑定根据需要随时发生),而lambda一旦发生,就完成了——魔法消失了,方法/属性将始终绑定。记住这一点。