了解C#中的事件和事件处理程序


329

我了解事件的目的,尤其是在创建用户界面的上下文中。我认为这是创建事件的原型:

public void EventName(object sender, EventArgs e);

事件处理程序做什么,为什么需要它们,以及如何创建事件处理程序?


9
如@Andy所述,此处的代码段描述了注册到事件的方法,而不是事件本身。
dthrasher


Answers:


660

要了解事件处理程序,您需要了解委托。在C#中,您可以将委托视为方法的指针(或引用)。这很有用,因为指针可以作为值传递。

代表的中心概念是其签名或形状。即(1)返回类型和(2)输入参数。例如,如果我们创建一个委托void MyDelegate(object sender, EventArgs e),则它只能指向return void并采用objectand的方法EventArgs。有点像方孔和方钉。因此,我们说这些方法与委托具有相同的签名或形状。

因此,了解了如何创建对方法的引用之后,我们来考虑事件的目的:当系统中其他地方发生事件时,我们希望使某些代码得以执行-或“处理事件”。为此,我们为要执行的代码创建特定的方法。事件和要执行的方法之间的粘合是委托。该事件必须在内部存储指向引发该事件时要调用的方法的指针的“列表”。*当然,要能够调用方法,我们需要知道要传递给它的参数!我们使用委托作为事件与将要调用的所有特定方法之间的“契约”。

因此,默认值EventHandler(以及许多类似的默认值)表示方法特定形状(再次是void / object-EventArgs)。声明事件时,您通过指定委托来说明事件将调用哪种形式的方法(EventHandler):

//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyEventHandler(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyEventHandler SomethingHappened;

//Here is some code I want to be executed
//when SomethingHappened fires.
void HandleSomethingHappened(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

//To raise the event within a method.
SomethingHappened("bar");

(*这是.NET中事件的关键,它消除了“魔术”-实际上,一个事件实际上只是一个具有相同“形状”的方法列表。该列表存储在事件所在的位置。何时事件是“引发的”,实际上只是“遍历此方法列表并使用这些值作为参数来调用每个方法”。分配事件处理程序只是将方法添加到此方法列表中的一种更简便的方法被称为)。


24
现在任何人都可以解释为什么该事件称为EventHandler?在所有令人困惑的命名约定中,这是最糟糕的...
乔尔(Joel)在2009年

37
Go中的@Joel事件不称为EventHandler-EventHandler是事件与与之通信的任何人必须具有的合同。就像“字符串MyString”-字符串在声明类型。事件MyEventHandler TheEvent声明与该事件进行交互的任何人都必须遵守MyEventHandler合同。Handler约定是因为合同主要描述了如何处理事件。
Rex M 2009年

18
该事件如何触发?
炼金术

17
@Rex M:谢谢您对我见过的“ MyEventHandler”的第一个连贯的解释:)
Joel在2010年

10
谢谢您的阶段:“事件和要执行的方法之间的纽带是委托。”,这真的很棒。
zionpi

103

C#知道两个术语,delegateevent。让我们从第一个开始。

代表

A delegate是对方法的引用。就像您可以创建对实例的引用一样:

MyClass instance = myFactory.GetInstance();

您可以使用委托创建对方法的引用:

Action myMethod = myFactory.GetInstance;

现在,您已经有了对方法的引用,您可以通过引用来调用该方法:

MyClass instance = myMethod();

但是你为什么呢?您也可以直接致电myFactory.GetInstance()。在这种情况下,您可以。但是,有许多情况需要考虑,您不希望在哪里让应用程序的其余部分了解myFactory或调用myFactory.GetInstance()直接。

一个明显的例子是,如果您希望能够从一个中心位置替换myFactory.GetInstance()myOfflineFakeFactory.GetInstance()另一种(又称工厂方法模式)。

工厂方法模式

因此,如果您有一个TheOtherClass类并且需要使用myFactory.GetInstance(),则这是没有委托的情况下代码的样子(您需要让您TheOtherClass知道的类型myFactory):

TheOtherClass toc;
//...
toc.SetFactory(myFactory);


class TheOtherClass
{
   public void SetFactory(MyFactory factory)
   {
      // set here
   }

}

如果使用委托,则不必公开我的工厂的类型:

TheOtherClass toc;
//...
Action factoryMethod = myFactory.GetInstance;
toc.SetFactoryMethod(factoryMethod);


class TheOtherClass
{
   public void SetFactoryMethod(Action factoryMethod)
   {
      // set here
   }

}

