委托和事件之间有什么区别?

What are the differences between delegates and events?

提问人:Sean Chambers 提问时间:8/27/2008 最后编辑:5StringRyanSean Chambers 更新时间:4/6/2021 访问量:153466

问:

委托和事件之间有什么区别?不是两者都包含对可以执行的函数的引用吗?

C# 事件 委托 术语表

评论

7赞 B Faley 11/26/2011
codeproject.com/KB/cs/events.aspx
3赞 Rahul Lalit 7/20/2016
这通过示例解释 看看 unitygeek.com/delegates-events-unity
3赞 Ben 3/17/2021
老问题(虽然仍然非常相关),但看起来文档现在也描述了它:learn.microsoft.com/en-us/dotnet/csharp/......对我来说,事件的可选性质是关键信息。

答:

332赞 mmcdole 8/27/2008 #1

事件声明在委托实例上添加了一层抽象和保护。此保护可防止委托的客户端重置委托及其调用列表,并且只允许在调用列表中添加或删除目标。

评论

14赞 Miguel Gamboa 2/18/2015
不完全正确。您可以在没有后端委托实例的情况下声明事件。在 c# 中,可以显式实现事件,并使用所选的不同后端数据结构。
111赞 Jorge Córdoba 8/27/2008 #2

除了句法和操作属性之外,还存在语义上的差异。

从概念上讲,委托是函数模板;也就是说,它们表达了函数必须遵守的契约,以便被视为委托的“类型”。

事件代表...好吧,事件。它们旨在在发生某些事情时提醒某人,是的,它们遵循委托定义,但它们不是一回事。

即使它们完全相同(在语法上和在 IL 代码中),仍然存在语义差异。一般来说,我更喜欢为两个不同的概念使用两个不同的名称,即使它们以相同的方式实现(这并不意味着我喜欢两次使用相同的代码)。

评论

1赞 Pap 8/18/2016
那么,我们能说一个事件是代表的“特殊”类型吗?
0赞 steve 9/30/2019
我不明白你的意思。您可以使用委托来“在发生某些事情时提醒某人”。也许你不会那样做,但你可以,因此它不是事件的固有属性。
0赞 Rahul_Patil 4/15/2020
@Jorge科尔多瓦的例子,代表和活动代表是报纸所有者和活动(订阅或取消订阅),有些人买了报纸,有些人不买报纸意味着报纸所有者不能强迫每个人购买报纸,我的观点是对还是错?
5赞 Paul Hill 8/25/2010 #3

您还可以在接口声明中使用事件,但对于委托则不然。

评论

2赞 Alexandr Nikitin 7/12/2013
@surfen接口可以包含事件,但不能包含委托。
2赞 chtenb 12/17/2015
你到底是什么意思?您可以拥有接口定义。Action a { get; set; }
4赞 supercat 8/28/2011 #4

.net 中的事件是 Add 方法和 Remove 方法的指定组合,两者都需要某种特定类型的委托。C# 和 vb.net 都可以为 add 和 remove 方法自动生成代码,这些方法将定义一个委托来保存事件订阅,并添加/删除传入的委托给/从该订阅委托。VB.net 还将自动生成代码(使用 RaiseEvent 语句)以调用订阅列表,当且仅当它为非空时;出于某种原因,C# 不会生成后者。

请注意,虽然使用多播委托管理事件订阅很常见,但这并不是这样做的唯一方法。从公共角度来看,潜在的事件订阅者需要知道如何让对象知道它想要接收事件,但它不需要知道发布者将使用什么机制来引发事件。另请注意,虽然在 .net 中定义事件数据结构的人显然认为应该有一种公开的方法来引发它们,但 C# 和 vb.net 都没有使用该功能。

38赞 vibhu 4/12/2013 #5

这是另一个值得参考的好链接。http://csharpindepth.com/Articles/Chapter2/Events.aspx

简而言之,从文章中可以看出 - 事件是对委托的封装。

引用文章:

假设事件在 C#/.NET 中不存在作为概念存在。其他班级如何订阅事件?三个选项:

  1. 公共委托变量

  2. 由属性支持的委托变量

  3. 具有 AddXXXHandler 和 RemoveXXXHandler 方法的委托变量

