Делегаты и события в C#

Автор оригинала: Shadman Kudchikar
  • Перевод
Перевод статьи подготовлен специально для студентов курса «Разработчик С#».




Что такое события в C#?


Событие может быть использовано для предоставления уведомлений. Вы можете подписаться на событие, если вам нужны эти уведомления. Вы также можете создавать свои собственные события, которые будут уведомлять вас о том, что случилось что-то, что вас интересует. .NET Framework предлагает встроенные типы, которые можно использовать для создания событий. Используя делегаты, лямбда-выражения и анонимные методы, вы можете создавать и использовать события удобным способом.

Понимание делегатов в C#


В C# делегаты образуют основные строительные блоки для событий. Делегат — это тип, который определяет сигнатуру метода. Например, в C++ это можно сделать с помощью указателя на функцию. В C# вы можете создать экземпляр делегата, указывающий на другой метод. Вы можете вызвать этот метод через экземпляр делегата.

Ниже приведен пример объявления делегата и вызова метода через него.

Использование делегата в C#


}class Program
{
    public delegate double MathDelegate(double value1, double value2);

    public static double Add(double value1, double value2)
    {
        return value1 + value2;
    }
    public static double Subtract(double value1, double value2)
    {
        return value1 - value2;
    }
        Console.ReadLine();

    public static void Main()
    {
        MathDelegate mathDelegate = Add;
        var result = mathDelegate(5, 2);
        Console.WriteLine(result);
        // вывод: 7

        mathDelegate = Subtract;
        result = mathDelegate(5, 2);
        Console.WriteLine(result);
        // вывод: 3

    }

Как видите, мы используем ключевое слово delegate, чтобы сообщить компилятору, что мы создаем тип делегата.

Инстанцировать делегаты легко вместе с автоматическим созданием нового типа делегата.

Для создания делегата вы также можете использовать ключевое слово new.

MathDelegate mathDelegate = new MathDelegate(Add);

Инстанцированный делегат является объектом; Вы можете также использовать его и передавать в качестве аргумента другим методам.

Многоадресные делегаты в C#


Еще одна замечательная особенность делегатов в том, что вы можете объединять их вместе. Это называется многоадресной передачей (multicasting). Вы можете использовать оператор + или +=, чтобы добавить другой метод в список вызовов существующего экземпляра делегата. Аналогично, вы также можете удалить метод из списка вызовов, используя оператор присваивания декремента (- или -=). Эта особенность служит основой для событий в C#. Ниже приведен пример многоадресного делегата.

class Program
{
    static void Hello(string s)
    {
        Console.WriteLine("  Hello, {0}!", s);
    }

    static void Goodbye(string s)
    {
        Console.WriteLine("  Goodbye, {0}!", s);
    }

    delegate void Del(string s);

    static void Main()
    {
        Del a, b, c, d;

        // Создаем делегат a ссылающийся на метод  Hello:
        a = Hello;

        // Создаем делегат b ссылающийся на метод  Goodbye:
        b = Goodbye;

        // Формируем композицию делегатов a и b - c: 
        c = a + b;

                // Удаляем a из композиции делегатов c, создавая делегат d, который в результате вызывает только метод  Goodbye:
        d = c - a;

        Console.WriteLine("Invoking delegate a:");
        a("A");
        Console.WriteLine("Invoking delegate b:");
        b("B");
        Console.WriteLine("Invoking delegate c:");
        c("C");
        Console.WriteLine("Invoking delegate d:");
        d("D");


        /* Вывод:
        Invoking delegate a:
          Hello, A!
        Invoking delegate b:
          Goodbye, B!
        Invoking delegate c:
          Hello, C!
          Goodbye, C!
        Invoking delegate d:
          Goodbye, D!
        */

        Console.ReadLine();
    }
}

Это возможно, поскольку делегаты наследуются от класса System.MulticastDelegate, который, в свою очередь, наследуется от System.Delegate. Вы можете использовать члены, определенные в этих базовых классах для ваших делегатов.

Например, чтобы узнать, сколько методов будет вызывать многоадресный делегат, вы можете использовать следующий код:

int invocationCount = d.GetInvocationList().GetLength(0);

Ковариантность и контравариантность в C#


Когда вы назначаете делегату метод, сигнатура метода не обязательно должна точно соответствовать делегату. Это называется ковариацией и контравариантностью. Ковариация позволяет методу иметь более производный тип возвращаемого значения, нежели тот, который определен в делегате. Контравариантность разрешает метод с типами параметров, которые являются менее производными, чем типы в делегате.

Ковариация в делегатах


Вот пример ковариации,

class Program
{
    public delegate TextWriter CovarianceDel();

    public static StreamWriter MethodStream() { return null;  }
    public static StringWriter MethodString() { return null;  }

    static void Main()
    {
        CovarianceDel del;

        del = MethodStream;
        del = MethodString;

        Console.ReadLine();
    }
}

Поскольку и StreamWriter, и StringWriter наследуются от TextWriter, вы можете использовать CovarianceDel с обоими методами.

Контравариантность в делегатах


Ниже приведен пример контравариантности.

class Program
{
    public static void DoSomething(TextWriter textWriter) { }
    public delegate void ContravarianceDel(StreamWriter streamWriter);

    static void Main()
    {
        ContravarianceDel del = DoSomething;

        Console.ReadLine();
    }
}

Поскольку метод DoSomething может работать с TextWriter, он, безусловно, может работать и с StreamWriter. Благодаря контравариантности вы можете вызывать делегат и передавать экземпляр StreamWriter в метод DoSomething.

Вы можете узнать больше об этой концепции здесь.

Лямбда-выражения в C#


Иногда вся сигнатура метода может требовать больше кода, чем само тело метода. Существуют также ситуации, в которых вам нужно создать целый метод только для того, чтобы использовать его в делегате.

Для этих случаев Microsoft добавила некоторые новые возможности в C#, например, анонимные методы в 2.0. В C# 3.0 дела стали обстоять еще лучше, когда были добавлены лямбда-выражения. Лямбда-выражение является предпочтительным способом при написании нового кода.

Ниже приведен пример новейшего лямбда-синтаксиса.

class Program
{
    public delegate double MathDelegate(double value1, double value2);

    public static void Main()
    {
        MathDelegate mathDelegate = (x,y) => x + y;
        var result = mathDelegate(5, 2);
        Console.WriteLine(result);
        // вывод: 7

        mathDelegate = (x, y) => x - y; ;
        result = mathDelegate(5, 2);
        Console.WriteLine(result);
        // вывод: 3

        Console.ReadLine();
    }

}

Для чтения этого кода вам нужно использовать слово “следует” в контексте специального лямбда-синтаксиса. Например, первое лямбда-выражение в вышеприведенном примере читается как «x и y следуют к сложению x и y».

Лямбда-функция не имеет конкретного имени в отличии от метода. Из-за этого лямбды называются анонимными функциями. Вам также не нужно явно указывать тип возвращаемого значения. Компилятор предполагает его автоматически из вашей лямбды. И в случае вышеприведенного примера типы параметров x и y также не указаны явно.

Вы можете создавать лямбды, которые охватывают несколько операторов. Вы можете сделать это, добавив фигурные скобки вокруг операторов, которые образуют лямбду, как показано в примере ниже.

MathDelegate mathDelegate = (x,y) => 
            {
                Console.WriteLine("Add");
                return x + y;
            };

Иногда объявление делегата для события кажется немного громоздким. Из-за этого .NET Framework имеет несколько встроенных типов делегатов, которые вы можете использовать при объявлении делегатов. В примере MathDelegate вы использовали следующий делегат:

public delegate double MathDelegate(double value1, double value2);

Вы можете заменить этот делегат одним из встроенных типов, а именно Func <int, int, int>.

вот так,

class Program
    {
        public static void Main()
        {
            Func<int, int, int> mathDelegate = (x,y) => 
            {
                Console.WriteLine("Add");
                return x + y;
            };

            var result = mathDelegate(5, 2);
            Console.WriteLine(result);
            // вывод: 7

            mathDelegate = (x, y) => x - y; ;
            result = mathDelegate(5, 2);
            Console.WriteLine(result);
            // вывод: 3

            Console.ReadLine();
        }

    }

Типы Func <...> можно найти в пространстве имен System. Они представляют делегаты, которые возвращают тип и принимают от 0 до 16 параметров. Все эти типы наследуются от System.MulticaseDelegate для того, чтобы вы могли добавить несколько методов в список вызовов.

Если вам нужен тип делегата, который не возвращает значение, вы можете использовать типы System.Action. Они также могут принимать от 0 до 16 параметров, но не возвращают значение.

Вот пример использования типа Action,

class Program
    {
        public static void Main()
        {
            Action<int, int> mathDelegate = (x,y) => 
            {
                Console.WriteLine(x + y);
            };

            mathDelegate(5, 2);
            // вывод: 7

            mathDelegate = (x, y) => Console.WriteLine(x - y) ;
            mathDelegate(5, 2);
            // вывод: 3

            Console.ReadLine();
        }

    }

Вы можете узнать больше о встроенных делегатах .NET здесь.

Все усложняется, когда ваша лямбда-функция начинает ссылаться на переменные, объявленные вне лямбда-выражения или на this. Обычно, когда элемент управления покидает область действия переменной, переменная становится недействительной. Но что, если делегат ссылается на локальную переменную. Чтобы исправить это, компилятор генерирует код, который продлевает срок службы захваченной переменной, по крайней мере, до тех пор, пока живет самый долгоживущий делегат. Это называется замыканием.

Вы можете узнать больше о замыканиях здесь.

События в C#


Рассмотрим популярный шаблон разработки — издатель-подписчик (pub/sub). Вы можете подписаться на событие, а затем вы будете уведомлены, когда издатель события инициирует новое событие. Эта система используется для установления слабой связи между компонентами в приложении.

Делегат формирует основу для системы событий в C#.

Событие — это особый тип делегата, который облегчает событийно-ориентированное программирование. События — это члены класса, которые нельзя вызывать вне класса независимо от спецификатора доступа. Так, например, событие, объявленное как public, позволило бы другим классам использовать += и -= для этого события, но запуск события (то есть вызов делегата) разрешен только в классе, содержащем событие. Давайте посмотрим на пример,

//Определяем класс-издатель как Pub
public class Pub
{
    //Свойство OnChange содержит список всех callback-методов подписчиков 
    public event Action OnChange = delegate { };

    public void Raise()
    {
        //Вызов OnChange
        OnChange();
    }
}

Затем метод в другом классе может подписаться на событие, добавив один из его методов в делегат события:

Ниже приведен пример, показывающий, как класс может предоставить открытый делегат и генерировать событие.

class Program
{
    static void Main(string[] args)
    {
        //Инициализируем объект класса pub
        Pub p = new Pub();

        //подписываем вывод Subscriber 1 на событие OnChange 
        p.OnChange += () => Console.WriteLine("Subscriber 1!");
        //подписываем вывод Subscriber 2 на событие OnChange
        p.OnChange += () => Console.WriteLine("Subscriber 2!");

        //генерируем событие
        p.Raise();

        //После вызова метода Raise() все подписанные callback-методы та же будут вызваны

        Console.WriteLine("Press enter to terminate!");
        Console.ReadLine();
    }
}

Даже если событие объявлено как public, оно не может быть запущено напрямую нигде, кроме как в классе, в котором оно находится.

Используя ключевое слово event, компилятор защищает наше поле от нежелательного доступа.

А также,

Он не позволяет использовать = (прямое назначение делегата). Следовательно, ваш код теперь защищен от риска удаления предыдущих подписчиков, используя = вместо +=.

Кроме того, вы могли заметить специальный синтаксис инициализации поля OnChange для пустого делегата, такого как delegate { }. Это гарантирует, что наше поле OnChange никогда не будет null. Следовательно, мы можем удалить null-проверку перед тем, как вызвать событие, если нет других членов класса, делающих его null.

Когда вы запускаете вышеуказанную программу, ваш код создает новый экземпляр Pub, подписывается на событие двумя разными методами и генерирует событие, вызывая p.Raise. Класс Pub совершенно не осведомлен ни об одном из подписчиков. Он просто генерирует событие.

Вы также можете прочитать мою статью «Шаблон проектирования издатель-подписчик в C#» для более глубокого понимания этой концепции.

Что ж, на этом пока это все. Надеюсь, вы уловили идею. Спасибо за прочтение поста. Пожалуйста, дайте мне знать, если есть какие-либо ошибки или необходимы изменения в комментарии ниже. Заранее спасибо!

Полезные ссылки


www.c-sharpcorner.com/blogs/c-sharp-generic-delegates-func-action-and-predicate
docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance
https://web.archive.org/web/20150707082707/http://diditwith.net/PermaLink,guid,235646ae-3476-4893-899d-105e4d48c25b.aspx
  • +21
  • 6,2k
  • 9
OTUS. Онлайн-образование
600,13
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 9

  • НЛО прилетело и опубликовало эту надпись здесь
      –11
      Вот чем меня раздражает C#, так это огромным количеством неявных вызовов. Серьёзно, в мало-мальски крупном проекте разобраться, что и в каком порядке вызывается (просто чтоб понять алгоритм работы), практически невозможно без запуска отладчика.
      Потому что там везде идёт оверюз композиции, причём в рантайме. И кругом тонны мелких методов, которые вызывают и перевызывают друг друга весьма неочевидным образом, пока голова кругом не пойдёт.
      C# код приятно писать, но это write-only код. Читать и разбираться в нём по-настоящему тяжко, если приложение хоть немного сложнее Hello World.
        +8
        Вы, наверное, путаете C# с каким-то другим языком программирования. Write-only — это регулярные выражения, в худшем случае Perl, но никак не C#.
          0

          регулярные выражения — намного легче читать, чем state-машину написанную на switch/case.
          Лучше регулярок для обработки строк пока ничего лучше не придумали.

            0
            Наверное, это дело вкуса. Ничего не имею против обработки строк регулярками, но на самом деле сложные выражения (с бэктрегингом и т. п.) читать и понимать лично для меня сложнее, чем код на C#.
          +4
          Не путайте говноязык с говнокодом. Навалять Наваять говнокод можно на любом, самом-самом распрекрасном языке. По мнению очень многих специалистов, как раз C# очень стройный, красивый и лаконичный язык. (Оставим за скобками новомодные изменения в 8-й версии.)
            0
            Непонятно, при чем тут язык? Хотелось бы увидеть конкреный пример, есть подозрениие, что во всем виноват программист Вася.
            +1
            Не рассказали, чем, собственно, отличается
            MathDelegate mathDelegate = Add;

            от
            MathDelegate mathDelegate = new MathDelegate(Add);


            И было бы круто привести список встроенных делегатов: Action, Func, Predicate, EventHandler и что там ещё есть.
              +2
              Не рассказали, чем, собственно, отличается
              MathDelegate mathDelegate = Add;

              от
              MathDelegate mathDelegate = new MathDelegate(Add);

              Если не ошибаюсь, то это синтаксический сахар. Компилятор первый пример кода превращает во второй при компиляции.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое