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

Posted

技术标签:

【中文标题】了解 C# 中的事件和事件处理程序【英文标题】:Understanding events and event handlers in C# 【发布时间】:2022-01-15 07:28:29 【问题描述】:

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

public void EventName(object sender, EventArgs e);

事件处理程序有什么作用,为什么需要它们,如何创建?

【问题讨论】:

【参考方案1】:

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

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

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

所以默认的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 中事件的关键,它揭开了“魔法”的面纱——在幕后,事件实际上只是一个具有相同“形状”的方法的列表。该列表存储在事件所在的位置当事件被“引发”时,它实际上只是“通过这个方法列表并调用每个方法,使用这些值作为参数”。分配一个事件处理程序只是一种更漂亮、更简单的方法来添加你的方法要调用的方法列表)。

【讨论】:

【参考方案2】:

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

委托

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 知道你的 @ 类型987654335@):

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)。这意味着当您调用委托时,将执行多个显式附加的方法。一个对象引用总是只能引用一个对象。

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

像 Java 这样的语言没有委托。他们使用接口代替。他们这样做的方式是让任何对“我们被点击”感兴趣的人实现某个接口(使用我们可以调用的某个方法),然后给我们实现该接口的整个实例。我们保留了实现此接口的所有对象的列表,并且只要我们被点击,就可以调用它们的“我们可以调用的特定方法”。

【讨论】:

【参考方案3】:

这实际上是一个事件处理程序的声明——一个在事件被触发时将被调用的方法。要创建一个事件,你可以这样写:

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 触发MyEvent 时,您的OnMyEvent 处理程序就会被调用。

您不必总是使用EventArgs 的实例作为第二个参数。如果要包含其他信息,可以使用派生自EventArgs 的类(EventArgs 是约定的基类)。例如,如果您查看在 WinForms 中的 Control 或 WPF 中的 FrameworkElement 上定义的一些事件,您可以看到将附加信息传递给事件处理程序的事件示例。

【讨论】:

【参考方案4】:

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

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();
    
  

【讨论】:

【参考方案5】:

只是为了在此处添加现有的出色答案 - 基于已接受的代码中的代码构建,该代码使用 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!");

【讨论】:

【参考方案6】:

我对事件的理解是;

代表:

保存对要执行的方法/方法的引用的变量。这使得传递变量之类的方法成为可能。