选项 1 显然是可怕的,出于所有正常原因,我们憎恶公共变量。

选项 2 稍微好一点,但允许订阅者有效地相互覆盖 - 编写 someInstance.MyEvent = eventHandler 太容易了;这将替换任何现有的事件处理程序,而不是添加新的事件处理程序。此外,您仍然需要编写属性。

选项 3 基本上是事件给你的,但有一个有保证的约定(由编译器生成,并由 IL 中的额外标志支持)和一个“免费”实现,如果你对类似字段的事件给你的语义感到满意。对事件的订阅和取消订阅是封装的,不允许任意访问事件处理程序列表,语言可以通过为声明和订阅提供语法来简化事情。

评论

0赞 jrh 8/30/2018
这更像是一个理论上的问题,但FWIW我一直觉得“选项1很糟糕,因为我们不喜欢公共变量”的论点可以用更多的澄清。如果他这么说是因为这是“糟糕的 OOP 实践”,那么从技术上讲,变量会暴露“数据”,但据我所知,OOP 从未提到过任何概念(它既不是“对象”也不是“消息”),而且 .NET 实际上几乎不会像对待数据一样对待委托。public DelegateDelegate
0赞 jrh 8/30/2018
虽然我还想提供更实用的建议,但如果你想确保只有一个处理程序,那么使用变量创建自己的方法可能是一个不错的选择。在这种情况下,您可以检查是否已设置处理程序,并做出适当的反应。如果您需要持有 的对象能够清除所有处理程序(没有为您提供任何执行此操作的方法),这也可能是一个很好的设置。AddXXXHandlerprivate DelegateDelegateevent
144赞 faby 7/12/2014 #6

要了解差异,您可以查看以下 2 个示例

带有委托的示例(在本例中为 Action - 这是一种不返回值的委托)

public class Animal
{
    public Action Run {get; set;}

    public void RaiseEvent()
    {
        if (Run != null)
        {
            Run();
        }
    }
}

若要使用委托,应执行如下操作:

Animal animal= new Animal();
animal.Run += () => Console.WriteLine("I'm running");
animal.Run += () => Console.WriteLine("I'm still running") ;
animal.RaiseEvent();

此代码运行良好,但您可能有一些弱点。

例如,如果我写这个:

animal.Run += () => Console.WriteLine("I'm running");
animal.Run += () => Console.WriteLine("I'm still running");
animal.Run = () => Console.WriteLine("I'm sleeping") ;

在最后一行代码中,我覆盖了以前的行为,只是缺少一个(我用而不是+=+=)

另一个弱点是,使用你的类的每个类都可以直接调用委托。例如,或 在 Animal 类之外有效。Animalanimal.Run()animal.Run.Invoke()

为了避免这些弱点,你可以在 c# 中使用。events

你的动物类将以这种方式改变:

public class ArgsSpecial : EventArgs
{
    public ArgsSpecial (string val)
    {
        Operation=val;
    }

    public string Operation {get; set;}
} 

public class Animal
{
    // Empty delegate. In this way you are sure that value is always != null 
    // because no one outside of the class can change it.
    public event EventHandler<ArgsSpecial> Run = delegate{} 
        
    public void RaiseEvent()
    {  
         Run(this, new ArgsSpecial("Run faster"));
    }
}

调用事件

 Animal animal= new Animal();
 animal.Run += (sender, e) => Console.WriteLine("I'm running. My value is {0}", e.Operation);
 animal.RaiseEvent();

差异:

  1. 您使用的不是公共属性,而是公共字段(使用事件,编译器可以保护您的字段免受不必要的访问)
  2. 无法直接分配事件。在这种情况下,它不会导致我在覆盖该行为时显示的先前错误。
  3. 班级以外的任何人都不能引发或调用该事件。例如,在 Animal 类之外无效,并且会产生编译器错误。animal.Run()animal.Run.Invoke()
  4. 事件可以包含在接口声明中,而字段不能

笔记:

EventHandler 声明为以下委托:

public delegate void EventHandler (object sender, EventArgs e)

它接受发送者(对象类型)和事件参数。如果发送方来自静态方法,则为 null。

此示例使用 ,也可以编写为 instead。EventHandler<ArgsSpecial>EventHandler

有关 EventHandler 的文档,请参阅此处

评论

8赞 dance2die 8/28/2014
一切看起来都很好,直到我遇到“班外没有人可以提出这个事件”。那是什么意思?只要调用方法可以访问使用事件的代码中的实例,任何人都不能调用吗?RaiseEventanimal
12赞 faby 8/28/2014
@Sung事件只能从课堂内部升起,也许我没有清楚地解释这一点。对于事件,您可以调用引发事件的函数(封装),但它只能从定义它的类内部引发。如果我不清楚,请告诉我。
1赞 chtenb 12/17/2015
“无法直接分配事件。”除非我理解错了,否则这不是真的。下面是一个示例:gist.github.com/Chiel92/36bb3a2d2ac7dd511b96
4赞 Pap 8/18/2016
@faby,你的意思是,即使事件被宣布为公开,我仍然不能?animal.Run(this, new ArgsSpecial("Run faster");
2赞 Jim Balter 3/14/2017
@ChieltenBrinke 当然,该事件可以在班级成员中分配......但不是其他。
10赞 Miguel Gamboa 2/18/2015 #7

活动和代表之间是多么大的误解!!委托指定 TYPE(例如 或 does),而事件只是一种 MEMBER(例如字段、属性等)。而且,就像任何其他类型的成员一样,事件也具有类型。但是,对于事件,事件的类型必须由委托指定。例如,您不能声明由接口定义的类型的事件。classinterface

最后,我们可以做出以下观察:事件的类型必须由委托定义。这是事件和委托之间的主要关系,在 II.18 定义 ECMA-335 (CLI) 分区 I 到 VI 的事件一节中进行了描述:

在典型用法中,TypeSpec(如果存在)标识一个委托,其签名与传递给事件的 fire 方法的参数匹配。

但是,这一事实并不意味着事件使用支持委托字段。事实上,事件可以使用您选择的任何不同数据结构类型的支持字段。如果在 C# 中显式实现事件,则可以自由选择存储事件处理程序的方式(请注意,事件处理程序是事件类型的实例,而事件处理程序又是强制委托类型---来自前面的观察)。但是,您可以将这些事件处理程序(即委托实例)存储在数据结构(如 a 或 a 或任何其他结构)中,甚至可以存储在支持委托字段中。但不要忘记,使用委托字段并不是强制性的。ListDictionary

评论

0赞 NeoZoom.lua 11/19/2020
我正在考虑是否可以说一个事件可能是许多代表的复合体。
0赞 Miguel Gamboa 11/19/2020
恕我直言,这种说法非常具有误导性,因为声明变量是一个对象。变量 STORES 对对象的引用。而不是对象本身。关于你的陈述“一个事件可能是许多代表的复合体”。好吧,事实并非如此。事件可以存储对委托实例的引用,而委托实例又可能是“可能由许多委托组成的组合”。
0赞 NeoZoom.lua 11/20/2020
那么委托实例本身是被设计成一个复合的吗?(复合图案)
8赞 Trevor 4/13/2015 #8

注意:如果您有权访问 C# 5.0 Unleashed,请阅读第 18 章中标题为“事件”的“对委托的简单使用限制”,以更好地了解两者之间的区别。


有一个简单、具体的例子总是对我有帮助。所以这里有一个给社区的。首先,我将展示如何单独使用委托来执行 Events 为我们所做的工作。然后,我将演示相同的解决方案如何与 的实例一起使用。然后我解释为什么我们不想做我在第一个例子中解释的事情。这篇文章的灵感来自约翰·斯基特(John Skeet)的一篇文章EventHandler

示例 1:使用公共委托

假设我有一个带有单个下拉框的 WinForms 应用程序。下拉列表绑定到 .其中 Person 具有 Id、Name、NickName、HairColor 属性。主窗体上是一个自定义用户控件,用于显示该人员的属性。当有人在下拉列表中选择某个人员时,用户控件中的标签将更新以显示所选人员的属性。List<Person>

enter image description here

这是如何工作的。我们有三个文件可以帮助我们把它放在一起:

  • Mediator.cs -- 静态类保存委托
  • Form1.cs -- 主窗体
  • DetailView.cs -- 用户控件显示所有详细信息

下面是每个类的相关代码:

class Mediator
{
    public delegate void PersonChangedDelegate(Person p); //delegate type definition
    public static PersonChangedDelegate PersonChangedDel; //delegate instance. Detail view will "subscribe" to this.
    public static void OnPersonChanged(Person p) //Form1 will call this when the drop-down changes.
    {
        if (PersonChangedDel != null)
        {
            PersonChangedDel(p);
        }
    }
}

以下是我们的用户控件:

public partial class DetailView : UserControl
{
    public DetailView()
    {
        InitializeComponent();
        Mediator.PersonChangedDel += DetailView_PersonChanged;
    }

    void DetailView_PersonChanged(Person p)
    {
        BindData(p);
    }

    public void BindData(Person p)
    {
        lblPersonHairColor.Text = p.HairColor;
        lblPersonId.Text = p.IdPerson.ToString();
        lblPersonName.Text = p.Name;
        lblPersonNickName.Text = p.NickName;

    }
}

最后,我们在 Form1.cs 中有以下代码。这里我们调用 OnPersonChanged,它调用订阅给委托的任何代码。

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    Mediator.OnPersonChanged((Person)comboBox1.SelectedItem); //Call the mediator's OnPersonChanged method. This will in turn call all the methods assigned (i.e. subscribed to) to the delegate -- in this case `DetailView_PersonChanged`.
}

