事件和委托之间的区别及其各自的应用

110

除了语法糖之外,我并没有看到使用事件比委托的优势。也许我有误解,但似乎事件只是委托的占位符。

您能否向我解释一下它们之间的区别以及何时使用哪个?它们各自的优缺点是什么?我们的代码与事件密切相关,我想彻底了解它们。

在什么情况下会使用委托,何时使用事件?请说明您在实际生产代码中对两者的经验。


是的,理解它们之间的差异确实很困难,它们看起来相同,在第一眼看上去似乎也做着相同的事情。 - Robert Gould
1
请参考这个问题 - Dimitri C.
1
两个事件和委托之间的区别是事实问题,而非观点问题。问题要求各自的应用,因为它们展示了这些技术解决问题的差异。这也不是观点问题,因为没有人问哪个更好。这个问题的任何部分都不是观点问题,这个陈述也不是观点。在我看来。你拿到徽章了吗? - Peter Wone
10个回答

56
关键字event是多路广播委托的作用域修饰符。使用它和仅声明多路广播委托的实际区别如下:
  • 您可以在接口中使用event
  • 对多路广播委托的调用访问权限限于声明类。行为就像对于调用而言,委托是私有的。对于赋值,访问权限由显式访问修饰符指定(例如public event)。

有趣的是,您可以将+-应用于多路广播委托,这是将委托组合分配给事件的+=-=语法的基础。这三个片段是等价的:

B = new EventHandler(this.MethodB);
C = new EventHandler(this.MethodC);
A = B + C;

示例二,演示直接赋值和组合赋值的两种方式。

B = new EventHandler(this.MethodB);
C = new EventHandler(this.MethodC);
A = B;
A += C;

第三个示例:更常见的语法。你可能熟悉将null赋值给移除所有处理程序。

B = new EventHandler(this.MethodB);
C = new EventHandler(this.MethodC);
A = null;
A += B;
A += C;

就像属性一样,事件也有一个完整的语法,但从来没有人使用它:

class myExample 
{
  internal EventHandler eh;

  public event EventHandler OnSubmit 
  { 
    add 
    {
      eh = Delegate.Combine(eh, value) as EventHandler;
    }
    remove
    {
      eh = Delegate.Remove(eh, value) as EventHandler;
    }
  }

  ...
}

这段代码完全和这个做的一样:

class myExample 
{
  public event EventHandler OnSubmit;
}

在VB.NET中,由于没有运算符重载,因此add和remove方法在语法上更加显眼(有点生硬)。


6
“对于多路广播委托,调用访问被限制在声明类内部”- 对我来说,这是委托和事件之间的关键区别点。 - RichardOD
2
另一个重要的区别(由itowlson提到)是不能通过分配给事件来取消所有事件处理程序,但可以通过委托来实现。 (顺便说一下,你的回答对我来说是最有用的)。 - Roman Starkov
4
虽然 Google 和 stackoverflow 非常方便,但所有这些信息和更多内容都可以在微软免费公开的 C# 语言规范中找到,其中详细解释了每个细节。我知道乍一看,似乎上帝创造了手册,而 Jon Skeet 吞下了它,但还有其他副本 :) - Peter Wone

49

从技术角度来看,其他回答已经解答了它们的不同之处。

从语义学的角度来看,事件是当对象达到某些条件时引发的操作。例如,我的股票类有一个名为Limit的属性,当股票价格达到限制时会引发一个事件。这种通知是通过事件完成的。无论是否有人真正关心这个事件并订阅它,都超出了所有者类的关注范围。

委托(delegate)是一个更通用的术语,用于描述类似于C/C ++术语中指针的构造。.Net中的所有委托都是多路广播委托。从语义上讲,它们通常被用作一种输入方式。特别地,它们是实现策略模式的完美方法。例如,如果我想对对象列表进行排序,可以向该方法提供比较器策略以告诉实现如何比较两个对象。

我使用过这两种方法在生产代码中。我的大量数据对象都会在满足某些属性时通知。最基本的示例是,每当属性更改时,都会引发PropertyChanged事件(请参见INotifyPropertyChanged接口)。我还在代码中使用委托来提供将某些对象转换为字符串的不同策略。这个特殊的例子是一个将某个对象类型的ToString()方法实现列表显式给用户的示例。


4
也许我漏掉了什么,但是事件处理程序不是委托的一种类型吗? - Powerlord
1
我的回答涉及到问题编辑#1和#2,从使用角度来看的区别。就本次讨论而言,它们是不同的,尽管从技术角度来看,你是正确的。请查看其他答案以了解技术上的差异。 - Szymon Rozga
3
所有的 .Net 委托都是多路广播委托吗?即使返回值的委托也是吗? - Qwertie
5
是的。有关历史,请查看http://msdn.microsoft.com/en-us/magazine/cc301816.aspx。请查看:http://msdn.microsoft.com/en-us/library/system.delegate.aspx。如果它们返回值,则返回的值是链中最后一个委托的评估。 - Szymon Rozga
委托是引用类型,指向订阅类中定义的事件处理程序。换句话说,委托用作发布者中事件和订阅者中定义的事件处理程序之间的链接。在应用程序中,将有多个订阅者需要侦听事件,在这种情况下,委托为我们提供了一种有效的方式来链接发布者和订阅者。 - josepainumkal

12

事件是一种语法糖。它们非常好吃。当我看到一个事件时,我知道该怎么做。但当我看到委托时,就不太确定了。

将事件与接口(更多的糖)结合起来会制作出令人垂涎欲滴的小吃。而委托和纯虚拟抽象类则不那么可口。


我也是这么看的。我想要更深入、更甜美的解释 :) - Sasha
14
吃太多糖会导致发胖,但是... =P - Erik Forbes

5