创建和调用事件的步骤:

    事件是委托的一个实例

    既然事件是委托的一个实例,那么我们必须先定义委托。

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

    触发事件(调用代理

示例:

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();
            
        

       


【讨论】:

【参考方案7】:
//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);

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

【参考方案9】:

我最近做了一个如何在 c# 中使用事件的示例,并将其发布在我的博客上。我试图用一个非常简单的例子来尽可能清楚地说明这一点。如果它可以帮助任何人,这里是:http://www.konsfik.com/using-events-in-csharp/

它包括描述和源代码(有很多 cmets),它主要关注事件和事件处理程序的正确(类似模板)使用。

一些关键点是:

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

事件处理程序是特定类型的委托(您可以将它们视为模板),它强制用户创建具有特定“签名”的事件。签名的格式为:(object sender, EventArgs eventarguments)。

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

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

【讨论】:

【参考方案10】:

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

如果你想在应用程序的多个地方使用一个组件,你需要制作一个低耦合的组件,并且必须委托特定的不关心的LOGIC OUTSIDE!这可确保您拥有解耦的系统和更简洁的代码。

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

也称为“IoC”、控制反转

您可以使用 Events、Delegates 和 DI(依赖注入)制作“IoC”。

访问子类中的方法很容易。但更难从子类访问父类中的方法。您必须将父引用传递给子代! (或使用带接口的 DI)

委托/事件允许我们在没有参考的情况下从孩子与父母交流!

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

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

在上图中,我使用委托/事件,组件 B 不必知道 A。(低耦合度)

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

【讨论】:

【参考方案11】:

我同意 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();
        
    

   


【讨论】:

【参考方案12】:

帖子中有很棒的技术答案!我没有技术上的可以补充。

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

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

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

【讨论】:

【参考方案13】:

代表、事件(事件处理程序/事件侦听器)、概念(多播/广播)、动作和功能

这将是一个很长但最简单的解释,这个话题如此令人讨厌的问题是因为人们只是用不同的词来解释同一件事

首先,你应该知道一些事情

DELEGATES:它只是一个方法列表,为什么要创建一个列表?因为当你的代码被执行时,这个列表会被一个一个地执行,只要不要听教科书的定义就可以了,你会没事的

也叫:

指向函数的指针 方法的包装器,可以像变量一样发送和接收方法

去创建一个委托

[[access modifier] delegate [return type] [delegate name]([parameters])]

example: public delegate int demo(int a);

现在要执行存储在一个名为委托的列表中的所有这些方法,你去

1. demo.invoke(a);
2. demo(a);   ..... both are valid

在使用 beginInvoke 的异步编程中使用点并明确表示调用会大放异彩,但这超出了本主题的范围

还有一件事叫做“创建委托/实例化委托的对象”,这听起来很像,但只是为了避免混淆(对于上面的例子)

example : demo del = new demo(); (or) Public demo del = null;

要将任何方法添加到称为委托的列表中,您可以 += 并且您还需要在“满足方法的要求”后将其删除 -=

(满足方法的要求意味着您不再需要该方法处于活动状态或也称为“监听”)如果您不删除它,它可能会导致“内存泄漏”,这意味着您的计算机内存将被活着吃掉, 技术上分配的内存不会被释放

例子:说有一个方法

public int calculate (int c)

to add this method to delegate you go

1.  del = calculate;
2.  del += calculate; .... all are valid

to remove 

del -= calculate

首先注意委托和方法之间的相似之处,返回类型(输出)和输入/参数是相同的,这是一个规则,你不能在委托中添加任何随机或一堆方法它需要遵循输入输出规则

现在为什么有两种不同的方式来做一件事,唯一不同的是赋值运算符(+,=),这就引入了一个新的话题,叫做

活动

这不过是一个受约束的委托版本,它仍然是一个方法列表,当人们解释这些术语时不要混淆,他们会更改名称,所以坚持这个来理解

什么是约束?你不能这样做del = calculate; 它有什么害处,比如说一堆方法被添加到委托(列表)中,你这样做?所有的都被消灭了,只剩下一个方法“计算”,所以为了防止使用事件, 事件语法

公共事件演示 del = null;

您不能对事件做的另一件事是直接调用委托,如 demo.invoke,因为它是公共的,它可以被访问和调用,但对于事件,它不能

现在您只需将方法添加到事件(一种特殊类型的委托)

何时使用事件与委托,取决于您的情况,但实际上事件很受欢迎

几个关键词

MULTICASTING:向委托添加多个方法 广播:向事件添加多个方法

PUBLISHER:执行方法的那个(广播中使用的术语),只有一个实体 SUBSCRIBER:正在执行的方法,可以是多个

LISTENER:与订阅者相同,但该术语用于多播

事件处理程序:与订阅者/事件侦听器相同,那么有什么区别?基本上是一样的,有人说事件监听器检测到事件的发生,事件处理程序“处理”或执行代码,实际上是一样的!

action 和 func 只是已经创建和实例化的委托,所以一句话就两行代码,区别只是返回类型

ACTION: 接受 0 或多于 1 个输入时不返回任何内容

FUNC:返回一件事并接受参数

如果你不擅长阅读这里是关于这个主题的最佳视频

https://www.youtube.com/playlist?list=PLFt_AvWsXl0dliMtpZC8Qd_ru26785Ih_

【讨论】:

以上是关于了解 C# 中的事件和事件处理程序的主要内容,如果未能解决你的问题,请参考以下文章

C# 事件处理程序委托中的闭包? [复制]

如何删除事件处理程序并将其重新附加到 c# 中的控件?

C# 中简单的跨应用程序数据和事件传递

在 C++ 中触发事件并在 C# 中处理它们

关闭事件处理程序 C#

C#里事件和委托有啥区别啊??