因此,您可以给其他类使用一个委托,而不必将您的类型暴露给其他类。您唯一要公开的是方法的签名(您拥有多少个参数等等)。

“我的方法的签名”,我以前在哪里听过?是的,接口!!!接口描述整个类的签名。认为代表只是描述一种方法的签名!

接口和委托之间的另一个大区别是,在编写类时,不必对C#说“此方法实现了那种委托”。对于接口,您确实需要说“此类实现了该类型的接口”。

此外,委托引用可以(有一些限制,请参见下文)引用多种方法(称为 MulticastDelegate)。这意味着,当您调用委托时,将执行多个显式附加的方法。对象引用始终只能引用一个对象。

对a的限制MulticastDelegate是(方法/委托)签名不应具有任何返回值(void)和关键字outref并且不能在签名中使用。显然,您不能调用两个返回数字并期望它们返回相同数字的方法。签名符合后,委托将自动为MulticastDelegate

事件

事件只是属性(例如get; set;实例字段的属性),它公开了其他对象对委托的订阅。但是,这些属性不支持get; set;。相反,他们支持添加;去掉;

因此,您可以:

    Action myField;

    public event Action MyProperty
    {
        add { myField += value; }
        remove { myField -= value; }
    }

UI中的用法(WinForms,WPF,UWP等)

因此,现在我们知道委托是对方法的引用,并且我们可以有一个事件让全世界知道他们可以向我们提供要从委托中引用的方法,我们是一个UI按钮,然后:可以要求对我是否被点击感兴趣的任何人(通过我们公开的事件)向我们注册他们的方法。我们可以使用所有提供给我们的方法,并由我们的代表引用它们。然后,我们将等待,直到用户来单击该按钮,然后我们才有足够的理由来调用委托。并且由于该委托引用了提供给我们的所有那些方法,因此将调用所有这些方法。我们不知道这些方法的作用,也不知道哪个类实现了这些方法。我们只关心有人对我们的点击感兴趣,

爪哇

像Java这样的语言没有委托。他们改用接口。他们这样做的方法是,请对“我们被点击”感兴趣的任何人实现一个特定的接口(使用可以调用的特定方法),然后为我们提供实现该接口的整个实例。我们保留实现此接口的所有对象的列表,并在单击时可以调用它们的“可以调用的某些方法”。


解释很高兴,但事件与接受订阅者的委托实例有何不同?他们看起来都一样吗?
BKSpurgeon '16

@BKSpurgeon是因为它们 “接受订户的代理”- event仅仅是语法糖,仅此而已。
Mathieu Guindon

“对于MulticastDelegate的限制是(方法/代理)签名不应具有任何返回值(void)”,我认为这是不正确的。如果它们有返回值,它将返回最后一个。
Hozikimaru

“因此,您可以给其他类使用一个委托,而不必将您的类型暴露给他们。唯一要暴露的是方法的签名……” -对我来说,这是关键。谢谢!
莱恩(Ryan)

40

这实际上是事件处理程序的声明-事件触发时将调用该方法。要创建事件,您需要编写以下内容:

public class Foo
{
    public event EventHandler MyEvent;
}

然后您可以订阅这样的活动:

Foo foo = new Foo();
foo.MyEvent += new EventHandler(this.OnMyEvent);

使用这样定义的OnMyEvent():

private void OnMyEvent(object sender, EventArgs e)
{
    MessageBox.Show("MyEvent fired!");
}

每当Foo触发时MyEventOnMyEvent就会调用您的处理程序。

您不必总是使用的实例EventArgs作为第二个参数。如果要包括其他信息,则可以使用派生自的类EventArgsEventArgs按惯例是基础)。例如,如果您查看ControlWinForms或FrameworkElementWPF中定义的某些事件,则可以看到将附加信息传递给事件处理程序的事件示例。


14
感谢您回答问题,不参加代表和活动。
Dived_byzero 2012年

3
我建议不要OnXXX为事件处理程序使用命名模式。(愚蠢的是,OnXXX在MFC中被表示为“ handle XXX”,而在.net中则被称为“ raise XXX”,因此,其含义尚不清楚且令人困惑- 有关详细信息,请参见此文章)。首选名称是RaiseXXX引发事件,HandleXXXSender_XXX用于事件处理程序。
杰森·威廉姆斯

1
您可以用一个简单的WinForms应用程序显示一个工作示例吗?
MC9000

40

这是一个可能有帮助的代码示例:

using System;
using System.Collections.Generic;
using System.Text;

