357 Stimmen

Das Verständnis von Ereignissen und Ereignishandlern in C#

Ich verstehe den Zweck von Ereignissen, insbesondere im Zusammenhang mit der Erstellung von Benutzeroberflächen. Ich denke, dies ist das Prototyp für die Erstellung eines Ereignisses:

public void EventName(object sender, EventArgs e);

Was machen Ereignishandler, warum werden sie benötigt und wie erstelle ich einen?

709voto

Rex M Punkte 138455

Um Ereignisbehandler zu verstehen, müssen Sie Delegaten verstehen. In C# kann ein Delegat als ein Zeiger (oder eine Referenz) auf eine Methode betrachtet werden. Das ist nützlich, weil der Zeiger als Wert weitergereicht werden kann.

Das zentrale Konzept eines Delegaten ist seine Signatur oder Form. Das sind (1) Der Rückgabetyp und (2) die Eingabeargumente. Wenn wir beispielsweise einen Delegaten erstellen void MyDelegate(object sender, EventArgs e), kann er nur auf Methoden zeigen, die void zurückgeben und ein object und EventArgs entgegennehmen. So ähnlich wie ein Quadratloch und ein quadratischer Zapfen. Daher sagen wir, dass diese Methoden die gleiche Signatur oder Form wie der Delegat haben.

Also, wenn Sie wissen, wie Sie einen Verweis auf eine Methode erstellen können, denken Sie über den Zweck von Ereignissen nach: Wir möchten, dass bestimmter Code ausgeführt wird, wenn etwas anderes im System passiert - oder wir das Ereignis "behandeln". Dafür erstellen wir spezifische Methoden für den auszuführenden Code. Der Kitt zwischen dem Ereignis und den auszuführenden Methoden sind die Delegaten. Das Ereignis muss intern eine "Liste" von Zeigern auf die Methoden speichern, die aufgerufen werden sollen, wenn das Ereignis ausgelöst wird.* Natürlich müssen wir wissen, welche Argumente an eine Methode übergeben werden sollen, um sie aufrufen zu können! Wir verwenden den Delegaten als "Vertrag" zwischen dem Ereignis und allen spezifischen Methoden, die aufgerufen werden.

Der Standard-EventHandler (und viele ähnliche) repräsentiert eine spezifische Form von Methode (wieder void/object-EventArgs). Wenn Sie also ein Ereignis deklarieren, sagen Sie, welche Form von Methode (EventHandler) dieses Ereignis auslöst, indem Sie einen Delegaten angeben:

//Dieser Delegat kann verwendet werden, um auf Methoden zu zeigen,
//die void zurückgeben und einen String entgegennehmen.
public delegate void MyEventHandler(string foo);

//Dieses Ereignis kann dazu führen, dass jede Methode, die
//MyEventHandler entspricht, aufgerufen wird.
public event MyEventHandler SomethingHappened;

//Hier ist etwas Code, der ausgeführt werden soll,
//wenn SomethingHappened ausgelöst wird.
void HandleSomethingHappened(string foo)
{
    //Mach etwas
}

//Ich erstelle einen Delegaten (Zeiger) auf HandleSomethingHappened
//und füge ihn der Liste "Ereignishandler" von SomethingHappened hinzu.
myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

//Um das Ereignis in einer Methode auszulösen.
SomethingHappened("bar");

(*Dies ist der Schlüssel zu Ereignissen in .NET und lüftet das "magische" - ein Ereignis ist unter der Oberfläche nur eine Liste von Methoden mit der gleichen "Form". Die Liste wird dort gespeichert, wo das Ereignis lebt. Wenn das Ereignis "ausgelöst" wird, bedeutet es wirklich nur "gehe durch diese Liste von Methoden und rufe jede einzelne auf, indem du diese Werte als Parameter verwendest". Das Zuweisen eines Ereignisbehandlers ist nur ein hübscherer, einfacherer Weg, um Ihre Methode zu dieser Liste von Methoden hinzuzufügen, die aufgerufen werden sollen).

112voto

tofi9 Punkte 5595

C# kennt zwei Begriffe, delegate und event. Fangen wir mit dem ersten an.

Delegate

Ein delegate ist ein Verweis auf eine Methode. Genau wie du einen Verweis auf eine Instanz erstellen kannst:

MyClass instanz = myFactory.GetInstance();

Kannst du einen delegate verwenden, um einen Verweis auf eine Methode zu erstellen:

Action meineMethode = myFactory.GetInstance;

Jetzt, da du diesen Verweis auf eine Methode hast, kannst du die Methode über den Verweis aufrufen:

MyClass instanz = meineMethode();

Aber warum würdest du das tun? Du kannst auch einfach myFactory.GetInstance() direkt aufrufen. In diesem Fall kannst du. Es gibt jedoch viele Fälle, in denen du nicht möchtest, dass der Rest der Anwendung Kenntnis von myFactory hat oder myFactory.GetInstance() direkt aufruft.