还行。因此,这就是您在不使用事件而仅使用委托的情况下实现它的方式。我们只是把一个公共委托放到一个类中——你可以把它变成静态的,或者是单例的,或者其他什么。伟大。

但是,但是,但是,我们不想做我刚才描述的。因为公共领域有很多很多原因。那么我们有哪些选择呢?正如 John Skeet 所描述的,以下是我们的选择:

  1. 一个公共委托变量(这就是我们刚才在上面所做的,不要这样做,我只是在上面告诉过你为什么它不好)
  2. 将委托放入带有 get/set 的属性中(这里的问题是订阅者可以相互覆盖——所以我们可以为委托订阅一堆方法,然后我们可能会不小心说 ,抹去所有其他订阅。这里仍然存在的另一个问题是,由于用户有权访问委托,因此他们可以调用调用列表中的目标 -- 我们不希望外部用户有权访问何时引发我们的事件。PersonChangedDel = null
  3. 具有 AddXXXHandler 和 RemoveXXXHandler 方法的委托变量

这第三种选择本质上是事件给我们的。当我们声明一个 EventHandler 时,它为我们提供了对委托的访问权——不是公开的,不是作为属性,而是作为我们称之为刚刚添加/删除访问器的事件。

让我们看看同一个程序是什么样子的,但现在使用事件而不是公共委托(我还将我们的调解器更改为单例):

示例 2:使用 EventHandler 而不是公共委托

调解人:

class Mediator
{

    private static readonly Mediator _Instance = new Mediator();

    private Mediator() { }

    public static Mediator GetInstance()
    {
        return _Instance;
    }

    public event EventHandler<PersonChangedEventArgs> PersonChanged; //this is just a property we expose to add items to the delegate.

    public void OnPersonChanged(object sender, Person p)
    {
        var personChangedDelegate = PersonChanged as EventHandler<PersonChangedEventArgs>;
        if (personChangedDelegate != null)
        {
            personChangedDelegate(sender, new PersonChangedEventArgs() { Person = p });
        }
    }
}

请注意,如果对 EventHandler 按 F12,它将显示定义只是一个带有额外“sender”对象的泛型委托:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

用户控件:

public partial class DetailView : UserControl
{
    public DetailView()
    {
        InitializeComponent();
        Mediator.GetInstance().PersonChanged += DetailView_PersonChanged;
    }

    void DetailView_PersonChanged(object sender, PersonChangedEventArgs e)
    {
        BindData(e.Person);
    }

    public void BindData(Person p)
    {
        lblPersonHairColor.Text = p.HairColor;
        lblPersonId.Text = p.IdPerson.ToString();
        lblPersonName.Text = p.Name;
        lblPersonNickName.Text = p.NickName;

    }
}

最后,下面是 Form1.cs 代码:

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
        Mediator.GetInstance().OnPersonChanged(this, (Person)comboBox1.SelectedItem);
}

