Розуміння подій та обробників подій у C #


329

Я розумію мету подій, особливо в контексті створення інтерфейсів користувача. Я думаю, що це прототип створення події:

public void EventName(object sender, EventArgs e);

Що роблять обробники подій, навіщо вони потрібні та як я їх створюю?


9
Як зазначає @Andy, тут фрагмент коду описує метод, зареєстрований для події, а не саму подію.
dthrasher


Відповіді:


660

Щоб зрозуміти обробників подій, вам потрібно зрозуміти делегатів . У C # ви можете розглядати делегата як вказівник (або посилання) на метод. Це корисно, оскільки вказівник може бути переданий навколо як значення.

Центральною концепцією делегата є його підпис, або форма. Це (1) тип повернення та (2) вхідні аргументи. Наприклад, якщо ми створимо делегата void MyDelegate(object sender, EventArgs e), він може вказувати лише на методи, які повертаються void, і приймають objectі 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 ?? З усіх заплутаних конвенцій про іменування, це найгірше ...
Джоель у Gö

37
@Joel in Go подія не називається EventHandler - EventHandler - це договір, який має мати подія з тим, хто спілкується з нею. Це як "string MyString" - рядок оголошує тип. подія MyEventHandler TheEvent заявляє, що кожен, хто взаємодіє з цією подією, повинен відповідати договору MyEventHandler. Конвенція Хендлера полягає в тому, що в контракті в основному описується спосіб поводження з подією.
Рекс М

18
Як розгортається подія?
алхімічна

17
@Rex M: дякую за перше узгоджене пояснення "MyEventHandler", яке я коли-небудь бачив :)
Джоель в Ґо,

10
Дякую за етап: "Клей між подією та методами, які потрібно виконати, - це делегати". Це справді приголомшливо.
zionpi

103

C # знає два терміни, delegateі event. Почнемо з першого.

Делегат

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) та ключові слова outта refне використовується в підписі. Очевидно, що ви не можете викликати два методи, які повертають число, і очікують, що вони повернуть одне і те ж число. Після того, як підпис відповідає, делегат автоматично a MulticastDelegate.

Подія

Події - це лише властивості (як, наприклад, get; set; властивості до примірників полів), які піддають передплату на делегата від інших об'єктів. Однак ці властивості не підтримують get; set ;. Натомість вони підтримують додавання; видалити;

Отже, ви можете мати:

    Action myField;

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

Використання в інтерфейсі (WinForms, WPF, UWP тощо)

Отже, тепер ми знаємо, що делегат - це посилання на метод, і ми можемо мати подію, щоб повідомити світові, що вони можуть дати нам свої методи, на які можна посилатись від нашого делегата, і ми є кнопкою інтерфейсу користувача, значить: можна попросити кожного, хто цікавиться, чи мене натискали, зареєструвати у нас їх метод (через подію, яку ми експонували). Ми можемо використовувати всі ті методи, які були нам надані, і посилатися на них нашим делегатом. І тоді ми будемо чекати і чекати .... поки прийде користувач і натисне на цю кнопку, тоді у нас буде достатньо підстав викликати делегата. А оскільки делегат посилається на всі ті методи, які нам дано, всі ці методи будуть використані. Ми не знаємо, що роблять ці методи, і не знаємо, який клас реалізує ці методи. Все, що нам важливо, це те, що хтось зацікавився, щоб нас натискали,

Java

Такі мови, як Java, не мають делегатів. Вони замість цього використовують інтерфейси. Як вони це роблять, - це попросити всіх, хто зацікавлений у тому, щоб нас "натискали", реалізувати певний інтерфейс (певним методом, який ми можемо викликати), а потім дати нам весь екземпляр, який реалізує інтерфейс. Ми зберігаємо список усіх об'єктів, що реалізують цей інтерфейс, і можемо викликати їх "певний метод, який ми можемо викликати", коли ми натискаємо.


вітає пояснення, але чим подія відрізняється від екземпляра делегата, який приймає передплатників? вони обидва схожі на абсолютно одне і те ж?
BKSpurgeon

@BKSpurgeon це тому , що вони є «делегатами , які приймають абонент» - eventце всього лише синтаксис, більше нічого.
Матьє Гіндон

"Обмеженнями для MulticastDelegate є те, що підпис (метод / делегат) не повинен мати жодного зворотного значення (недійсного)", я не вважаю це правильним. Якщо вони мають значення повернення, вони повернуть останнє.
Хозікімару

"Таким чином, ви можете надати делегата до якогось іншого класу для використання, не піддаючи їм свого типу. Єдине, що ви виставляєте, - це підпис вашого методу ..." - це для мене критична точка. Дякую!
Райан

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пожежа викличе MyEvent, тоді OnMyEventвикличуть вашого обробника.

Вам не завжди потрібно використовувати екземпляр EventArgsяк другий параметр. Якщо ви хочете включити додаткову інформацію, ви можете використовувати клас, похідний від EventArgs( EventArgsє базовою умовою). Наприклад, якщо ви подивитеся на деякі події, визначені Controlв WinForms або FrameworkElementWPF, ви можете побачити приклади подій, які передають додаткову інформацію обробникам подій.


14
Дякуємо, що відповіли на запитання та не входили в Делегати та події.
divide_byzero

3
Я рекомендую не використовувати OnXXXшаблон іменування для вашого обробника подій. (Тупо вважається, що OnXXX означає «обробляти XXX» у MFC, а «піднімати XXX» у .net, і тепер його значення незрозуміле та заплутане - детальніше див. У цій публікації ). Кращими іменами було б RaiseXXXвикликати події та / HandleXXXабо Sender_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..."));
Тіма Шмелтера

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, за винятком того, що ключове слово "подія" розглядаю як псевдонім для "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).

  • Ви можете створити власний підклас EventArgs, щоб включити будь-який тип інформації, яку потрібно передати події. Не потрібно використовувати EventHandlers під час використання подій. Ви можете повністю їх пропустити і використовувати власний вид делегата на їх місці.

  • Однією з ключових відмінностей між використанням подій та делегатами є те, що події можна викликати лише з класу, в якому вони були оголошені, навіть якщо вони можуть бути оголошені як загальнодоступні. Це дуже важлива відмінність, оскільки вона дозволяє виставляти ваші події так, щоб вони були "пов'язані" із зовнішніми методами, і в той же час були захищені від "зовнішнього нецільового використання".


0

Інша річ, про яку потрібно знати , в деяких випадках вам доведеться використовувати Делегатів / Події, коли вам потрібен низький рівень зв'язку !

Якщо ви хочете використовувати компонент у декількох місцях додатка , вам потрібно зробити компонент з низьким рівнем зв'язку, а специфічна незахищена LOGIC повинна бути делегована ВНУТРІСТЬ вашого компонента! Це гарантує наявність у вас роз'єднаної системи та більш чистий код.

У принципі SOLID це " D ", ( принцип інверсії D- залежності).

Також відомий як " IoC ", Інверсія управління .

Ви можете зробити " IoC " за допомогою подій, делегатів та DI (введення залежності).

Доступ до методу в дитячому класі легко. Але складніше отримати доступ до методу в батьківському класі від дитини. Ви повинні передати батьківське посилання на дитину! (або використовувати DI з інтерфейсом)

Делегати / події дозволяють нам спілкуватися від дитини до батька без посилання!

введіть тут опис зображення

На цій діаграмі вище я не використовую Delegate / Event, і батьківський компонент B повинен мати посилання на батьківський компонент A для виконання незахищеної бізнес-логіки методом A. (високий рівень з'єднання)

При такому підході мені доведеться скласти всі посилання на всі компоненти, які використовують компонент B! :(

введіть тут опис зображення

На цій діаграмі вище я використовую Delegate / Event і компонент B не повинен бути відомим A. (низький рівень зв'язку)

І ви можете використовувати свій компонент B в будь-якому місці вашої програми !

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.