События и делегаты в языке 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
:
И вот обработчик события для события нажатия (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);
Исходные коды для этой статьи вы можете найти выше.