namespace Event_Example
{
  // First we have to define a delegate that acts as a signature for the
  // function that is ultimately called when the event is triggered.
  // You will notice that the second parameter is of MyEventArgs type.
  // This object will contain information about the triggered event.

  public delegate void MyEventHandler(object source, MyEventArgs e);

  // This is a class which describes the event to the class that receives it.
  // An EventArgs class must always derive from System.EventArgs.

  public class MyEventArgs : EventArgs
  {
    private string EventInfo;

    public MyEventArgs(string Text) {
      EventInfo = Text;
    }

    public string GetInfo() {
      return EventInfo;
    }
  }

  // This next class is the one which contains an event and triggers it
  // once an action is performed. For example, lets trigger this event
  // once a variable is incremented over a particular value. Notice the
  // event uses the MyEventHandler delegate to create a signature
  // for the called function.

  public class MyClass
  {
    public event MyEventHandler OnMaximum;

    private int i;
    private int Maximum = 10;

    public int MyValue
    {
      get { return i; }
      set
      {
        if(value <= Maximum) {
          i = value;
        }
        else 
        {
          // To make sure we only trigger the event if a handler is present
          // we check the event to make sure it's not null.
          if(OnMaximum != null) {
            OnMaximum(this, new MyEventArgs("You've entered " +
              value.ToString() +
              ", but the maximum is " +
              Maximum.ToString()));
          }
        }
      }
    }
  }

  class Program
  {
    // This is the actual method that will be assigned to the event handler
    // within the above class. This is where we perform an action once the
    // event has been triggered.

    static void MaximumReached(object source, MyEventArgs e) {
      Console.WriteLine(e.GetInfo());
    }

    static void Main(string[] args) {
      // Now lets test the event contained in the above class.
      MyClass MyObject = new MyClass();
      MyObject.OnMaximum += new MyEventHandler(MaximumReached);
      for(int x = 0; x <= 15; x++) {
        MyObject.MyValue = x;
      }
      Console.ReadLine();
    }
  }
}

4
委托调用可以在C#6中简化为:OnMaximum?.Invoke(this,new MyEventArgs("you've entered..."));
Tim Schmelter,2016年

23

只是为了增加现有的好答案-在接受的代码的基础上,使用delegate void MyEventHandler(string foo)...

由于编译器知道SomethingHappened事件的委托类型,因此:

myObj.SomethingHappened += HandleSomethingHappened;

完全等同于:

myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

而且处理器也可以是未注册使用-=这样的:

// -= removes the handler from the event's list of "listeners":
myObj.SomethingHappened -= HandleSomethingHappened;

为了完整起见,仅在拥有事件的类中才能引发事件:

//Firing the event is done by simply providing the arguments to the event:
var handler = SomethingHappened; // thread-local copy of the event
if (handler != null) // the event is null if there are no listeners!
{
    handler("Hi there!");
}

需要处理程序的线程本地副本,以确保调用是线程安全的-否则,在检查了事件是否存在之后,线程可以转到该事件并立即注销该事件的最后一个处理程序,在那里null我们将获得一个“乐趣” NullReferenceException


C#6为这种模式引入了很好的捷径。它使用空传播运算符。

SomethingHappened?.Invoke("Hi there!");

13

我对这些事件的理解是:

代表:

用于保存对要执行的方法的引用的变量。这样就可以传递变量之类的方法。

