В данной статье будет вкратце обсуждаться проблемная для новичков тема делегатов, событий, анонимных методов, функторов и действий.
Делегатом является объект ссылочного типа, предоставляющий ссылки на метод или группу методов приложения.
Делегаты имеют следующие свойства:
- Делегаты похожи на указатели функций в C++, но являются объектно-ориентированными и типобезопасными
- Делегаты допускают передачу методов в качестве параметров.
- Делегаты можно использовать для определения методов обратного вызова (callback'ов).
- Делегаты можно связывать друг с другом; например, при появлении одного события можно вызывать несколько методов.
В следующем примере показано объявление делегата:
delegate возвращаемый_тип имя_делегата (список_параметров);
Делегату можно назначить любой метод из любого доступного класса или структуры, соответствующей типу делегата. Этот метод должен быть статическим методом или методом экземпляра. Это позволяет программно изменять вызовы метода, а также включать новый код в существующие классы.
Рассмотрим базовый пример объявления и вызова делегата:
delegate void TestDelegate();
static void Voice()
{
Console.WriteLine("make XGM great again");
}
static void Main(string[] args)
{
TestDelegate del = Voice;
del();
Console.Read();
}
Воj время выполнения программы вызывается метод Voice через делегат TestDelegate, благодаря чему мы увидим на консоли вывод "make XGM great again".
Зададим делегату и ссылаемому методу входные параметры:
delegate void TestDelegate(string a, string b);
static void Voice(string lover, string who)
{
Console.WriteLine(lover + " love "+who);
}
static void Main(string[] args)
{
TestDelegate del = Voice;
del("Msey","Goose"); // выведет: Msey love Goose
Console.Read();
}
А затем зададим им возвращаемое значение и слегка изменим логику отображения результата:
delegate string TestDelegate(string a, string b);
static string Voice(string lover, string who)
{
return lover + " love "+who;
}
static void Main(string[] args)
{
TestDelegate del = Voice;
string example = del("Msey","Goose");
Console.WriteLine(example); // выведет: Msey love Goose
Console.Read();
}
Преобразование делегируемых методов:
С самых ранних версий C# была введена возможность преобразования делегируемых методов, позволяющая присвоить имя метода делегату не прибегая к оператору new или вызову конструктора делегата. Рассмотрим пример данного синтаксического сахара:
delegate string TestDelegate();
static string Bad()
{
return "Voice from Bad method";
}
static string Good()
{
return "Voice from Good method";
}
static void Main(string[] args)
{
TestDelegate del = Bad;
string example;
example = del();
Console.WriteLine(example); // Voice from Bad method
del = Good;
example = del();
Console.WriteLine(example); // Voice from Good method
Console.Read();
}
Также у делегатов существует полезное свойство - это групповая адресация, предоставляющая возможность создать список или цепочку вызовов для методов, которые вызываются автоматически при обращении к делегату. Создать такую цепочку нетрудно. Для этого достаточно получить экземпляр делегата, а затем добавить методы в цепочку с помощью оператора + или += или для удаления через - или -= соответственно. Если делегат возвращает значение, то им становится значение, возвращаемое последним методом в списке вызовов. Поэтому делегат, в котором используется групповая адресация, обычно имеет возвращаемый тип void.
Рассмотрим применение данного приема:
delegate void TestDelegate();
static void Bad()
{
Console.WriteLine("Voice from Bad method");
}
static void Good()
{
Console.WriteLine("Voice from Good method");
}
static void Neutral()
{
Console.WriteLine("Voice from Neutral method");
}
static void Main(string[] args)
{
TestDelegate del = Neutral;
del += Bad;
del += Good;
del();
Console.Read();
}
Вывод выглядит следующим образом:
Voice from Neutral method
Voice from Bad method
Voice from Good method
Также возможен вариант делегирование методов экземпляра класса, который вы можете протестировать самостоятельно.
ковариантность и контравариантность делегатов (сложность: средняя)
Благодаря ковариантности и контравариантности делегирование методов становится еще более гибким средством в программировании. Как правило, метод, передаваемый делегату, должен иметь такой же возвращаемый тип и сигнатуру, как и делегат. Но в отношении производных типов это правило оказывается не таким строгим благодаря ковариантности и контравариантности. В частности, ковариантность позволяет присвоить делегату метод,
возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата.
Чтобы было более понятно, приведу пример с ковариантностью и контравариантностью по-отдельности.
возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата.
Чтобы было более понятно, приведу пример с ковариантностью и контравариантностью по-отдельности.
delegate void TestDelegate<out T>();
class ParentClass
{
public virtual void ShowMe()
{
Console.WriteLine("Привет из ParentMethod");
}
}
class ChildClass : ParentClass
{
public override void ShowMe()
{
Console.WriteLine("Привет из ChildMethod");
}
}
static void Main(string[] args)
{
TestDelegate<ParentClass> parent = new ParentClass().ShowMe;
TestDelegate<ChildClass> child = new ChildClass().ShowMe;
parent(); //Привет из ParentMethod
child(); //Привет из ChildMethod
parent = child; // ковариантность
parent(); //Привет из ChildMethod
child(); //Привет из ChildMethod
Console.Read();
}
А пример с контравариантностью делегатов выглядит следующим образом:
delegate void TestDelegate<in T>();
class ParentClass
{
public virtual void ShowMe()
{
Console.WriteLine("Привет из ParentMethod");
}
}
class ChildClass : ParentClass
{
public override void ShowMe()
{
Console.WriteLine("Привет из ChildMethod");
}
}
static void Main(string[] args)
{
TestDelegate<ParentClass> parent = new ParentClass().ShowMe;
TestDelegate<ChildClass> child = new ChildClass().ShowMe;
parent(); //Привет из ParentMethod
child(); //Привет из ChildMethod
child = parent; // контравариантность
parent(); //Привет из ParentMethod
child(); //Привет из ParentMethod
Console.Read();
}
Типы делегатов являются запечатанными — от них нельзя наследовать, а от Delegate нельзя создавать производные пользовательские классы. Поскольку созданный экземпляр делегата является объектом, его можно передавать как параметр или назначать свойству. Это позволяет методу принимать делегат в качестве параметра и вызывать делегат в дальнейшем. Эта процедура называется асинхронным обратным вызовом и обычно используется для уведомления вызывающего объекта о завершении длительной операции. Когда делегат используется таким образом, коду, использующему делегат, не требуются сведения о реализации используемого метода. Данные функциональные возможности аналогичны возможностям, предоставляемым интерфейсами инкапсуляции.
Пример обратного вызова:
delegate void TestDelegate();
static void TestMethod(TestDelegate callback)
{
Console.WriteLine("Здесь, например, сработала операция 1");
callback();
Console.WriteLine("Здесь, например, сработала операция 2");
Console.WriteLine("Здесь, например, сработала операция 3");
callback();
}
static void Callback()
{
Console.WriteLine("Сработал обратный вызов");
}
static void Main(string[] args)
{
TestMethod(Callback);
Console.Read();
}
Результат очевиден:
Здесь, например, сработала операция 1
Сработал обратный вызов
Здесь, например, сработала операция 2
Здесь, например, сработала операция 3
Сработал обратный вызов
Анонимные методы
В версиях языка C# до 2.0 объявить делегат можно было только с помощью именованных методов. В версии C# 2.0 были представлены анонимные методы, в версии C# 3.0 и более поздних замененные лямбда-выражениями, которые теперь рекомендуется использовать для написания встроенного кода.
Анонимный метод - один из способов создания безымянного блока кода, связанного с конкретным экземпляром делегата. Для создания анонимного метода достаточно указать кодовый блок после ключевого слова delegate.
Пример:
delegate void TestDelegate();
static void Main(string[] args)
{
TestDelegate del;
del = delegate () { Console.WriteLine("Anonymous"); };
del(); // Вывод: Anonymous
Console.Read();
}
Теперь поговорим о событиях и их свойствах:
Событие представляет собой автоматическое уведомление о том, что произошло некоторое действие. События действуют по принципу работы паттерна проектирования "издатель-подписчик": объект, проявляющий интерес к событию, регистрирует обработчик этого события. Когда же событие происходит, вызываются все зарегистрированные обработчики этого события. Обработчики событий обычно представлены делегатами.
События похожи на свойства: внутри они содержат поле делегата, доступ напрямую к которому запрещен. Публичное поле делегата (или публичное свойство) может привести к тому, что список обработчиков события может быть очищен другим объектом, или что событие будет вызвано извне — в то время как мы хотим вызывать его только из исходного объекта.
Свойства представляют собой пару get/set методов. События же - это пара методов add/remove, подробности о которых вы можете прочитать здесь.
События являются членами класса и объявляются с помощью ключевого слова event. Чаще всего для этой цели используется следующая форма:
event делегат_события название_события;
Свойства событий:
- Издатель определяет, когда возникает событие; подписчики определяют, какое действие выполняется в ответ на событие.
- У события может быть несколько подписчиков. Подписчик может обрабатывать несколько событий от нескольких издателей.
- События, не имеющие подписчиков, никогда не возникают.
- Обычно события используются для оповещения о действиях пользователя, например нажатиях кнопок или выборе пунктов меню в графических пользовательских интерфейсах.
- Если событие имеет несколько подписчиков, при возникновении события обработчики событий вызываются синхронно.
- В библиотеке классов .NET Framework события основываются на делегате EventHandler и базовом классе EventArgs.
Рассмотрим базовый пример использования события:
class Handler
{
public delegate void TestDelegate(); // объявляем делегат
public event TestDelegate myEvent; // объявляем событие
string unitState = null; // объявляем строку-состояние юнита
public void CreateUnit()
{
myEvent += ShowUnitState; // подписываемся на событие о выводе состояния юнита
unitState = "living Msey"; // задаем состояние юнита как живой юнит
myEvent(); // вызываем событие вывода состояния юнита
}
public void KillUnit()
{
unitState = "dead Msey"; // задаем состояние юнита как мертвый юнит
myEvent(); // вызываем событие вывода состояния юнита
myEvent -= ShowUnitState; // отписываемся от события о выводе состояния юнита
}
private void ShowUnitState()
{
Console.WriteLine("unit is: "+ unitState); // выводим состояние юнита
}
}
static void Main(string[] args)
{
Handler handler = new Handler(); // создаем класс обработчик
handler.CreateUnit(); // создаем юнита
handler.KillUnit(); // "убиваем" его
Console.Read();
}
Вывод:
unit is: living Msey
unit is: dead Msey
Также можно подписку и отписку записать в виде анонимных функций:
myEvent += () => { Console.WriteLine("unit is: " + unitState); }; ;
и
myEvent -= () => { Console.WriteLine("unit is: " + unitState); }; ;
Функторы и действия
В .NET есть несколько встроенных делегатов, которые используются в различных ситуациях. И наиболее используемыми, с которыми часто приходится сталкиваться, являются Action и Func.
Делегат Action является обобщенным, принимает до 16 параметров и не возвращает значение.
Пример:
static void Main(string[] args)
{
Action<string> action;
action = (s)=>{ Console.Write(s); };
action("XGM.GURU");
Console.Read();
}
Делегат Func является обобщенным, принимает до 16 параметров и возвращает значение.
// Пример использования функтора только с выходным параметром:
static void Main(string[] args)
{
Func<string> func;
func= ()=>{ return "XGM.GURU"; };
Console.WriteLine(func); // XGM.GURU
Console.Read();
}
//Бывают случаи, когда надо передать параметры в функтор, и, уже на основе их возвращать из него значения:
static void Main(string[] args)
{
Func<string, string, string, string> func;
func = (s1, s2, s3) => { return s1 + s2 + s3 + "AGAIN"; };
Console.WriteLine(func("MAKE ", "XGM ", "GREAT ")); // MAKE XGM GREAT AGAIN
Console.Read();
}
Ред. Isstrebitel
Даже не помню уже, почему у меня такое не получалось, должен же был хоть попробовать
А с тех пор только на плюсах писал, вот так вот -_-
С с++ не работал, поэтому, с учетом отсутствия примера, опровергающего это: Я остановлюсь на мсдн'овском варианте.
# объяснить подробнее про вычитание списка делегатов
Например, разъяснить почему:
рассказать про аллокации, связанные с лямбдами, когда они происходят и почему, а так же почему то что оно аллоцирует может быть проблемой
рассказать про неявный захват переменных
ну и про замыкания само собой, т.е. почему вот это выведет 333
Ред. ScorpioT1000
Ред. Devion
На выходе компилятора будет примерно вот такое:
Так же можно создать переменную со значением и записать туда i, и уже эту переменную прокинуть в лямбду, в этом случае будет создаваться отдельный экземпляр делегата каждый виток цикла (но в старых версиях компилятора это будет работать иначе, что вроде как баг, ибо для пользователя оно по факту должно выглядеть как "передача ссылки").
Бтв, тут всплывает тема с аллокацией, т.к. как ты можешь заметить создание экземпляра каждый виток цикла это дичь )