Ein offensichtlicher Fall ist, wenn du in der Lage sein möchtest, myFactory.GetInstance() an einer zentralen Stelle durch myOfflineFakeFactory.GetInstance() zu ersetzen (auch bekannt als Factory-Methodenmuster).

Factory-Methodenmuster

Wenn du also eine Klasse TheOtherClass hast und sie myFactory.GetInstance() verwenden soll, dann sieht der Code ohne Delegates wie folgt aus (du musst TheOtherClass über den Typ deiner myFactory informieren):

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

class TheOtherClass
{
   public void SetFactory(MyFactory factory)
   {
      // hier setzen
   }

}

Wenn du Delegates verwenden würdest, musst du den Typ meiner Factory nicht offenlegen:

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

class TheOtherClass
{
   public void SetFactoryMethod(Action factoryMethode)
   {
      // hier setzen
   }

}

Daher kannst du einer anderen Klasse einen Delegate geben, ohne deinen Typen für sie freizulegen. Das einzige, was du freilegst, ist die Signatur deiner Methode (wie viele Parameter du hast und so weiter).

"Signatur meiner Methode", wo habe ich das schon gehört? Ach ja, Interfaces!!! Interfaces beschreiben die Signatur einer ganzen Klasse. Denke an Delegates als Beschreibung der Signatur nur einer Methode!

Ein weiterer großer Unterschied zwischen einem Interface und einem Delegate besteht darin, dass du beim Schreiben deiner Klasse nicht zu C# sagen musst "diese Methode implementiert diesen Typ eines Delegates". Bei Interfaces musst du sagen "diese Klasse implementiert diesen Typ eines Interfaces".

Weiterhin kann ein Delegate-Verweis (mit einigen Einschränkungen, siehe unten) auf mehrere Methoden verweisen (genannt MulticastDelegate). Das bedeutet, dass beim Aufrufen des Delegates mehrere explizit angehängte Methoden ausgeführt werden. Ein Objektverweis kann immer nur auf ein Objekt verweisen.

Die Einschränkungen für ein MulticastDelegate sind, dass die (Methoden/Delegate-)Signatur keinen Rückgabewert haben sollte (void) und die Schlüsselwörter out und ref nicht in der Signatur verwendet werden. Offensichtlich kannst du nicht zwei Methoden aufrufen, die eine Zahl zurückgeben und erwarten, dass sie die gleiche Zahl zurückgeben. Sobald die Signatur übereinstimmt, ist der Delegate automatisch ein MulticastDelegate.

Event

Events sind nur Eigenschaften (wie die get;set;-Eigenschaften für Instanzfelder), die eine Abonnement auf den Delegate von anderen Objekten freigeben. Diese Eigenschaften unterstützen jedoch kein get;set;. Stattdessen unterstützen sie add; remove;

Du kannst also haben:

    Action meinFeld;

    public event Action MeineEigenschaft
    {
        add { meinFeld += value; }
        remove { meinFeld -= value; }
    }

Verwendung in UI (WinForms, WPF, UWP usw.)

Also wissen wir jetzt, dass ein Delegate ein Verweis auf eine Methode ist und dass wir ein Event haben können, um der Welt mitzuteilen, dass sie uns ihre Methoden geben können, die von unserem Delegate referenziert werden können. Wenn wir also ein UI-Button sind, können wir alle, die daran interessiert sind, ob ich geklickt wurde, bitten, ihre Methode bei uns zu registrieren (über das von uns freigegebene Event). Wir können alle diese Methoden verwenden, die uns gegeben wurden, und sie durch unseren Delegate referenzieren. Und dann warten wir und warten... bis ein Benutzer kommt und auf diesen Button klickt, dann haben wir genug Grund, den Delegate aufzurufen. Und weil der Delegate auf all diese uns gegebenen Methoden verweist, werden all diese Methoden aufgerufen. Wir wissen nicht, was diese Methoden tun, noch wissen wir, welche Klasse diese Methoden implementiert. Alles, worüber wir uns sorgen, ist, dass sich jemand für unser Klicken interessiert und uns einen Verweis auf eine Methode gegeben hat, die unserer gewünschten Signatur entspricht.

Java

Sprachen wie Java haben keine Delegates. Sie verwenden stattdessen Interfaces. Der Weg, wie sie das machen, besteht darin, alle, die an 'uns angeklickt werden' interessiert sind, aufzufordern, eine bestimmte Schnittstelle zu implementieren (mit einer bestimmten Methode, die wir aufrufen können), dann geben sie uns die ganze Instanz, die die Schnittstelle implementiert. Wir halten eine Liste aller Objekte, die diese Schnittstelle implementieren, und können ihre 'bestimmte Methode, die wir aufrufen können' aufrufen, wann immer wir angeklickt werden.

47voto

Andy Punkte 29060

Dies ist tatsächlich die Deklaration für einen Ereignishandler - eine Methode, die aufgerufen wird, wenn ein Ereignis ausgelöst wird. Um ein Ereignis zu erstellen, würde man etwas wie dies schreiben:

public class Foo
{
    public event EventHandler MyEvent;
}

Und dann können Sie sich für das Ereignis wie folgt anmelden:

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

Mit der Definition von OnMyEvent() wie folgt:

private void OnMyEvent(object sender, EventArgs e)
{
    MessageBox.Show("MyEvent wurde ausgelöst!");
}

Immer wenn Foo MyEvent auslöst, wird dann Ihr OnMyEvent Ereignishandler aufgerufen werden.

Sie müssen nicht immer eine Instanz von EventArgs als zweiten Parameter verwenden. Wenn Sie zusätzliche Informationen einschließen möchten, können Sie eine Klasse ableiten, die von EventArgs abgeleitet ist. (EventArgs ist konventionell die Basis). Beispielsweise, wenn Sie sich einige der Ereignisse auf Control in WinForms, oder FrameworkElement in WPF ansehen, können Sie Beispiele von Ereignissen sehen, die zusätzliche Informationen an die Ereignishandler übergeben.

43voto

Gary Willoughby Punkte 48229

Hier ist ein Codebeispiel, das hilfreich sein könnte:

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

namespace Event_Example
{
  // Zuerst müssen wir einen Delegaten definieren, der als Signatur für die
  // Funktion dient, die letztendlich aufgerufen wird, wenn das Ereignis ausgelöst wird.
  // Sie werden feststellen, dass der zweite Parameter vom Typ MyEventArgs ist.
  // Dieses Objekt enthält Informationen über das ausgelöste Ereignis.

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

  // Das ist eine Klasse, die das Ereignis der Klasse beschreibt, die es empfängt.
  // Eine EventArgs-Klasse muss immer von System.EventArgs abgeleitet sein.

  public class MyEventArgs : EventArgs
  {
    private string EventInfo;

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

    public string GetInfo() {
      return EventInfo;
    }
  }

  // Diese nächste Klasse ist diejenige, die ein Ereignis enthält und es auslöst,
  // sobald eine Aktion ausgeführt wird. Beispielsweise lösen wir dieses Ereignis aus,
  // sobald eine Variable über einen bestimmten Wert inkrementiert wird. Beachten Sie,
  // dass das Ereignis den MyEventHandler-Delegaten verwendet, um eine Signatur
  // für die aufgerufene Funktion zu erstellen.

  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 
        {
          // Um sicherzustellen, dass das Ereignis nur ausgelöst wird, wenn ein Handler vorhanden ist,
          // überprüfen wir das Ereignis, um sicherzustellen, dass es nicht null ist.
          if(OnMaximum != null) {
            OnMaximum(this, new MyEventArgs("Sie haben " +
              value.ToString() +
              " eingegeben, aber das Maximum ist " +
              Maximum.ToString()));
          }
        }
      }
    }
  }

  class Program
  {
    // Dies ist die tatsächliche Methode, die dem Ereignishandler in der obigen Klasse zugewiesen wird.
    // Hier führen wir eine Aktion aus, sobald das Ereignis ausgelöst wurde.

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

    static void Main(string[] args) {
      // Lassen Sie uns jetzt das Ereignis in der obigen Klasse testen.
      MyClass MyObject = new MyClass();
      MyObject.OnMaximum += new MyEventHandler(MaximumReached);
      for(int x = 0; x <= 15; x++) {
        MyObject.MyValue = x;
      }
      Console.ReadLine();
    }
  }
}

28voto

Mathieu Guindon Punkte 67008

Nur um den bereits hier vorhandenen großartigen Antworten hinzuzufügen - basierend auf dem Code im akzeptierten Antwort, der ein delegate void MyEventHandler(string foo) verwendet...

Da der Compiler den Delegattyp des SomethingHappened Ereignisses kennt, ist dies:

myObj.SomethingHappened += HandleSomethingHappened;

Völlig gleichwertig zu:

myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

Und Handler können auch mit unregister mit -= wie folgt:

// -= entfernt den Handler aus der Liste der "Zuhörer" des Ereignisses:
myObj.SomethingHappened -= HandleSomethingHappened;

Zur Vollständigkeit halber kann das Auslösen des Ereignisses nur in der Klasse, die das Ereignis besitzt, erfolgen:

// Das Auslösen des Ereignisses erfolgt durch einfaches Bereitstellen der Argumente für das Ereignis:
var handler = SomethingHappened; // Thread-lokale Kopie des Ereignisses
if (handler != null) // das Ereignis ist null, wenn keine Zuhörer vorhanden sind!
{
    handler("Hallo!");
}

Die thread-lokale Kopie des Handlers ist notwendig, um sicherzustellen, dass die Ausführung thread-sicher ist - ansonsten könnte ein Thread den letzten Handler für das Ereignis sofort nach der Überprüfung auf null abmelden, und wir würden eine "spaßige" NullReferenceException haben.


C# 6 führte eine schöne Kurzschreibweise für dieses Muster ein. Es verwendet den Nullausbreitungsoperator.

SomethingHappened?.Invoke("Hallo!");

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X