由于 EventHandler 希望 EventArgs 作为参数,因此我创建了这个类,其中只有一个属性:

class PersonChangedEventArgs
{
    public Person Person { get; set; }
}

希望这能向你展示一些关于我们为什么有活动,以及它们与代表的不同之处——但在功能上是相同的。

评论

0赞 Ivaylo Slavov 5/27/2015
虽然我很欣赏这篇文章中的所有优秀工作,并且我喜欢阅读其中的大部分内容,但我仍然觉得有一个问题没有得到解决——.在最新版本的 中,只要有对单例的引用,您仍然可以调用 。也许你应该提到,这种方法不会阻止这种特定行为,并且更接近于事件总线。The other problem that remains here is that since the users have access to the delegate, they can invoke the targets in the invocation list -- we don't want external users having access to when to raise our eventsMediatorOnPersonChangeMediator
2赞 Venkatesh Muniyandi 9/13/2016 #9

要以简单的方式定义 about 事件:

Event 是对具有两个限制的委托的 REFERENCE

  1. 不能直接调用
  2. 不能直接赋值(例如 eventObj = delegateMethod)

以上两个是代表的弱点,在事件中得到解决。显示 fiddler 差异的完整代码示例在这里 https://dotnetfiddle.net/5iR3fB

在“事件”和“委托”以及调用/分配要委托的值的客户端代码之间切换注释,以了解差异

下面是内联代码。

 /*
This is working program in Visual Studio.  It is not running in fiddler because of infinite loop in code.
This code demonstrates the difference between event and delegate
        Event is an delegate reference with two restrictions for increased protection

            1. Cannot be invoked directly
            2. Cannot assign value to delegate reference directly

Toggle between Event vs Delegate in the code by commenting/un commenting the relevant lines
*/

public class RoomTemperatureController
{
    private int _roomTemperature = 25;//Default/Starting room Temperature
    private bool _isAirConditionTurnedOn = false;//Default AC is Off
    private bool _isHeatTurnedOn = false;//Default Heat is Off
    private bool _tempSimulator = false;
    public  delegate void OnRoomTemperatureChange(int roomTemperature); //OnRoomTemperatureChange is a type of Delegate (Check next line for proof)
    // public  OnRoomTemperatureChange WhenRoomTemperatureChange;// { get; set; }//Exposing the delegate to outside world, cannot directly expose the delegate (line above), 
    public  event OnRoomTemperatureChange WhenRoomTemperatureChange;// { get; set; }//Exposing the delegate to outside world, cannot directly expose the delegate (line above), 

    public RoomTemperatureController()
    {
        WhenRoomTemperatureChange += InternalRoomTemperatuerHandler;
    }
    private void InternalRoomTemperatuerHandler(int roomTemp)
    {
        System.Console.WriteLine("Internal Room Temperature Handler - Mandatory to handle/ Should not be removed by external consumer of ths class: Note, if it is delegate this can be removed, if event cannot be removed");
    }

    //User cannot directly asign values to delegate (e.g. roomTempControllerObj.OnRoomTemperatureChange = delegateMethod (System will throw error)
    public bool TurnRoomTeperatureSimulator
    {
        set
        {
            _tempSimulator = value;
            if (value)
            {
                SimulateRoomTemperature(); //Turn on Simulator              
            }
        }
        get { return _tempSimulator; }
    }
    public void TurnAirCondition(bool val)
    {
        _isAirConditionTurnedOn = val;
        _isHeatTurnedOn = !val;//Binary switch If Heat is ON - AC will turned off automatically (binary)
        System.Console.WriteLine("Aircondition :" + _isAirConditionTurnedOn);
        System.Console.WriteLine("Heat :" + _isHeatTurnedOn);

    }
    public void TurnHeat(bool val)
    {
        _isHeatTurnedOn = val;
        _isAirConditionTurnedOn = !val;//Binary switch If Heat is ON - AC will turned off automatically (binary)
        System.Console.WriteLine("Aircondition :" + _isAirConditionTurnedOn);
        System.Console.WriteLine("Heat :" + _isHeatTurnedOn);

    }

