Программирование: C# Делегаты и события

» Раздел: C#

В данной статье будет вкратце обсуждаться проблемная для новичков тема делегатов, событий, анонимных методов, функторов и действий.
Делегатом является объект ссылочного типа, предоставляющий ссылки на метод или группу методов приложения.
Делегаты имеют следующие свойства:
  • Делегаты похожи на указатели функций в 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();
        }

Просмотров: 645

» Лучшие комментарии


Isstrebitel #1 - 5 месяцев назад (отредактировано ) 0
За ковариантность и контравариантность огромное спасибо, месяца два назад наткнулся именно на такое, где прекрасно понимал, что "вот бы такое было" - без этого было крайне неудобно, но не знал, что такое есть и так называется.
Даже не помню уже, почему у меня такое не получалось, должен же был хоть попробовать
А с тех пор только на плюсах писал, вот так вот -_-
Doc #2 - 5 месяцев назад 4
Делегаты похожи на указатели функций в C++, но являются объектно-ориентированными и типобезопасными
Понимаю, что перевод с МСДН, но бред. Функшн поинтеры в С++ вполне себе типобезопасны.
Msey #3 - 5 месяцев назад 0
Doc:
С с++ не работал, поэтому, с учетом отсутствия примера, опровергающего это:
Delegates are like C++ function pointers but are type safe.
Я остановлюсь на мсдн'овском варианте.
ScorpioT1000 #4 - 5 месяцев назад 0
Функшн поинтеры в С++ вполне себе типобезопасны.
То же хотел сказать) либо имели ввиду C поинтеры
Devion #5 - 5 месяцев назад 0
Имхо еще что можно сделать по теме, чуть посложнее чем для новичков, но интересно:
# объяснить подробнее про вычитание списка делегатов
Например, разъяснить почему:
  Action a = () => Console.Write("A");
  Action b = () => Console.Write("B");
  Action c = () => Console.Write("C");
  Action s = a + b + c;
  (s - (b + c))();      //A
  //но при этом
  (s - (a + c))();      //ABC
Упомянуть про порядок вычитания, например
s = a + b + a;
(s - a)(); //AB, а не BA
# рассказать про аллокации, связанные с лямбдами, когда они происходят и почему, а так же почему то что оно аллоцирует может быть проблемой
например, как тут:
    string str = "str1";
    Action d1 = () => Debug.Log(str); //аллоцирует при каждом вызове метода, который объявляет d1
    Action d2 = () => Debug.Log("str1"); //аллоцирует лишь однажды
# рассказать про неявный захват переменных
Например объяснить, почему если очистить 'd1', 'a' останется в памяти до очистки 'd2'
    var a = new object();;
    var b = new object();
    
    Action d1 = () =>
    {
        Debug.Log(a);
        Debug.Log(b);
    };
    Action d2 = () => Debug.Log(b);
# ну и про замыкания само собой, т.е. почему вот это выведет 333
    var actions = new List<Action>();
    for (int i = 0; i < 3; i++)
        actions.Add(() => Console.Write(i));

    foreach (var action in actions)
        action();
ScorpioT1000 #6 - 5 месяцев назад (отредактировано ) 0
Про последнее, оно замыкает по ссылке даже базовый тип? Хотя вроде это везде так вроде, кроме пхп и вроде c++11, там надо явно указать.
Devion #7 - 5 месяцев назад (отредактировано ) 0
даже базовый тип ничего не сделает, т.к. под капотом 'i' будет находиться в экземпляре делегата, и это будет один и тот же экземпляр во всех элементах списка.
На выходе компилятора будет примерно вот такое:
    public void Do()
    {
      List<Action> actionList = new List<Action>();
      DisplayClass displayClass = new DisplayClass();
      for (displayClass.i = 0; displayClass.i < 3; displayClass.i++)
      {
        actionList.Add(new Action((object) displayClass, __methodptr(<Do>b__0)));
      }
    }

    [CompilerGenerated]
    private sealed class DisplayClass
    {
      public int i;

      internal void <Do>b__0()
      {
        Console.Write((object) this.i);
      }
    }
т.к. вся лямбда внутри контекста захваченных переменных будет формировать отдельный класс то по факту замыкания не будет, если вынести лямбду в отдельный от цикла метод.
Так же можно создать переменную со значением и записать туда i, и уже эту переменную прокинуть в лямбду, в этом случае будет создаваться отдельный экземпляр делегата каждый виток цикла (но в старых версиях компилятора это будет работать иначе, что вроде как баг, ибо для пользователя оно по факту должно выглядеть как "передача ссылки").
Бтв, тут всплывает тема с аллокацией, т.к. как ты можешь заметить создание экземпляра каждый виток цикла это дичь )