创建和调用事件的步骤:

  1. 该事件是委托的实例

  2. 由于事件是委托的实例,因此我们必须首先定义委托。

  3. 分配触发事件时要执行的方法(调用委托

  4. 触发事件(呼叫代表

例:

using System;

namespace test{
    class MyTestApp{
        //The Event Handler declaration
        public delegate void EventHandler();

        //The Event declaration
        public event EventHandler MyHandler;

        //The method to call
        public void Hello(){
            Console.WriteLine("Hello World of events!");
        }

        public static void Main(){
            MyTestApp TestApp = new MyTestApp();

            //Assign the method to be called when the event is fired
            TestApp.MyHandler = new EventHandler(TestApp.Hello);

            //Firing the event
            if (TestApp.MyHandler != null){
                TestApp.MyHandler();
            }
        }

    }   

}

3

发布者:事件发生的位置。发布者应指定该类正在使用哪个委托并生成必要的参数,然后将这些参数及其自身传递给该委托。

订户:响应发生的位置。订户应指定响应事件的方法。这些方法应采用与委托相同的参数类型。订阅者然后将此方法添加到发布者的委托中。

因此,当事件在发布者中发生时,委托将收到一些事件参数(数据等),但是发布者不知道所有这些数据将发生什么。订阅者可以在自己的类中创建方法以响应发布者类中的事件,以便订阅者可以响应发布者的事件。


2
//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyDelegate(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyDelegate MyEvent;

//Here is some code I want to be executed
//when SomethingHappened fires.
void MyEventHandler(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.MyEvent += new MyDelegate (MyEventHandler);

0

我同意KE50,除了我将'event'关键字视为'ActionCollection'的别名之外,因为事件包含要执行的动作(即委托)的集合,所以我同意。

using System;

namespace test{

class MyTestApp{
    //The Event Handler declaration
    public delegate void EventAction();

    //The Event Action Collection 
    //Equivalent to 
    //  public List<EventAction> EventActions=new List<EventAction>();
    //        
    public event EventAction EventActions;

    //An Action
    public void Hello(){
        Console.WriteLine("Hello World of events!");
    }
    //Another Action
    public void Goodbye(){
        Console.WriteLine("Goodbye Cruel World of events!");
    }

    public static void Main(){
        MyTestApp TestApp = new MyTestApp();

        //Add actions to the collection
        TestApp.EventActions += TestApp.Hello;
        TestApp.EventActions += TestApp.Goodbye;

        //Invoke all event actions
        if (TestApp.EventActions!= null){
            //this peculiar syntax hides the invoke 
            TestApp.EventActions();
            //using the 'ActionCollection' idea:
            // foreach(EventAction action in TestApp.EventActions)
            //     action.Invoke();
        }
    }

}   

}

0

帖子中提供了很棒的技术解答!从技术上讲,我没有什么要补充的。

通常,新功能会在语言和软件中出现的主要原因之一是市场营销或公司政治!:-)不可低估!

我认为这也适用于代表和事件的某些扩展!我发现它们很有用,并为C#语言增添了价值,但另一方面,Java语言决定不使用它们!他们认为,无论您用委托人解决什么问题,您都可以使用该语言的现有功能(例如接口)来解决

大约在2001年左右,Microsoft发布了.NET框架和C#语言作为Java的竞争对手解决方案,因此拥有Java没有的新功能非常好。


0

我最近举了一个如何在c#中使用事件的示例,并将其发布在我的博客上。我试图通过一个非常简单的示例使它尽可能清晰。万一它可以帮助任何人,请访问:http : //www.konsfik.com/using-events-in-csharp/

它包括描述和源代码(带有大量注释),并且主要关注事件和事件处理程序的正确使用(类似于模板)。

一些关键点是:

  • 事件就像“子类型的代表”,只是受到更多限制(以一种很好的方式)。实际上,事件的声明始终包含委托(EventHandlers是委托的一种)。

  • 事件处理程序是特定类型的委托人(您可能将它们视为模板),它迫使用户创建具有特定“签名”的事件。签名的格式为:(对象发送者,EventArgs eventarguments)。

  • 您可以创建自己的EventArgs子类,以包括事件需要传达的任何类型的信息。使用事件时不必使用EventHandlers。您可以完全跳过它们,并使用自己的代理代替它们。

  • 使用事件和委托之间的主要区别在于,即使可以将事件声明为公共事件,也只能从声明它们的类中调用事件。这是一个非常重要的区别,因为它允许您公开事件,以便将它们“连接”到外部方法,同时保护它们免受“外部滥用”。


0

要了解的另一件事,在某些情况下,当您需要低级别的耦合时必须使用“委托/事件”

如果要在应用程序中的多个位置使用组件,则需要使组件具有较低的耦合级别,并且必须将特定的无关LOGIC委托给外部您的组件!这样可以确保您具有解耦的系统和更简洁的代码。

按照SOLID原理,这是“ D ”,(D依赖倒置原则)。

也称为“ IoC ”,控制反转

您可以使用事件,代表和DI来创建“ IoC(依赖注入)。

在子类中访问方法很容易。但是从子级访问父类中的方法更加困难。您必须将父代参考传递给孩子!(或与接口一起使用DI)

委托/活动使我们无需参考即可从孩子与父母沟通!

在此处输入图片说明

在上面的图中,我不使用Delegate / Event,并且父组件B 必须具有父组件A 的引用才能执行A方法中无关的业务逻辑(高级耦合)。

使用这种方法,我将不得不放置所有使用组件B的组件的所有引用!:(

在此处输入图片说明

在上面的图表中,我使用了Delegate / Event,而组件B不必知道A。(低耦合度)

您可以在应用程序中的任何位置使用B组件!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.