Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
На заметку! Делегаты Action<> и Func<> интенсивно используются во многих важных API-интерфейсах .NET Core, включая инфраструктуру параллельного программирования и LINQ (помимо прочих).
Итак, первоначальный экскурс в типы делегатов окончен. Теперь давайте перейдем к обсуждению связанной темы — ключевого слова event языка С#.
Понятие событий C#
Делегаты — довольно интересные конструкции в том плане, что позволяют объектам, находящимся в памяти, участвовать в двустороннем взаимодействии. Тем не менее, прямая работа с делегатами может приводить к написанию стереотипного кода (определение делегата, определение необходимых переменных-членов, создание специальных методов регистрации и отмены регистрации для предохранения инкапсуляции и т.д.).
Более того, во время применения делегатов непосредственным образом как механизма обратного вызова в приложениях, если вы не определите переменную-член типа делегата в классе как закрытую, тогда вызывающий код будет иметь прямой доступ к объектам делегатов. В таком случае вызывающий код может присвоить переменной-члену новый объект делегата (фактически удаляя текущий список функций, которые подлежат вызову) и, что даже хуже, вызывающий код сможет напрямую обращаться к списку вызовов делегата. В целях демонстрации создайте новый проект консольного приложения по имени PublicDelegateProblem и добавьте следующую переделанную (и упрощенную) версию класса Car из предыдущего примера CarDelegate:
namespace PublicDelegateproblem
{
public class Car
{
public delegate void CarEngineHandler(string msgForCaller);
// Теперь это член public!
public CarEngineHandler ListOfHandlers;
// Просто вызвать уведомление Exploded.
public void Accelerate(int delta)
{
if (ListOfHandlers != null)
{
ListOfHandlers("Sorry, this car is dead...");
}
}
}
}
Обратите внимание, что у вас больше нет закрытых переменных-членов с типами делегатов, инкапсулированных с помощью специальных методов регистрации. Поскольку эти члены на самом деле открытые, вызывающий код может получить доступ прямо к переменной-члену ListOfHandlers, присвоить ей новые объекты CarEngineHandler и вызвать делегат по своему желанию:
using System;
using PublicDelegateProblem;
Console.WriteLine("***** Agh! No Encapsulation! *****n");
// Создать объект Car.
Car myCar = new Car();
// Есть прямой доступ к делегату!
myCar.ListOfHandlers = CallWhenExploded;
myCar.Accelerate(10);
// Теперь можно присвоить полностью новый объект...
// что в лучшем случае сбивает с толку.
myCar.ListOfHandlers = CallHereToo;
myCar.Accelerate(10);
// Вызывающий код может также напрямую вызывать делегат!
myCar.ListOfHandlers.Invoke("hee, hee, hee...");
Console.ReadLine();
static void CallWhenExploded(string msg)
{
Console.WriteLine(msg);
}
static void CallHereToo(string msg)
{
Console.WriteLine(msg);
}
Открытие доступа к членам типа делегата нарушает инкапсуляцию, что не только затруднит сопровождение кода (и отладку), но также сделает приложение уязвимым в плане безопасности! Ниже показан вывод текущего примера:
***** Agh! No Encapsulation! *****
Sorry, this car is dead...
Sorry, this car is dead...
hee, hee, hee...
Очевидно, что вы не захотите предоставлять другим приложениям возможность изменять то, на что указывает делегат, или вызывать его члены без вашего разрешения. С учетом сказанного общепринятая практика предусматривает объявление переменных-членов, имеющих типы делегатов, как закрытых.
Ключевое слово event
В качестве сокращения, избавляющего от необходимости создавать специальные методы для добавления и удаления методов из списка вызовов делегата, в языке C# предлагается ключевое слово event. В результате обработки компилятором ключевого слова event вы автоматически получаете методы регистрации и отмены регистрации, а также все необходимые переменные-члены для типов делегатов. Такие переменные-члены с типами делегатов всегда объявляются как закрытые и потому они не доступны напрямую из объекта, инициирующего событие. В итоге ключевое слово event может использоваться для упрощения отправки специальным классом уведомлений внешним объектам.
Определение события представляет собой двухэтапный процесс. Во-первых, понадобится определить тип делегата (или задействовать существующий тип), который будет хранить список методов, подлежащих вызову при возникновении события. Во-вторых, необходимо объявить событие (с применением ключевого слова event) в терминах связанного типа делегата.
Чтобы продемонстрировать использование ключевого слова event, создайте новый проект консольного приложения по имени CarEvents. В этой версии класса Car будут определены два события под названиями AboutToBlow и Exploded, которые ассоциированы с единственным типом делегата по имени CarEngineHandler. Ниже показаны начальные изменения, внесенные в класс Car:
using System;
namespace CarEvents
{
public class Car
{
...
// Этот делегат работает в сочетании с событиями Car.
public delegate void CarEngineHandler(string msgForCaller);
// Этот объект Car может отправлять следующие события:
public event CarEngineHandler Exploded;
public event CarEngineHandler AboutToBlow;
...
}
}
Отправка события вызывающему коду сводится просто к указанию события по имени наряду со всеми обязательными параметрами, как определено ассоциированным делегатом. Чтобы удостовериться в том, что вызывающий код действительно зарегистрировал событие, перед вызовом набора методов делегата событие следует проверить на равенство null. Ниже приведена новая версия метода Accelerate() класса Car:
public void Accelerate(int delta)
{
// Если автомобиль сломан, то инициировать событие Exploded.
if (_carIsDead)
{
Exploded?.Invoke("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Почти сломан?
if (10 == MaxSpeed - CurrentSpeed)
{
AboutToBlow?.Invoke("Careful buddy! Gonna blow!");
}
// Все еще в порядке!
if (CurrentSpeed >= MaxSpeed)
{
_carIsDead = true;
}
else
{
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
}
Итак, класс Car был сконфигурирован для отправки двух специальных событий без необходимости в определении специальных функций регистрации или в объявлении переменных-членов, имеющих типы делегатов. Применение нового объекта вы увидите очень скоро, но сначала давайте чуть подробнее рассмотрим архитектуру событий.