在元数据中,事件被标记为事件。这使得Windows Forms或ASP.NET设计器可以区分事件和仅为委托类型的属性,并为它们提供适当的支持(特别是在属性窗口的“事件”选项卡上显示它们)。

与委托类型的属性不同的另一个区别是,用户只能添加和删除事件处理程序,而对于委托类型的属性,他们可以设置值:

someObj.SomeCallback = MyCallback;  // okay, replaces any existing callback
someObj.SomeEvent = MyHandler;  // not okay, must use += instead

这有助于隔离事件订阅者:我可以将我的处理程序添加到事件中,你可以将你的处理程序添加到同一事件中,而你不会意外地覆盖我的处理程序。

4

为了理解区别,您可以查看以下两个示例

使用委托的示例(在此情况下,使用不返回值的委托操作)

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

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

为了使用委托,您应该像这样做:

Animale 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类的类都可以通过调用animal.RaiseEvent()来引发RaiseEvent

为避免这些弱点,您可以在C#中使用事件

您的Animal类将以此方式更改

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

        public string Operation {get; set;}
   } 



 public class Animal
    {
       public event EventHandler<ArgsSpecial> Run = delegate{} //empty delegate. In this way you are sure that value is always != null because no one outside of the class can change it

       public void RaiseEvent()
       {  
          Run(this, new ArgsSpecial("Run faster"));
       }
    }

调用事件

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

区别:

  1. 你使用的是公共字段而不是公共属性(使用事件时,编译器会保护你的字段免受未经授权的访问)。
  2. 无法直接分配事件。在这种情况下,你无法像之前我展示的重写行为那样出现错误。
  3. 类外部的任何人都无法引发该事件。
  4. 事件可以包含在接口声明中,而字段则不能。

注释:

EventHandler被声明为以下委托:

public delegate void EventHandler (object sender, EventArgs e)

它需要一个发送者(Object类型)和事件参数。如果它来自静态方法,则发送器为null。

您也可以使用EventHAndler,而不是此示例中使用的EventHandler<ArgsSpecial>

请参见此处有关EventHandler的文档


4
虽然事件通常使用多播委托来实现,但并不要求以这种方式使用。如果一个类公开事件,那么该类就公开了两个方法。它们的含义本质上是:
1. 这里有一个委托,请在发生有趣的事情时调用它。 2. 这里有一个委托,请在方便的时候立即销毁所有对它的引用(不再调用它)。
一个类处理其公开的事件最常见的方式是定义一个多播委托,并添加/删除传递给上述方法的任何委托,但并不要求它们以这种方式工作。不幸的是,事件架构未能做一些使替代方法更加清晰的事情(例如让订阅方法返回一个MethodInvoker,由订阅者保留;要取消订阅事件,只需调用返回的方法),因此,多播委托是迄今为止最常见的方法。

4

编辑#1 您何时会使用委托而不是事件?请说明您在实际生产代码中对两者的经验。

当我设计自己的API时,我定义了委托,并将其作为参数传递给方法或类的构造函数:

  • 这样一个方法就可以实现简单的“模板方法”模式(例如,PredicateAction委托被传递给.Net通用集合类)
  • 或者这样一个类可以执行“回调”(通常是回调到创建它的类的方法)。

这些委托通常在运行时是必需的(即不能为null)。

我倾向于不使用事件;但是在我使用事件的情况下,我将其用于可选地零个、一个或多个客户端发出信号,这些客户端可能感兴趣,即当一个类存在并运行时,是否有任何客户端添加了事件处理程序到其事件上(例如,窗体的“鼠标按下”事件存在,但是是否有外部客户端有兴趣安装事件处理程序到该事件上是可选的)。


3
尽管我没有技术上的理由,但我在 UI 样式代码中使用事件,在代码的较高层级中使用它们,并在代码深层逻辑中使用委托。正如我所说,你可以使用其中任何一种,但如果没有其他理由,我发现这种使用模式在逻辑上是合理的,至少它有助于记录回调的类型及其层次结构。
编辑:我认为我使用的不同用法模式是,我认为忽略事件是完全可以接受的,它们只是钩子/存根,如果你需要知道事件信息,请监听它们,如果你不关心事件,就忽略它们。这就是为什么我在 UI 中使用它们,有点像 JavaScript/浏览器事件样式。然而,当我有一个委托时,我真的希望有人处理委托的任务,如果没有处理,就会抛出异常。

你能详细说明一下吗?因为我在UI中也使用了事件。一个好的例子就足够了...谢谢。 - Sasha

3

3
欢迎来到Stack Overflow!虽然这理论上回答了问题,但最好在此处包含答案的关键部分,并提供参考链接。 - GhostCat

1
如果我们仅使用委托来替代事件,那么订阅者就有机会像下面的图像所示那样对委托进行clone()、invoke()操作,这是不正确的。

enter image description here

这就是事件和委托之间的主要区别。订阅者只有一个权利,即监听事件。 ConsoleLog类通过EventLogHandler订阅日志事件。
public class ConsoleLog
{
    public ConsoleLog(Operation operation)
    {
        operation.EventLogHandler += print;
    }

    public void print(string str)
    {
        Console.WriteLine("write on console : " + str);
    }
}
类通过订阅日志事件。
public class FileLog
{
    public FileLog(Operation operation)
    {
        operation.EventLogHandler += print;
    }

    public void print(string str)
    {
        Console.WriteLine("write in File : " + str);
    }
}

操作类正在发布日志事件。
public delegate void logDelegate(string str);
public class Operation
{
    public event logDelegate EventLogHandler;
    public Operation()
    {
        new FileLog(this);
        new ConsoleLog(this);
    }

    public void DoWork()
    {
        EventLogHandler.Invoke("somthing is working");
    }
}

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