Uploaded by Ditkovskaya1990

Лабораторная работа №20. Делегаты и события

advertisement
1
Лабораторная работа №20. Делегаты и события
Контрольные вопросы и задания:
1. Что такое делегат, экземпляр делегата?
Строго говоря, делегат - это класс, содержащий данные о сигнатуре метода. Экземпляр
делегата (delegae instance) - объект, который позволяет привязаться к конкретному методу,
соответствующему определенной сигнатуре (подобно объявлению указателя на функцию в
C/C++). Как и указатель на функцию делегат может быть передан в виде аргумента.
2. Каким образом происходит объявление делегата, инициализация делегата и вызов метода с
помощью делегата?
Делегаты можно объявить с помощью любого из следующих методов:
Объявите тип делегата и объявите метод с соответствующей сигнатурой:
C#Копировать

// Declare a delegate.
delegate void NotifyCallback(string str);
// Declare a method with the same signature as the delegate.
static void Notify(string name)
{
Console.WriteLine($"Notification received for: {name}");
}
C#Копировать
// Create an instance of the delegate.
NotifyCallback del1 = new NotifyCallback(Notify);
Назначьте группу методов типу делегата:
C#Копировать

// C# 2.0 provides a simpler way to declare an instance of NotifyCallback.
NotifyCallback del2 = Notify;
Объявите анонимный метод:
C#Копировать

// Instantiate NotifyCallback by using an anonymous method.
NotifyCallback del3 = delegate(string name)
{ Console.WriteLine($"Notification received for: {name}"); };
Используйте лямбда-выражение:
C#Копировать

// Instantiate NotifyCallback by using a lambda expression.
NotifyCallback del4 = name => { Console.WriteLine($"Notification received for: {name}"); };
Дополнительные сведения см. в разделе Лямбда-выражения.
Следующий пример демонстрирует объявление, создание экземпляра и использование
делегата. Класс BookDB инкапсулирует базу данных книжного магазина, в которой хранится
информация о книгах. Он предоставляет метод ProcessPaperbackBooks, который находит в базе
данных все книги в мягкой обложке и вызывает делегат для каждой из них. Используется
тип delegate с именем ProcessBookCallback. Класс Test использует этот класс для печати
заголовков и средней цены книг в мягкой обложке.
Использование делегата помогает правильно разделить функции между базой данных
книжного магазина и кодом клиента. Код клиента не имеет сведений о том, как хранятся книги
и как код книжного магазина находит книги в мягкой обложке. Код книжного магазина не
имеет сведений о том, какая обработка выполняется для найденных книг в мягкой обложке.
2
3. Что такое многократная адресация? Каким образом добавляются и удаляются ссылки на метод
экземпляру делегата?
Множественная адресация (multihoming) – это термин, который используется для описания
подключения автономной системы более чем к одному провайдеру Internet. Так делается
обычно по одной из двух следующих причин:
–Для повышения надежности подключения к Internet, чтобы обеспечить как минимум одно
надежное соединение.
–Для повышения производительности, чтобы для обеспечения пути к определенным
получателям использовался лучший путь.
Когда вы объявляете переменную делегатного типа, на самом деле вы получаете объект,
производный от MulticastDelegate. Исходники этого класса можно посмотреть тут.
Вам обычно не должно быть нужно «разобрать» MulticastDelegate на составляющие его
одиночные делегаты, вы вызываете всю группу делегатов вместе:
Action d = () => Console.WriteLine("delegate 1");
d += () => Console.WriteLine("delegate 2");
d();
Выводит следующее:
delegate 1
delegate 2
Но если очень хочется, можно получить список по частям, используя GetInvocationList():
foreach (Action part in d.GetInvocationList())
part();
Результат будет такой же, даже порядок делегатов гарантированно тот же.
4. Что такое анонимный метод? Приведите синтаксис анонимного метода.
С делегатами тесно связаны анонимные методы. Анонимные методы используются для
создания экземпляров делегатов.
Определение анонимных методов начинается с ключевого слова delegate, после которого идет
в скобках список параметров и тело метода в фигурных скобках:
1
2
3
4
delegate(параметры)
{
// инструкции
}
Например:
1
2
3
4
5
3
6
7
MessageHandler handler = delegate (string mes)
{
Console.WriteLine(mes);
};
handler("hello world!");
delegate void MessageHandler(string message);
Анонимный метод не может существовать сам по себе, он используется для инициализации
экземпляра делегата, как в данном случае переменная handler представляет анонимный метод.
И через эту переменную делегата можно вызвать данный анонимный метод.
Другой пример анонимных методов - передача в качестве аргумента для параметра, который
представляет делегат:
1
2
3
4
5
6
7
8
9
10
11
ShowMessage("hello!", delegate (string mes)
{
Console.WriteLine(mes);
});
static void ShowMessage(string message, MessageHandler handler)
{
handler(message);
}
delegate void MessageHandler(string message);
Если анонимный метод использует параметры, то они должны соответствовать параметрам
делегата. Если для анонимного метода не требуется параметров, то скобки с параметрами
опускаются. При этом даже если делегат принимает несколько параметров, то в анонимном
методе можно вовсе опустить параметры:
1
2
3
4
5
6
7
MessageHandler handler = delegate
{
Console.WriteLine("анонимный метод");
};
handler("hello world!"); // анонимный метод
4
delegate void MessageHandler(string message);
То есть если анонимный метод содержит параметры, они обязательно должны соответствовать
параметрам делегата. Либо анонимный метод вообще может не содержать никаких параметров,
тогда он соответствует любому делегату, который имеет тот же тип возвращаемого значения.
При этом параметры анонимного метода не могут быть опущены, если один или несколько
параметров определены с модификатором out.
Также, как и обычные методы, анонимные могут возвращать результат:
1
2
3
4
5
6
7
8
Operation operation = delegate (int x, int y)
{
return x + y;
};
int result = operation(4, 5);
Console.WriteLine(result);
// 9
delegate int Operation(int x, int y);
При этом анонимный метод имеет доступ ко всем переменным, определенным во внешнем
коде:
1
2
3
4
5
6
7
8
9
int z = 8;
Operation operation = delegate (int x, int y)
{
return x + y + z;
};
int result = operation(4, 5);
Console.WriteLine(result);
// 17
delegate int Operation(int x, int y);
5. Каким образом осуществляется передача аргументов анонимному методу?
Анонимному методу можно передать один или несколько аргументов. Для этого достаточно
указать в скобках список параметров после ключевого слова delegate, а при обращении к
экземпляру делегата — передать ему соответствующие аргументы. В качестве примера ниже
приведен вариант предыдущей программы, измененный с целью передать в качестве аргумента
конечное значение для подсчета.
5
// Продемонстрировать применение анонимного метода, принимающего аргумент.
using System;
// Обратите внимание на то, что теперь у делегата Countlt имеется параметр,
delegate void Countlt(int end);
class AnonMethDemo2 {
static void Main() {
// Здесь конечное значение для подсчета передается анонимному методу.
Countlt count = delegate (int end) {
for(int i=0; i <= end; i++)
Console.WriteLine(i);
};
count C);
Console.WriteLine ();
count E);
}
}
В этом варианте программы делегат CountIt принимает целочисленный аргумент. Обратите
внимание на то, что при создании анонимного метода список параметров указывается после
ключевого слова delegate. Параметр end становится доступным для кода в анонимном методе
таким же образом, как и при создании именованного метода. Ниже приведен результат
выполнения данной программы.
0
1
2
3
0
1
2
3
4
5
6. Что такое лямбда-выражения? Приведите синтексис лямбда-выражения.
Лямбда-выражения принимают две формы. Форма, которая наиболее прямо заменяет
анонимный метод, представляет собой блок кода, заключенный в фигурные скобки. Это —
прямая замена анонимных методов. Лямбда-выражения, с другой стороны, предоставляют ещё
более сокращенный способ объявлять анонимный метод и не требуют ни кода в фигурных
скобках, ни оператора return. Оба типа лямбда-выражений могут быть преобразованы в
делегаты.
Во всех лямбда-выражениях используется лямбда-оператор =>, который читается как
«переходит в» (в языках Java, F# и PascalABC.NET используется оператор ->). Левая часть
лямбда-оператора определяет параметры ввода (если таковые имеются), а правая часть
содержит выражение или блок оператора. Лямбда-выражение x => x * 5 читается как «функция
x, которая переходит в x, умноженное на 5»
7. В чем заключаются особенности использования лямбда-выражений?
Лямбда-выражение — это специальный синтаксис для определения функциональных
объектов, заимствованный из λ-исчисления.
6
Применяется как правило для объявления анонимных функций по месту их использования, и
обычно допускает замыкание на лексический контекст, в котором это выражение
использовано.
Используя лямбда-выражения, можно объявлять функции в любом месте кода.
8. Что такое событие, обработчик события?
Обработчик события
- это функция, которая обрабатывает, или откликается на событие. Приложения должны
зарегистрировать свои функции обработчиков событий в веб-браузере, указав тип события и
цель. Когда в указанном целевом объекте возникнет событие указанного типа, браузер вызовет
обработчик. Когда обработчики событий вызываются для какого-то объекта, мы иногда
говорим, что браузер «возбудил» или «сгенерировал» событие.
9. Для чего необходимы события? Опишите принцип действия событий, порядок обработки
события.
События (events) - это те средства, которые обеспечивают интерактивность web- страниц. Зная
события и умея управлять ими, можно превратить простую двумерную web- страницу в
трехмерную. Такая страница предлагает пользователю возможность как бы касаться, ощущать
и изменять ее содержание вместо того, чтобы просто "глазеть" на нее.
Очень простым примером служит событие Click, т.е. щелчок левой кнопкой мыши. Например,
можно написать JavaScript-код, который вычисляет сумму платежа с учетом выбранных
пользователем товаров. Важным фактором становится определение того, когда этот код
выполнять? Логичным ответом на этот вопрос является выполнение кода тогда, когда это
потребуется самому пользователю. Совсем нетрудно представить себе web-страницу с кнопкой
"Найти общую сумму". Тогда код для вычисления платежа нужно выполнять тогда, когда
пользователь щелкнет на этой кнопке. Таким образом, щелчок на кнопке является событием.
Фактически это событие и называется "Click"!
Принцип обработки событий, по существу, напоминает обработку прерываний. Но если
прерывания и их обработка встроены в операционную систему, то события и их обработка
тесно интегрированы с браузером, документом и пользователем.
Под событием понимается определенный сигнал, возникающий в результате действия
пользователя, например
щелчка левой кнопкой мыши (событие Click),
двойного щелчка левой кнопкой мыши (событие DblClick),
наведения указателя мыши на конкретный элемент документа (событие MouseOver), нажатия
клавиши (событие KeyPress) и др.,
или автоматически формируемый системой при изменении ее состояния, например
при окончании загрузки документа (событие Load),
обнаружении ошибки (событие Error) и др.
В ответ на все эти события система реагирует определенным образом, обычно вызовом
специальной функции.
Нетрудно заметить, что события называются английскими словами (регистр букв не играет
роли), передающими содержательный смысл события.
7
Под обработчиком события (event handler) понимается код (скрипт - script), написанный на
скриптовом языке (scripting language); этот скрипт перехватывает событие и выполняет
некоторые ответные действия.
Например, при щелчке на кнопке активизируется ассоциированный с этим событием
обработчик события и реагирует на событие Click. Названия (имена) обработчиков событий
образуются добавлением префикса on к названию события, например обработчик события
Click называется onClick (часто названия обработчиков событий даются на нижнем регистре,
т.е onclick).
Был определен набор событий, применимых к конкретным элементам на странице. В браузерах
Netscape Navigator 3 и Internet Explorer 3 эти события были определены и реализованы на языке
JavaScript. В браузерах четвертого поколения события были значительно расширены, но, к
сожалению, почти полностью несовместимыми для браузеров способами. Большие новшества
в части событий реализованы и в браузерах пятого поколения.
В браузерах четвертого поколения модель событий усовершенствована благодаря введению
объекта Event, который система автоматически формирует при возникновении любого
события. Этот объект содержит информацию (она называется свойствами этого объекта) об
особенностях возникшего события и состоянии определенных компонентов системы,
например о нажатой клавише-модификаторе Ctrl, Alt и Shift.
Браузер Internet Explorer поддерживает расширенную объектную модель документа (Document
Object Model - DOM). В этой модели все содержание документа, включая элементы и атрибуты,
оказывается программно доступным и управляемым. Другими словами, в браузере каждый
элемент отражается как объект иерархической структуры. Следовательно, любой элемент на
странице может быть источником полного набора событий мыши и клавиатуры. Основные
положения объектной модели документа в части событий сводятся к следующему:
Все элементы способны генерировать события.
Имеются события взаимодействия, события обновления и события изменения.
Модель событий обеспечивает реакции на действия пользователя.
Механизм доставки событий разрешает отмену действия по умолчанию.
События всплывают по иерархической структуре документа.
События асинхронны.
События определяются способом, нейтральным к языку и платформе.
Имеется интерфейс для привязки к событиям.
а
тра
с
юс
ч
8
!§
ия простых событий
Click
Это событие применяется наиболее часто и поддерживается почти всеми браузерами.
Blur
Также поддерживается почти всеми браузерами. Событие Blur запускается, когда элемент
теряет фокус, обычно когда пользователь переходит к другому элементу формы, нажимая
клавишу Tab или производя щелчок левой кнопкой мыши.
Change
Это событие запускается при изменении содержимого текстового поля или текстовой области,
когда изменение фиксируется переходом к другому элементу формы.
Focus
Данное событие возникает, когда указатель мыши фокусируется на
элементе, придавая ему фокус ввода.
MouseOver и MouseOut
Эти события возникают, когда указатель мыши "входит" в элемент и "выходит" из элемента.
KeyDown и KeyUp
Эти события запускаются, когда пользователь нажимает и отпускает клавишу на клавиатуре.
MouseDown и MouseUp
Помимо обработки события Click поддерживаются события MouseDown и MouseUp.
KeyPress
Это событие запускается, когда нажата клавиша.
MouseMove
Данное событие запускается всякий раз, когда указатель мыши перемещается по элементу,
точнее, пикселам элемента.
SelectStart
Это событие запускается, когда пользователь начинает выбирать текст в конкретном элементе.
DragStart
Данное событие запускается, когда пользователь начинает буксировать ("тащить") конкретный
элемент.
9
Help
Это событие запускается, когда пользователь щелкает на элементе для выбора его, а затем
нажимает клавишу {F1}.
DblClick
Событие DblClick запускается, когда пользователь производит на элементе двойной щелчок.
Отметим, что первый щелчок запускает событие Click, а второй щелчок, производимый в
определенном временном промежутке, запускает событие DblClick.
Select
Обработчик события Select выполняется, когда выбирается текст в элементе.
Модель события (event model) конкретизирует, как действия пользователя или системы
активизируют обработчики событий (скрипты) в документе. Она определяет следующее:
Какие события генерируются.
Как они доставляются в обработчики событий.
Какая информация доступна обработчику события для обработки.
Браузеры Internet Explorer (IE) и Netscape Navigator (NN) определяют различные модели
событий для объектной модели документа (Document Object Model - DOM) Браузер IE
реализует так называемое всплывание событий (event bubbling), являющееся подходом снизувверх, а браузер NN реализует захват событий (event capturing), являющий подходом сверхувниз.
События генерируются (запускаются - trigger, или даже "зажигаются" - fire) двумя способами
- пользователем или системой. Генерируемые пользователем события возникают, когда
пользователь взаимодействует с документом путем движения (Move) мыши, щелчка на кнопке
мыши (Click) или нажатия клавиши (KeyPress) на клавиатуре. Генерируемые системой события
возникают, когда изменяется состояние системы, например возникла ошибка (Error) или
закончилась загрузка страницы (Load).
Модель события показывает, при каких контекстах событие можно генерировать и какова цель
события в данном контексте. Для генерируемых пользователем событий модель события
определяет, какие действия пользователя достоверны для различных элементов документа и
какой целевой элемент (target element) документа определяется для события. С системными
событиями целевой элемент обычно не ассоциируется и они захватываются обработчиками
событий, определяемыми глобальным скриптом на странице.
Доставка события определяет процесс, с помощью которого событие доставляется коду
(скрипту), реагирующему на событие. Способ доставки события зависит от типа возникшего
события и типа реализованной в браузере модели события.
Традиционно все события имеют целевые элементы (target element) документа, которые
определяют код для обработчика события как атрибут элемента документа. Однако более
совершенные способы обработки событий требуют, чтобы события обрабатывались
элементами документа, отличающимися от целевого элемента. События проходят
("всплывают" - bubble) по иерархической структуре документа и обрабатываются теми
элементами страницы, которые наиболее подходят для события.
10
Модель события определяет также объект Event, который экспонирует (предоставляет)
важную информацию о событии. Она включает в себя тип события, цель события, позицию
указателя мыши в момент щелчка кнопкой мыши и нажатые клавиши. Обработчики событий
могут обращаться к этой информации для определения необходимой реакции на событие.
События возникают на конкретных элементах документа, поэтому необходимо ассоциировать
обработчик события с конкретным событием и конкретным элементом документа, или
объектом, чтобы можно было вызвать обработчик при появлении события на данном элементе.
Эта операция называется также привязкой (binding) или подключением (attachment)
обработчика события к элементу. Элементы описываются тэгами языка HTML, поэтому при
ассоциировании должны обязательно привлекаться тэги документа. Для ассоциирования
обработчика события с элементами и происходящими на них событиями применяются
следующие, по существу, эквивалентные способы.
Определить обработчик события как особенный атрибут тэга, именем которого является
название события, а значением JavaScript-код, выполняемый при запуске события. Такой
обработчик события часто называется внутренним (буквально "внутристрочным" - inline event
handler). Простейшим примером служит кнопка с обработчиком события onClick:
Вот как выглядит определение данной кнопки:
<form>
<input type="button" name="Кнопка" value="Щелкни на мне!" onc^k^'alert^'Ebi щелкнули на
кнопке!')">
</form>
Здесь при щелчке на кнопке выводится информационный бокс (alert box), содержащий текст
'Вы щелкнули на кнопке!'
Как обычно, значение атрибута заключается в двойные кавычки, а затем определяются любые
операторы JavaScript, разделяемые точкой с запятой. Если в этих операторах потребуются
кавычки, придется применять одиночные кавычки.
Гораздо удобнее объявить для обработчика события функцию, а с помощью атрибута события
в тэге элемента вызвать эту функцию. Вот как выглядит определение функции с именем flip и
ассоциирование ее с элементом IMG в качестве обработчика события MouseOver:
<script language="JavaScript"> function flip() {
// Некоторые действия, выполняемые функцией }
</script>
.Содержание документа
<img src="sample.gif" onmouseover="flip()">
Этот пример показывает ассоциирование обработчика события с функцией. Отметим, что
после имени функции требуются круглые скобки, а поскольку функция является значением
атрибута тэга HTML, ее имя заключается в двойные кавычки.
Функции обработчиков событий допускается объявлять в любом месте документа. Однако
обычно объявления всех функций помещаются в секции HEAD документа, потому что при
11
этом функции загружаются первыми и гарантируется доступность функций до окончания
загрузки всего документа и возможных действий пользователя, инициирующих различные
события. Кроме того, функции обработчиков событий можно вызывать событиями от
нескольких элементов, т.е. разделять (share) эти функции. Сами функции могут содержать
любой JavaScript-код.
Третий способ состоит в объявлении кода обработчика события в тэгах <SCRIPT> и </SCRIPT>
и использовании атрибутов FOR и EVENT этого тэга для ассоциирования кода с конкретным
событием на конкретном элементе. Для этого потребуется идентифицировать (именовать)
нужный элемент с помощью атрибута ID. Следующий пример определяет некоторый
JavaScript-код и ассоциирует его как обработчик события MouseOver для элемента IMG,
имеющего идентификатор MyImage.
<script for="MyImage" event=onmouseover language="JavaScript">
// Некоторые действия, выполняемые обработчиком события </script>
.Содержание документа
<img id="MyImage" src="sample.gif">
Этот способ ассоциирования события на определенном элементе с обработчиком события
поддерживает только броузер Internet Explorer, поэтому он применяется в интрасетях
(intranets), т.е. внутренних локальных сетях организаций, компьютеры которых оснащены
только этим браузером. В документах World Wide Web этот способ практически не
применяется.
Последний способ ассоциирования функции с конкретным событием состоит в том, чтобы
присвоить ссылку на функцию свойству объекта обработчика события:
<script language="JavaScript"> function myAlert() {
alert("Спасибо за щелчок на кнопке.");
}
</script>
<form name="myForm">
<input type="button" name="myButton" value-'Щелкни здесь">
</form>
<script language="JavaScript"> document.myForm.myButton.onclick=myAlert;
</script>
В этом способе событие Click на кнопке myButton в форме myForm документа будет вызывать
функцию myAlert обработчика события.
Общий синтаксис этого способа имеет следующий вид: object.eventhandler = function;
например: window.onload = welcome;
12
Этот оператор присваивает функцию welcome() обработчику события onLoad окна. Отметим,
что обработчик события должен быть написан строчными буквами (onload вместо onLoad), что
необходимо для совместимости с броузерами третьего поколения.
Отметим также, что свойство обработчика события должно присваиваться ссылке функции, а
не вызову функции. Поэтому нужно присвоить welcome, а не welcome(), так как скобки
означают вызов функции, вычисляющий возвращаемые функцией значения.
Как общее правило, необходимо всегда определять и ассоциировать обработчики событий в
документе как можно раньше. В зависимости от используемого способа ассоциирования
обработчик может начать обработку событий либо в процессе загрузки документа, либо сразу
по ее окончании. Когда определяется внутренний обработчик события, он ассоциируется,
когда создается элемент. Код обработки события с помощью атрибутов FOR и EVENT
ассоциируется, когда документ полностью загружен.
Можно ассоциировать один и тот же обработчик события с несколькими элементами в
документе. Этот прием удобен, когда требуется выполнять некоторое действие при появлении
события на любом из элементов. Обычно обработчики событий разрабатываются для
обработки только одного типа события, но можно создать обработчики, которые могут
обрабатывать события нескольких типов.
Не требуется ограничиваться одним обработчиком для каждого типа события. Если действие,
которое должно выполняться для данного события в одном элементе, отличается от действия
для такого же события в другом элементе, можно определить два обработчика и ассоциировать
каждый с его элементом. В общем, допускается определять любое число обработчиков
событий для одного и того же типа события и ассоциировать каждый из обработчиков с одним
или несколькими элементами документа.
Объект Event
Объект Event поддерживается обоими браузерами. Свойства этого объекта доступны для
каждого события, возникающего на каждом объекте в документе. Однако имена свойств в
браузерах различаются. Далее мы рассмотрим свойства объекта Event только для броузера
Internet Explorer.
Напомним, что объект - это конструкция, обладающая свойствами и методами, которые
отражают данные, привязанные к конкретному компоненту. Например, объект document
обладает свойствами, которые отражают характеристики открытого документа: его адрес URL,
его фоновый цвет, его название (title) и др. Объект также обладает методами, являющимися
функциями, которые выполняют конкретную задачу. Например, объект window имеет метод
close(), который закрывает окно, и метод open(), который открывает новое окно.
Следовательно, объект Event содержит информацию о событии. А о каком событии? Событии,
которое только что произошло! Другими словами, объект Event является снимком деталей,
окружающих событие сразу после его возникновения. Можно использовать свойства объекта
Event в коде обработчика события для получения подробных сведений о событии, например,
где находился указатель мыши в момент события, были ли нажаты некоторые клавиши в
момент появления события и т.д.
Объект Event предоставляет подробную информацию о том, где в документе возникает
событие, поэтому легко написать обработчик события, который адаптирует свои действия,
опираясь на источник события. Если ассоциировать обработчик события с таким элементом,
который содержит все элементы, в которых может возникать событие, то всплывание событий
гарантирует, что обработчик обязательно вызывается, когда происходит событие в каждом
элементе. Это означает, что не нужно ассоциировать обработчик события с каждым элементом.
13
10. В чем заключается особенность использования обычных методов в качестве обработчиков
события?
События сигнализируют системе о том, что произошло определенное действие. И если нам
надо отследить эти действия, то как раз мы можем применять события.
Например, возьмем следующий класс, который описывает банковский счет:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Account
{
// сумма на счете
public int Sum { get; private set; }
// в конструкторе устанавливаем начальную сумму на счете
public Account(int sum) => Sum = sum;
// добавление средств на счет
public void Put(int sum) => Sum += sum;
// списание средств со счета
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
}
}
}
В конструкторе устанавливаем начальную сумму, которая хранится в свойстве Sum. С
помощью метода Put мы можем добавить средства на счет, а с помощью метода Take, наоборот,
снять деньги со счета. Попробуем использовать класс в программе - создать счет, положить и
снять с него деньги:
1
2
3
4
5
6
7
Account account = new Account(100);
account.Put(20); // добавляем на счет 20
14
Console.WriteLine($"Сумма на счете: {account.Sum}");
account.Take(70); // пытаемся снять со счета 70
Console.WriteLine($"Сумма на счете: {account.Sum}");
account.Take(180); // пытаемся снять со счета 180
Console.WriteLine($"Сумма на счете: {account.Sum}");
Консольный вывод:
Сумма на счете: 120
Сумма на счете: 50
Сумма на счете: 50
Все операции работают как и положено. Но что если мы хотим уведомлять пользователя о
результатах его операций. Мы могли бы, например, для этого изменить метод Put следующим
образом:
1
2
3
4
5
public void Put(int sum)
{
Sum += sum;
Console.WriteLine($"На счет поступило: {sum}");
}
Казалось, теперь мы будем извещены об операции, увидев соответствующее сообщение на
консоли. Но тут есть ряд замечаний. На момент определения класса мы можем точно не знать,
какое действие мы хотим произвести в методе Put в ответ на добавление денег. Это может
вывод на консоль, а может быть мы захотим уведомить пользователя по email или sms. Более
того мы можем создать отдельную библиотеку классов, которая будет содержать этот класс, и
добавлять ее в другие проекты. И уже из этих проектов решать, какое действие должно
выполняться. Возможно, мы захотим использовать класс Account в графическом приложении
и выводить при добавлении на счет в графическом сообщении, а не консоль. Или нашу
библиотеку классов будет использовать другой разработчик, у которого свое мнение, что
именно делать при добавлении на счет. И все эти вопросы мы можем решить, используя
события.
Определение и вызов событий
События объявляются в классе с помощью ключевого слова event, после которого указывается
тип делегата, который представляет событие:
1
2
delegate void AccountHandler(string message);
event AccountHandler Notify;
В данном случае вначале определяется делегат AccountHandler, который принимает один
параметр типа string. Затем с помощью ключевого слова event определяется событие с именем
Notify, которое представляет делегат AccountHandler. Название для события может быть
произвольным, но в любом случае оно должно представлять некоторый делегат.
Определив событие, мы можем его вызвать в программе как метод, используя имя события:
1
Notify("Произошло действие");
Поскольку событие Notify представляет делегат AccountHandler, который принимает один
параметр типа string - строку, то при вызове события нам надо передать в него строку.
15
Однако при вызове событий мы можем столкнуться с тем, что событие равно null в случае, если
для его не определен обработчик. Поэтому при вызове события лучше его всегда проверять на
null. Например, так:
1
if(Notify !=null) Notify("Произошло действие");
Или так:
1
Notify?.Invoke("Произошло действие");
В этом случае поскольку событие представляет делегат, то мы можем его вызвать с помощью
метода Invoke(), передав в него необходимые значения для параметров.
Объединим все вместе и создадим и вызовем событие:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Account
{
public delegate void AccountHandler(string message);
public event AccountHandler? Notify;
// 1.Определение события
public Account(int sum) => Sum = sum;
public int Sum { get; private set; }
public void Put(int sum)
{
Sum += sum;
Notify?.Invoke($"На счет поступило: {sum}"); // 2.Вызов события
}
public void Take(int sum)
{
if (Sum >= sum)
{
16
Sum -= sum;
Notify?.Invoke($"Со счета снято: {sum}"); // 2.Вызов события
}
else
{
Notify?.Invoke($"Недостаточно денег на счете. Текущий баланс: {Sum}"); ;
}
}
}
Теперь с помощью события Notify мы уведомляем систему о том, что были добавлены средства
и о том, что средства сняты со счета или на счете недостаточно средств.
Добавление обработчика события
С событием может быть связан один или несколько обработчиков. Обработчики событий - это
именно то, что выполняется при вызове событий. Нередко в качестве обработчиков событий
применяются методы. Каждый обработчик событий по списку параметров и возвращаемому
типу должен соответствовать делегату, который представляет событие. Для добавления
обработчика события применяется операция +=:
1
Notify += обработчик события;
Определим обработчики для события Notify, чтобы получить в программе нужные
уведомления:
1
2
3
4
5
6
7
8
9
10
Account account = new Account(100);
account.Notify += DisplayMessage; // Добавляем обработчик для события Notify
account.Put(20); // добавляем на счет 20
Console.WriteLine($"Сумма на счете: {account.Sum}");
account.Take(70); // пытаемся снять со счета 70
Console.WriteLine($"Сумма на счете: {account.Sum}");
account.Take(180); // пытаемся снять со счета 180
Console.WriteLine($"Сумма на счете: {account.Sum}");
void DisplayMessage(string message) => Console.WriteLine(message);
В данном случае в качестве обработчика используется метод DisplayMessage, который
соответствует по списку параметров и возвращаемому типу делегату AccountHandler. В итоге
при вызове события Notify?.Invoke() будет вызываться метод DisplayMessage, которому для
параметра message будет передаваться строка, которая передается в Notify?.Invoke(). В
DisplayMessage просто выводим полученное от события сообщение, но можно было бы
определить любую логику.
Если бы в данном случае обработчик не был бы установлен, то при вызове события
Notify?.Invoke() ничего не происходило, так как событие Notify было бы равно null.
Консольный вывод программы:
17
На счет поступило: 20
Сумма на счете: 120
Со счета снято: 70
Сумма на счете: 50
Недостаточно денег на счете. Текущий баланс: 50
Сумма на счете: 50
Теперь мы можем выделить класс Account в отдельную библиотеку классов и добавлять в
любой проект.
Добавление и удаление обработчиков
Для одного события можно установить несколько обработчиков и потом в любой момент
времени их удалить. Для удаления обработчиков применяется операция -=. Например:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Account account = new Account(100);
account.Notify += DisplayMessage;
// добавляем обработчик DisplayMessage
account.Notify += DisplayRedMessage; // добавляем обработчик DisplayRedMessage
account.Put(20); // добавляем на счет 20
account.Notify -= DisplayRedMessage; // удаляем обработчик DisplayRedMessage
account.Put(50); // добавляем на счет 50
void DisplayMessage(string message) => Console.WriteLine(message);
void DisplayRedMessage(string message)
{
// Устанавливаем красный цвет символов
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
// Сбрасываем настройки цвета
Console.ResetColor();
}
Консольный вывод:
На счет поступило: 20
На счет поступило: 20
На счет поступило: 50
В качестве обработчиков могут использоваться не только обычные методы, но также делегаты,
анонимные методы и лямбда-выражения. Использование делегатов и методов:
1
18
2
3
4
5
6
7
8
9
Account acc = new Account(100);
// установка делегата, который указывает на метод DisplayMessage
acc.Notify += new Account.AccountHandler(DisplayMessage);
// установка в качестве обработчика метода DisplayMessage
acc.Notify += DisplayMessage;
// добавляем обработчик DisplayMessage
acc.Put(20);
// добавляем на счет 20
void DisplayMessage(string message) => Console.WriteLine(message);
В данном случае разницы между двумя обработчиками никакой не будет.
Установка в качестве обработчика анонимного метода:
1
2
3
4
5
6
Account acc = new Account(100);
acc.Notify += delegate (string mes)
{
Console.WriteLine(mes);
};
acc.Put(20);
Установка в качестве обработчика лямбда-выражения:
1
2
3
Account account = new Account(100);
account.Notify += message => Console.WriteLine(message);
account.Put(20);
Управление обработчиками
С помощью специальных акссесоров add/remove мы можем управлять добавлением и
удалением обработчиков. Как правило, подобная функциональность редко требуется, но тем
не менее мы ее можем использовать. Например:
1
2
3
4
5
6
7
8
9
19
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Account
{
public delegate void AccountHandler(string message);
AccountHandler? notify;
public event AccountHandler Notify
{
add
{
notify += value;
Console.WriteLine($"{value.Method.Name} добавлен");
}
remove
{
notify -= value;
Console.WriteLine($"{value.Method.Name} удален");
}
}
public Account(int sum) => Sum = sum;
public int Sum { get; private set; }
public void Put(int sum)
{
Sum += sum;
notify?.Invoke($"На счет поступило: {sum}"); // 2.Вызов события
}
public void Take(int sum)
{
if (Sum >= sum)
20
{
Sum -= sum;
notify?.Invoke($"Со счета снято: {sum}"); // 2.Вызов события
}
else
{
notify?.Invoke($"Недостаточно денег на счете. Текущий баланс: {Sum}"); ;
}
}
}
Теперь опредление события разбивается на две части. Вначале просто определяется
переменная делегата, через которую мы можем вызывать связанные обработчики:
1
AccountHandler notify;
Во второй части определяем акссесоры add и remove. Аксессор add вызывается при добавлении
обработчика, то есть при операции +=. Добавляемый обработчик доступен через ключевое
слово value. Здесь мы можем получить информацию об обработчике (например, имя метода
через value.Method.Name) и определить некоторую логику. В данном случае для простоты
просто выводится сообщение на консоль:
1
2
3
4
5
add
{
notify += value;
Console.WriteLine($"{value.Method.Name} добавлен");
}
Блок remove вызывается при удалении обработчика. Аналогично здесь можно задать
некоторую дополнительную логику:
1
2
3
4
5
remove
{
notify -= value;
Console.WriteLine($"{value.Method.Name} удален");
}
Внутри класса событие вызывается также через переменную notify. Но для добавления и
удаления обработчиков в программе используется как раз Notify:
1
2
3
4
5
6
7
Account acc = new Account(100);
21
acc.Notify += DisplayMessage;
// добавляем обработчик DisplayMessage
acc.Put(20); // добавляем на счет 20
acc.Notify -= DisplayMessage; // удаляем обработчик DisplayMessage
acc.Put(20); // добавляем на счет 20
void DisplayMessage(string message) => Console.WriteLine(message);
Консольный вывод программы:
DisplayMessage добавлен
На счет поступило: 20
DisplayMessage удален
Передача данных события
Нередко при возникновении события обработчику события требуется передать некоторую
информацию о событии. Например, добавим и в нашу программу новый класс
AccountEventArgs со следующим кодом:
1
2
3
4
5
6
7
8
9
10
11
12
class AccountEventArgs
{
// Сообщение
public string Message{get;}
// Сумма, на которую изменился счет
public int Sum {get;}
public AccountEventArgs(string message, int sum)
{
Message = message;
Sum = sum;
}
}
Данный класс имеет два свойства: Message - для хранения выводимого сообщения и Sum - для
хранения суммы, на которую изменился счет.
Теперь применим класс AccoutEventArgs, изменив класс Account следующим образом:
1
2
3
4
5
6
7
8
9
10
22
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Account
{
public delegate void AccountHandler(Account sender, AccountEventArgs e);
public event AccountHandler? Notify;
public int Sum { get; private set; }
public Account(int sum) => Sum = sum;
public void Put(int sum)
{
Sum += sum;
Notify?.Invoke(this, new AccountEventArgs($"На счет поступило {sum}", sum));
}
public void Take(int sum)
{
if (Sum >= sum)
{
Sum -= sum;
Notify?.Invoke(this, new AccountEventArgs($"Сумма {sum} снята со счета", sum));
}
else
{
Notify?.Invoke(this, new AccountEventArgs("Недостаточно денег на счете", sum));
}
}
}
По сравнению с предыдущей версией класса Account здесь изменилось только количество
параметров у делегата и соответственно количество параметров при вызове события. Теперь
делегат AccountHandler в качестве первого параметра принимает объект, который вызвал
событие, то есть текущий объект Account. А в качестве второго параметра принимает объект
AccountEventArgs, который хранит информацию о событии, получаемую через конструктор.
Теперь изменим основную программу:
1
2
3
23
4
5
6
7
8
9
10
11
12
Account acc = new Account(100);
acc.Notify += DisplayMessage;
acc.Put(20);
acc.Take(70);
acc.Take(150);
void DisplayMessage(Account sender, AccountEventArgs e)
{
Console.WriteLine($"Сумма транзакции: {e.Sum}");
Console.WriteLine(e.Message);
Console.WriteLine($"Текущая сумма на счете: {sender.Sum}");
}
По сравнению с предыдущим вариантом здесь мы только изменяем количество параметров и
их использования в обработчике DisplayMessage. Благодаря первому параметру в методе
можно получить информацию об отправителе события - счете, с которым производится
операция. А через второй параметр можно получить инфомацию о состоянии операции.
11. Что такое аксессоры событий? Приведите расширенный синтаксис объявления события.
Для управления списком обработчиков событий служит расширенная форма оператора event,
позволяющая использовать аксессоры событий. Эти аксессоры предоставляют средства для
управления реализацией подобного списка в приведенной ниже форме:
event делегат_события имя_события {
add {
// Код добавления события в цепочку событий
}
remove {
// Код удаления события из цепочки событий
}
}
В эту форму входят два аксессора событий: add и remove. Аксессор add вызывается, когда
обработчик событий добавляется в цепочку событий с помощью оператора +=. В то же время
аксессор remove вызывается, когда обработчик событий удаляется из цепочки событий с
помощью оператора -=.
Когда вызывается аксессор add или remove, он принимает в качестве параметра добавляемый
или удаляемый обработчик. Как и в других разновидностях аксессоров, этот неявный параметр
называется value. Реализовав аксессоры add или remove, можно организовать специальную
схему хранения обработчиков событий. Например, обработчики событий можно хранить в
массиве, стеке или очереди.
Вышесказанное иллюстрирует нижеследующее:
using System;
namespace ConsoleApplication1
{
24
delegate void UI ();
class MyEvent
{
UI[] evnt = new UI[5];
// Объявляем событие
public event UI UserEvent
{
// Используем аксессоры событий
add
{
evnt[1] = value;
}
remove
{
evnt[1] = null;
}
}
// Используем метод для запуска события
public void OnUserEvent()
{
evnt[1]();
}
}
class UserInfo
{
string uiName, uiFamily;
int uiAge;
public UserInfo(string Name, string Family, int Age)
{
this.Name = Name;
this.Family = Family;
this.Age = Age;
}
public string Name { set { uiName = value; } get { return uiName; } }
public string Family { set { uiFamily = value; } get { return uiFamily; } }
public int Age { set { uiAge = value; } get { return uiAge; } }
// Обработчик события
public void UserInfoHandler()
{
Console.WriteLine("Событие вызвано!\n");
Console.WriteLine("Имя: {0}\nФамилия: {1}\nВозраст: {2}",Name,Family,Age);
}
}
class Program
{
static void Main()
25
{
MyEvent evt = new MyEvent();
UserInfo user1 = new UserInfo(Name: "Alex", Family: "Erohin", Age: 26);
// Добавляем обработчик события
evt.UserEvent += user1.UserInfoHandler;
// Запустим событие
evt.OnUserEvent();
Console.ReadLine();
}
}
}
Длинная нотация для определения событий удобна, если необходимо сделать нечто большее,
чем просто добавлять и удалять обработчики событий, например, добавить синхронизацию для
многопоточного доступа. Элементы управления WPF используют длинную нотацию для
добавления функциональности "пузырькового" и туннельного распространения событий.
12. В чем заключается особенность применения анонимных методов и лямбда-выражений вместе
с событиями?
Разница в том, что лямбда, в зависимости от контекста использования, может быть
скомпилирована в две совершенно разных вещи:
Если контекст подразумевает использование лямбды как анонимного метода - лямбда
компилируется в анонимный метод.
Func<int, bool> filter = x => x > 2;
будет превращено компилятором в то же, во что он превратит
Func<int, bool> filter = delegate(int x) { return x > 2 };
Т.е. в этом варианте использования - лямбды - это просто сокращенный вариант старого
синтаксиса анонимных методов. Именно в таком виде они внедрены в Java / C++ и остальных
языках, которые "добавили лямбды" за последние пару лет.
В той же Java лямбды - это единственный способ объявить анонимный метод. До введения
лямбд в Java были анонимные классы (анонимные реализации интерфейсов), но не было
анонимных методов - что сделало написание современного кода с промисами (аналогом Task)
дико неудобным. Введение анонимных методов ("лямбд") значительно упростило написание
цепочек тасков с коллбеками.
В C# синтаксис анонимных методов был введен в 2.0, в 2005-ом году, цепочки промисов можно
делать уже больше десяти лет, так что что лямбды в качестве альтернативного синтаксиса
ключевому слову delegate картины не изменили. Вводились лямбды совсем не ради него.
В этом варианте использования различия между лямбдами и delegate {} минимальны - на
уровне "тут меньше скобок". Никаких принципиальных различий или значительных
преимуществ между двумя синтаксисами нет.
Если контекст подразумевает использование лямбды в виде значения типа Expression, то
лямбда компилируется не в анонимный метод, а объект-композит, представляющий код
лямбды в виде AST:
Expression<Func<int, bool>> filter = x => x > 2;
превращается в
ParameterExpression paramX = Expression.Parameter(typeof(int));
Expression<Func<int, bool>> filter = Expression.Lambda<Func<int, bool>>(
Expression.GreaterThan(
paramX, Expression.Constant(2)
),
paramX
);
Такое представление позволяет в рантайме преобразовывать выражения из кода (x > 2) в код
на другом языке (SQL: WHERE table.ColumnX > 2, OData: $filter=x&gt2).
26
Именно из-за этого варианта использования и ввели лямбды в C#, и именно за счет него
работают LINQ-провайдеры вроде Entity Framework.
Download