    public async void SimulateRoomTemperature()
    {
        while (_tempSimulator)
        {
            if (_isAirConditionTurnedOn)
                _roomTemperature--;//Decrease Room Temperature if AC is turned On
            if (_isHeatTurnedOn)
                _roomTemperature++;//Decrease Room Temperature if AC is turned On
            System.Console.WriteLine("Temperature :" + _roomTemperature);
            if (WhenRoomTemperatureChange != null)
                WhenRoomTemperatureChange(_roomTemperature);
            System.Threading.Thread.Sleep(500);//Every second Temperature changes based on AC/Heat Status
        }
    }

}

public class MySweetHome
{
    RoomTemperatureController roomController = null;
    public MySweetHome()
    {
        roomController = new RoomTemperatureController();
        roomController.WhenRoomTemperatureChange += TurnHeatOrACBasedOnTemp;
        //roomController.WhenRoomTemperatureChange = null; //Setting NULL to delegate reference is possible where as for Event it is not possible.
        //roomController.WhenRoomTemperatureChange.DynamicInvoke();//Dynamic Invoke is possible for Delgate and not possible with Event
        roomController.SimulateRoomTemperature();
        System.Threading.Thread.Sleep(5000);
        roomController.TurnAirCondition (true);
        roomController.TurnRoomTeperatureSimulator = true;

    }
    public void TurnHeatOrACBasedOnTemp(int temp)
    {
        if (temp >= 30)
            roomController.TurnAirCondition(true);
        if (temp <= 15)
            roomController.TurnHeat(true);

    }
    public static void Main(string []args)
    {
        MySweetHome home = new MySweetHome();
    }


}
5赞 Weidong Shen 1/14/2019 #10

Delegate 是一个类型安全的函数指针。Event 是使用委托的发布者-订阅者设计模式的实现。

2赞 NeoZoom.lua 11/20/2020 #11

对于生活在 2020 年并想要一个干净答案的人们来说......

定义:

  • delegate:定义函数指针。
  • event:定义
    • (1) 受保护的接口,以及
    • (2) 操作(+=-=),以及
    • (3)优点:您不再需要使用关键字。new

关于形容词 protected

// eventTest.SomeoneSay = null;              // Compile Error.
// eventTest.SomeoneSay = new Say(SayHello); // Compile Error.

另请注意Microsoft中的这一部分:https://learn.microsoft.com/en-us/dotnet/standard/events/#raising-multiple-events

代码示例:

跟:delegate

public class DelegateTest
{
    public delegate void Say(); // Define a pointer type "void <- ()" named "Say".
    private Say say;

    public DelegateTest() {
        say  = new Say(SayHello);     // Setup the field, Say say, first.
        say += new Say(SayGoodBye);
        
        say.Invoke();
    }
    
    public void SayHello() { /* display "Hello World!" to your GUI. */ }
    public void SayGoodBye() { /* display "Good bye!" to your GUI. */ }
}

跟:event

public class EventTest
{
    public delegate void Say();
    public event Say SomeoneSay;  // Use the type "Say" to define event, an 
                                  // auto-setup-everything-good field for you.
    public EventTest() {
         SomeoneSay += SayHello;
         SomeoneSay += SayGoodBye;

         SomeoneSay();
    }
    
    public void SayHello() { /* display "Hello World!" to your GUI. */ }
    public void SayGoodBye() { /* display "Good bye!" to your GUI. */ }
}

参考:

事件与委托 - 解释 C# 中事件和委托模式之间的重要区别以及它们为何有用 https://dzone.com/articles/event-vs-delegate