Делегаты и их использование на манер событий

166
22 декабря 2019, 02:10

Копаясь в книге Троэлсена по-поводу делегатов заметил такую забавную штуку что делегаты могут использоваться на манер событий и собсвтенно ввесь вопрос у меня в методе регистратора делегата который выглядит следующим образом

public class Car
{
    //  1.  Определить  тип  делегата.
    public delegate void CarEngineHandler(string msgForCaller);
    //  2.  Определить  переменную-член  типа  этого  делегата.
    private CarEngineHandler listOfHandlers;
    //  3.  Добавить  регистрационную  функцию  для  вызывающего  кода. 
    public void RegisterWithCarEngine(CarEngineHandler methodToCall)
    {
        listOfHandlers = methodToCall;
    }
}

Здесь метод RegisterWithCarEngine принимает объект делегата в в качестве параметра и затем дальше определяем метод для вызова делегата

//  4.  Реализовать  метод  Accelerate()  для  обращения
//  к  списку  вызовов  делегата  при  нужных  условиях.
public void Accelerate(int delta)
{
    //  Если  этот  автомобиль  сломан,  отправить  сообщение  об  этом.
    if (carIsDead)
    {
        if (listOfHandlers != null)
            listOfHandlers("Sorry,  this  car  is  dead...");
    }
    else
    {
        CurrentSpeed += delta;
        //  Автомобиль  почти  сломан?
        if (10 == (MaxSpeed - CurrentSpeed) && listOfHandlers != null)
        {
            listOfHandlers("Careful  buddy!  Gonna  blow!");
        }
        if (CurrentSpeed >= MaxSpeed)
            carIsDead = true;
        else
            Console.WriteLine("CurrentSpeed=  {0}", CurrentSpeed);
    }
}

И после всего этого в main'е вызываем метод для регистрации делегата,но сделано это не совсем для меня привычным образом так вроде как передаётся анонимный объект делегата(путём вызова конструктора и передаче ему имени метода) который затем присваивается тому полю что объявлено в классе для хранения информации о том что за метод будет вызван при наступлении события(здесь как бы делегатами это сделано поэтому не путайте с событиями) и вот собственно код который вызвал у меня вопрос и я хотел бы уточнить верно ли что передаётся анонимный объект делегата

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("*****  Delegates  as  event  enablers  *****\n");
        //  Сначала  создать  объект  Car.
        Car cl = new Car("SlugBug", 100, 10);
        //  Теперь  сообщить  ему,  какой  метод  вызывать,
        //  когда  он  захочет  отправить  сообщение.
        cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
        //  Ускорить  (это  инициирует  события).
        Console.WriteLine("*****  Speeding  up  *****");
        for (int i = 0; i < 6; i++)
            cl.Accelerate(20);
        Console.ReadLine(); 
    }
    //  Это  цель  для  входящих  сообщений.
    public static void OnCarEngineEvent(string msg)
    {
        Console.WriteLine("\n*****  Message  From  Car  Object  *****");
        Console.WriteLine("=>  {0}", msg);
        Consоle.WriteLine("*  *  *\n");
    }
}

Чтобы прямо совсем конкретно то интересует строка с вызовом метода RegisterWithCarEngine и его аргумент которым по факту является вызов метода new что именно здесь произойдёт. Типа создастя объект делегата без имени и затем мы его передадим тому полю что есть в каждом объекте класса Car и таким образом запишем какой метод нужно вызывать(мне было бы понятнее если бы скажем передали переменную делегата,но здесь сделано так и я хочу уточнить верно ли понял)

Answer 1

В современных объектно-ориентированных языках принято правило всё является объектом. Это относится к Smalltalk, к Java и к C#.

Именно поэтому целые числа в C# это не примитивные сущности, а объекты класса System.Int32. То же самое и с указателями на функции, которые встречаются в C и C++. Для каждого такого указателя компилятор неявно генерирует класс-наследник от System.Delegate.

Взглянем на строку:

cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));

Здесь создаётся новый объект неявно сгенерированного класса Car.CarEngineHandler. Обратите внимание, что вы нигде явно этот класс не описывали, вы написали:

public delegate void CarEngineHandler(string msgForCaller);

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

Впрочем, в качестве синтаксического сахара вы можете не создавать объект явно и писать короткий код:

cl.RegisterWithCarEngine(OnCarEngineEvent);

Но в этом случае компилятор всё равно сделает то же самое, что вы писали вручную выше.

То есть да, в вашем коде будет создан объект-делегат, который будет содержать один адрес метода и этот объект будет скопирован в поле listOfHandlers.

Теперь о том, что из себя представляют делегаты. Исторически, когда придумывали C#, предполагали, что делегаты будут использоваться для реализации механизма событий, то есть в них можно будет хранить несколько методов-подписчиков. Виделось это так, что будут одиночные делегаты (single cast delegates), которые хранят только один адрес метода, и множественные делегаты (multi cast delegates), в которых хранятся много адресов методов.

Потом оказалось, что это всё усложняет, и делегаты в конечном счёте сделали универсальными. В .NET нет одиночных делегатов, в каждом делегате всегда может храниться много адресов методов. Тут наверное следует напомнить, что если вы используете метод класса (статический), то хранится только адрес метода, а если метод экземпляра, то хранится и адрес метода, и адрес подписанного объекта.

Теперь о том, как связаны между собой делегаты и события. Приблизительно также, как поля и свойства. К полю класса вы имеете полный доступ: можете его читать, изменять, получать его адрес. Свойства закрывают прямой доступ, и вы из всех возможностей получаете только возможность читать и писать, да и то не обязательно. Это позволяет вам не только скрывать и инкапсулировать реализацию, но ещё и позволяет применять в языке множественное наследование интерфейсов, за счёт того, что в интерфейсе нет полей, а есть только абстрактные виртуальные методы.

Точно также и событие (event) ограничивает доступ к делегату двумя операциями: добавление нового метода и удаление существующего метода, add и remove. Вы уже не можете, например, вызвать событие снаружи класса (только изнутри), и не можете очистить его полностью, хотя, если бы это было поле-делегат, вы могли бы вызывать его даже снаружи.

Ну, и как в случае со свойствами, вы можете описывать события в интерфейсах, но не можете описывать в них поля-делегаты.

Наконец, последнее, про операции += и -=. Это тоже синтаксический сахар. Делегаты — неизменяемые объекты (immutable), поэтому вы не можете добавить к делегату ещё один метод (+=). Когда вы так пишите, компилятор вызывает статический метод Delegate.Combine, который создаёт новый делегат из старого делегата и вашего метода. Но для события += означает, что компилятор вызовет неявно сгенерированный метод add для этого события.

Answer 2
listOfHandlers += rnethodToCall;
...
cl.RegisterWithCarEngine(OnCarEngineEvent);
READ ALSO
Задача на наследование C#

Задача на наследование C#

Создать такую иерархию классов A, B и C, чтобы код ниже компилировался и выводил текст "ABC"

145
Word. Как получить Заголовки С#

Word. Как получить Заголовки С#

Использую MicrosoftOffice

139