<Mr.ElectroNick>
/* personal website */

События и делегаты в языке C#

Когда я занялся изучением событий и делегатов я прочитал огромное количество статей документации для того чтобы окончательно разобраться в этом вопросе. Теперь я хочу предложить вам прочитать эту статью и узнать всё к чему я пришел.


Введение


Когда я занялся изучением событий и делегатов я прочитал огромное количество статей документации для того чтобы окончательно разобраться в этом вопросе. Теперь я хочу предложить вам прочитать эту статью и узнать всё самое главное относительно событий и делегатов.


Что такое ДЕЛЕГАТЫ?


Понятия делегатов и событий всецело связаны друг с другом. Делегаты это указатели на функции. Delegate это класс. Когда высоздаете экземпляр этого класса, необходимо передать имя функции параметром конструктора класса делегата. На переданную функцию и будет ссылаться делегат.


Каждый делегат имеет сигнатуру. Делегат объявляется таким образом:

Delegate int SomeDelegate(string s, bool b);

Когда я говорю что делегат имеет сигнатуру, то я имею ввиду что делегат возвращает int и имеет два параметра string и bool.


Я уже говорил что когда вы создаете экземпляр делегата, то вы передаете в конструктор делегата имя функции на которую делегат будет ссылаться. Важно отметить что только функции имеющие схожую сигнатуру (набор параметров и возвращаемый тип) могут быть переданы в качестве параметра при создании экземпляра класса делегата.


Рассмотрим следующую функцию:

private int SomeFunction(string str, bool bln){…}

Вы можете передать эту функцию в конструктор класса делегата SomeDelegate, потому что сигнатуры делегата и функции соответствуют.

SomeDelegate sd = new SomeDelegate(SomeFunction);

Теперь, sd ссылается на SomeFunction, или другими словами, SomeFunction зарегестрирована в sd. Если вы вызываете sd, SomeFunction будет также вызвана.

sd(«somestring», true);

Теперь, когда мы разобрались с делегатами, давайте разберемся с событиями…


Что такое СОБЫТИЯ?



  • Button является классом, когда вы на неё нажимаете, срабатывает событие Click.
  • Timer является классом, каждую миллисекунду срабатывает событие Tick.

Хотите узнать что в этот момент происходит? Давайте по порядку:


У нас есть класс Counter. Этот класс содержит метод CountTo(int countTo, int reachableNum) который начинает отсчет от 0 до countTo, и запускает событие NumberReached когда значение счета достигает reachableNum.


Наш класс содержит событие: NumberReached. События — это переменные с типом делегата. Я имею ввиду что если вы хотите объявить событие, то вы просто объявляете событие с тем же типом что и делегат и ставите ключевое слово event перед объявлением. Например как сделано сдесь:

public event NumberReachedEventHandler NumberReached;

В вышеописаном объявлении, NumberReachedEventHandler это делегат. Может быть было бы лучше назвать делегат как: NumberReachedDelegate, но обратите внимание что Microsoft не называет системные делегаты например так: MouseDelegate или PaintDelegate, наоборот, спецификация по которой описываются делегаты такова: MouseEventHandler и PaintEventHandler.


Вы видите, перед тем как мы объявляем событие, мы должны определить делегат (обработчик события — event handler). Это должно выглядеть примерно так:

public delegate void NumberReachedEventHandler(
object sender, NumberReachedEventArgs e);

Как вы видите, имя делегата: NumberReachedEventHandler, и его сигнатура содержит возвращаемый тип void и 2 параметра object и NumberReachedEventArgs. Если вы где-либо собираетесь объявить этот делегат, то функция переданная в конструктор делегата должна иметь ту же сигнатуру (это уже упоминалось выше).


Использовали ли вы когда-либо PaintEventArgs или MouseEventArgs в вашем коде для определения положения мыши, где она перемещается, или свойство Graphics объекта вызвавшего событие Paint? В действительности, мы определяем набор данных в классе наслодованом от базового класса EventArgs. Например, нам нужно передать конечное число счетчика, вот как выглядит определение класса:

public class NumberReachedEventArgs : EventArgs
{
private int _reached;
public NumberReachedEventArgs(int num)
{
this._reached = num;
}
public int ReachedNumber
{
get
{
return _reached;
}
}
}

Если нет необходимости передавать в аргументах события какую либо информацию то можно использовать просто класс EventArgs, с минимальным набором параметров.


Теперь всё готово чтобы разглядеть внутренности класса Counter:

namespace Events
{
public delegate void NumberReachedEventHandler(object sender,
NumberReachedEventArgs e);
///
/// Summary description for Counter.
///

public class Counter
{
public event NumberReachedEventHandler NumberReached;

public Counter()
{
//
// TODO: Add constructor logic here
//
}
public void CountTo(int countTo, int reachableNum)
{
if(countTo < reachableNum) throw new ArgumentException(
«reachableNum should be less than countTo»);
for(int ctr=0;ctr<=countTo;ctr++) { if(ctr == reachableNum)
{
NumberReachedEventArgs e = new NumberReachedEventArgs(
reachableNum);
OnNumberReached(e);
return;//don’t count any more
}
}
}
protected virtual void OnNumberReached(NumberReachedEventArgs e)
{
if(NumberReached != null)
{
NumberReached(this, e);//Raise the event
}
}
}


Вышепреведеном коде мы вызываем событие по достижении граничного значение счетчика. Ещё много чего нужно расказать:



  • Срабатывание события совершается посредством вызова события внутри класса (экземпляр делегата NumberReachedEventHandler):
    NumberReached(this, e);

  • Мы подготавливаем экземпляр аргументов события:
    NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);

  • Возникает вопрос: почему мы вызываем NumberReached(this, e) через OnNumberReached(NumberReachedEventArgs e)? Почему бы не использовать следующий код?:
    if(ctr == reachableNum)
    {
    NumberReachedEventArgs e = new NumberReachedEventArgs(reachableNum);
    //OnNumberReached(e);
    if(NumberReached != null)
    {
    NumberReached(this, e);//Raise the event
    }
    return;//don’t count any more
    }

    Вопрос хороший! Если вы хотите знать почему вызов идет не напрямую, внимательно посмотрите на сигнатуру OnNumberReached:

    protected virtual void OnNumberReached(NumberReachedEventArgs e)


    • Вы увидите, этот метод является protected, это означает что этот метод доступен лишь внутри экземпляра класса или наследуемым классам (inherited).
    • Данный метод кроме всего ещё и virtual, что означает что он может быть переопределен (перегружен) в наследуемом классе.

    И это очень удобно. Предположим что вы разрабатываете класс который наследуется от класса Counter . Переопределением метода OnNumberReached, вы можете сделать дополнительные действия внутри вашего класса перед срабатыванием события. Например:

    protected override void OnNumberReached(NumberReachedEventArgs e)
    {
    //Do additional work
    base.OnNumberReached(e);
    }

    Обратите внимание что если вы не вызываете base.OnNumberReached(e), событие никогда не сработает! Это иногда полезно когда вы, наследуюясь от какого-либо класса, хотите исключить отработку некоторых (или даже всех) событий!


    Для реального примера, вы можете создать новое ASP.NET приложение и покопаться внутри кода который был сгенерирован. Вы увидите что страница наследуется от класса System.Web.UI.Page. Этот класс содержит виртуальный protected метод OnInit. Вы видите что метод InitializeComponent() вызываете внутри переопределенного метода как дополнительная функциональность, и дальше уже в OnInit(e) идет вызов функциональности базового метода:

    #region Web Form Designer generated code
    protected override void OnInit(EventArgs e)
    {
    //CODEGEN: This call is required by the ASP.NET Web Form Designer.
    InitializeComponent();
    base.OnInit(e);
    }
    ///
    /// Required method for Designer support — do not modify
    /// the contents of this method with the code editor.
    ///

    private void InitializeComponent()
    {
    this.Load += new System.EventHandler(this.Page_Load);
    }
    #endregion

  • Отметте что делегат NumberReachedEventHandler определен вне класса, но внутри того же пространства имен (namespace), и доступен для всех классов приложения.

OK. Теперь настало время практически использовать класс Counter:


В нашем приложении, используем 2 textboxes txtCountTo и txtReachable:


sample application


И вот обработчик события для события нажатия (click) btnRun:

private void cmdRun_Click(object sender, System.EventArgs e)
{
if(txtCountTo.Text == «» || txtReachable.Text==«»)
return;
oCounter = new Counter();
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.CountTo(Convert.ToInt32(txtCountTo.Text),
Convert.ToInt32(txtReachable.Text));
}
private void oCounter_NumberReached(object sender, NumberReachedEventArgs e)
{
MessageBox.Show(«Reached: « + e.ReachedNumber.ToString());
}

Вот синтаксис для инициации обработчика для определенного события:

oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);

Теперь, вы разрбалить в происходящем! Вы инициируете делегат NumberReachedEventHandler.


Также обратите внимание на то что мы используем += вместо =.


Это потому что делегаты — специализированные объекты которые могут содержать ссылки на одну и более функций. Например, если есть ещё онда функция oCounter_NumberReached2 с той же сигнатурой что и oCounter_NumberReached, на обе функции можно ссылаться таким вот образом:

oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);

Теперь, после срабатывания события, обе функции будут выполнены одна за одной.


Если где-либо в вашем коде, вы решите что oCounter_NumberReached2 не должна больше вызываться по срабатыванию события NumberReached, вы можете сделать вот так:

oCounter.NumberReached -= new NumberReachedEventHandler(
oCounter_NumberReached2);

В завершении


Не забудте поставить следующие строки в конструкторе вашей формы, вместо cmdRun_Click.

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
oCounter = new Counter();
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached);
oCounter.NumberReached += new NumberReachedEventHandler(
oCounter_NumberReached2);

Исходные коды для этой статьи вы можете найти выше.