Создано при помощи нейросети Kandinsky

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

User Rating: 0 / 5

Изображение для статьи создано при помощи нейросети Kandinsky

Делегаты являются одним из ключевых инструментов в языке программирования C#, позволяя передавать методы как параметры, хранить ссылки на методы и вызывать методы динамически. Давайте детальнее рассмотрим, что такое делегаты, посмотрим на их ключевые особенности, ограничения и примеры использования в коде.

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

Что такое делегат? Примеры объявления делегатов в C#

Делегат в C# - это тип данных, который представляет собой ссылку на метод с заданным списком параметров и типом возвращаемого значения. Он позволяет передавать методы как параметры другим методам, сохранять ссылки на методы и вызывать методы динамически во время выполнения программы.

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

Давайте рассмотрим небольшой пример объявления делегата:

public delegate void InputStringParamDelegate(string param);

В данном случае мы объявили делегат с именем InputStringParamDelegate, которому можно впоследствии установить ссылку на какой-то метод, у которого будет тип возвращаемого значения void и который принимает единственный параметр с типом string. Обратите внимание на использование ключевого слова delegate в объявлении делегата.

Представим теперь, что у нас есть два метода PrintRegularMessage и PrintErrorMessage:

public static void PrintRegularMessage(string message) {
	Console.WriteLine(string.Format("Обычное сообщение: {0}", message));
}

public static void PrintErrorMessage(string errorMessage) {
	Console.WriteLine(string.Format("Сообщение об ошибке: {0}", errorMessage));
}

Как видим, первый предназначен для вывода обычного сообщения на экран консольного приложения C#, второй - для вывода сообщения об ошибке.

Также у обоих методов тип возвращаемого значения void, и оба метода принимают на вход единственный параметр с типом string - что полностью совместимо с типом нашего делегата InputStringParamDelegate.

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

// Объявили переменную myDelegate с типом делегата InputStringParamDelegate 
InputStringParamDelegate myDelegate;

// Установили переменной ссылку на метод PrintRegularMessage
myDelegate = PrintRegularMessage;

// Вызвали делегат, передав ему в параметре строку "Моё обычное сообщение"
myDelegate("Моё обычное сообщение");	

// Теперь установили переменной ссылку на другой метод PrintErrorMessage
myDelegate = PrintErrorMessage;

// Снова вызываем делегат, на этот раз с другим сообщением
myDelegate("Моё сообщение об ошибке"); 

Если мы запустим этот небольшой пример, оформив его в виде консольного приложения на C#, то мы увидим в выводе  консоли следующие два сообщения:

Обычное сообщение: Моё обычное сообщение
Сообщение об ошибке: Моё сообщение об ошибке

Как можно видеть из вывода на консоль, фактически мы последовательно вызвали сначала метод PrintRegularMessage, передав ему на вход строку "Моё обычное сообщение", а затем вызвали метод PrintErrorMessage, передав ему на вход строку "Моё сообщение об ошибке", но вызвали мы эти методы не напрямую, а через наш делегат, а именно переменную myDelegate с типом делегата InputStringParamDelegate.

Рассмотрим ещё несколько абстрактных примеров объявления делегата - уже с другими типами возвращаемых значений и типами входных параметров.

К примеру, такой делегат мог бы использоваться для вызова различных методов с типом int в качестве типа возвращаемого значения и принимающих на вход три параметра с типами string, int и double:

public delegate int ReturnIntValueDelegate(string message, int value, double precision);

Мы также можем не ограничиваться лишь примитивными типами данных, доступными в языке C#. Давайте предположим, что у нас где-то в программе объявлен собственный класс с именем MyClass, и мы бы теперь хотели объявить другой делегат, который может ссылаться на методы, возвращающие экземпляры данного класса и не принимающие никаких входных параметров. Тогда объявление делегата могло бы выглядеть следующим образом:

public delegate MyClass ReturnMyClassObjectDelegate();

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

// Статический метод CreateMyClassObject, который умеет создавать экземпляр класса MyClass, проводить с ним какую-то работу и затем возвращать этот экземпляр из метода
public static MyClass CreateMyClassObject() {
	MyClass myClassObject = new MyClass();
	
	// ... здесь можно провести необходимую работу с объектом myClassObject... 
	
	return myClassObject;
}

// Объявить переменную returnMyClassObjectDelegate и инициализировать её ссылкой на метод CreateMyClassObject:
ReturnMyClassObjectDelegate returnMyClassObjectDelegate = CreateMyClassObject;

// Вызывать делегат (фактически через делегат будет вызван метод CreateMyClassObject)
returnMyClassObjectDelegate();

 

Делегаты и лямбда-выражения

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

Давайте рассмотрим это на примере. Пусть мы объявили такой делегат:

public delegate double DoubleOperationDelegate(double a, double b);

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

Теперь посмотрим, как мы можем объявить переменную с типом делегата DoubleOperationDelegate и присваивать ей различные лямбда-выражения, описывающие разные арифметические операции:

// Объявили и инициализировали переменную op с типом нашего делегата DoubleOperationDelegate. Для инициализации используется лябмда-выражение, преобразующееся в тип делегата.
DoubleOperationDelegate op = (a, b) => a + b;

// Вызываем делегат и присваиваем результат операции переменной myResult1. Фактически через вызов делегата мы вызываем анонимную функцию, созданную нами через лямбда-выражение выше. Эта анонимная функция сложит значения переданных нами аргументов 3 и 2.
double myResult1 = op(3, 2); // Результат операции: 3 + 2, т.е. в myResult1 будет записано 5
Console.WriteLine(myResult1); // на консоль будет выведено 5

// Теперь мы присвоили переменной op другое лямбда-выражение и другую анонимную функцию.
op = (a, b) => a * b;

// Вызываем делегат и присваиваем результат новой анонимной функции в переменную myResult2. Результатом операции на этот раз будет 1.5 * 2 = 3

double myResult2 = op(1.5, 2);
Console.WriteLine(myResult2); // теперь на консоль будет выведено 3

 

Вариативность в делегатах - ковариация и контрвариантность

При работе с делегатами следует также знать про такую важную их особенность, как гибкость при сопоставлении типа делегата с сигнатурой метода.

  • ковариация (или ковариантность) - позволяет методу иметь тип возвращаемого значения, степень наследования которого больше, чем указано в делегате
  • контрвариантность - позволяет использовать метод с типами параметров, степень наследования которых меньше, чем у типа делегата

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

Пример ковариации (ковариантности):

Предположим, у нас есть родительский класс ParentClass и его класс-наследник ChildClass:

class ParentClass {
}

class ChildClass : ParentClass {
}

Теперь мы объявим делегат CovarianceDelegate, у которого тип возвращаемого значения - это ParentClass:

public delegate ParentClass CovarianceDelegate();

Пусть у нас также есть два метода, один из которых имеет тип возвращаемого значения ParentClass, а второй - ChildClass. И, положим, для простоты мы возвращаем из каждого метода новый экземпляр конкретного класса:

public static ParentClass GetParentClassObject() {
	return new ParentClass();
}

public static ChildClass GetChildClassObject() {
	return new ChildClass();
}

Теперь посмотрим, как проявляется ковариантность при создании переменных с типом нашего делегата CovarianceDelegate. Благодаря ей, несмотря на то, что в типе делегата возвращаемое значение задано как ParentClass, мы всё равно можем присвоить переменной delegateChildObj ссылку на метод GetChildClassObject, хотя его возвращаемый тип - это ChildClass, а не ParentClass:

CovarianceDelegate delegateParentObj = GetParentClassObject;

// За счёт ковариации данное присвоение разрешено:
CovarianceDelegate delegateChildObj = GetChildClassObject;	

В этом и есть основной смысл ковариантности. В нашем примере у метода GetChildClassObject для типа возвращаемого им значения степень наследования больше, чем в самом типе делегата, однако мы можем присваивать переменной делегата delegateChildObj ссылку на этот метод.

Пример контрвариантности:

Концепция поддержки контрвариантности у делегатов может часто встретиться на практике при рассмотрении реализации системы событий для различных элементов управления в приложениях Windows Forms. Для передачи параметров в методы-обработчики событий часто используется базовый класс System.EventArgs или производные от него. В данном случае контрвариантность у подобных делегатов выражается в том, что несмотря на то, что в параметрах типа для таких делегатов указываются типы-наследники от System.EventArgs, сам метод, ответственный за обработку события, может иметь меньшую степень наследования, т.е. в качестве типа параметра иметь System.EventArgs, а не производный от System.EventArgs класс, полностью совпадающий с типом параметра делегата.

Для лучшего понимания, давайте рассмотрим абстрактный пример кода, отражающий принцип контрвариантности. Пусть у нас есть базовый класс BaseClass и два его класса-наследника Child1OfBaseClass, Child2OfBaseClass.

Создадим класс-пример ContravarianceExampleClass, в котором объявим 2 делегата MyHandlerForChild1Delegate и MyHandlerForChild2Delegate. Оба делегата принимают по два параметра, и для второго параметра inputObject у них заданы типы Child1OfBaseClass и Child2OfBaseClass, соответственно. Также объявим в классе два события (event) с именами MyEventForChild1 и MyEventForChild2 с типами указанных делегатов. И в классе создадим метод MyMethodForDelegate, который вторым параметром принимает родительский класс BaseClass:

    public class BaseClass {
    }

    public class Child1OfBaseClass : BaseClass {
    }

    public class Child2OfBaseClass : BaseClass {
    }

    public class СontravarianceExampleClass {
        public delegate void MyHandlerForChild1Delegate(object sender, Child1OfBaseClass inputObject);
        public delegate void MyHandlerForChild2Delegate(object sender, Child2OfBaseClass inputObject);

        public event MyHandlerForChild1Delegate MyEventForChild1;
        public event MyHandlerForChild2Delegate MyEventForChild2;

        public void MyMethodForDelegate(object sender, BaseClass inputObject) {
            Console.WriteLine("Вызван MyMethodForDelegate, тип параметра inputObject: " + inputObject.GetType());
        }

        public void ShowExample() {
            // Мы можем использовать метод MyMethodForDelegate, у которого тип второго параметра - BaseClass,
            // хотя событие MyEventForChild1 ожидает тип для второго параметра Child1OfBaseClass
            MyEventForChild1 += MyMethodForDelegate;

            // Аналогичным образом, для события MyEventForChild2 можем использовать всё тот же метод
            // MyMethodForDelegate
            MyEventForChild2 += MyMethodForDelegate;

            MyEventForChild1?.Invoke(this, new Child1OfBaseClass());
            MyEventForChild2?.Invoke(this, new Child2OfBaseClass());
        }

В методе ShowExample() данного класса мы установим для обоих событий MyEventForChild1 и MyEventForChild2 целевой метод MyMethodForDelegate класса, а в конце метода ShowExample() мы просто вызовем оба события через метод Invoke, как показано в примере выше.

Обратите внимание на особенность контрвариантности: сам метод MyMethodForDelegate имеет тип BaseClass для второго входного параметра, однако мы можем без каких-либо ограничений использовать его для обоих событий класса, хотя типы данных для второго параметра у их делегатов основаны на классах-наследниках от BaseClass, а не на самом BaseClass.

Если теперь запустить простую программу на базе данного класса, создающую экземпляр класса-примера и вызывающую его метод ShowExample(),

СontravarianceExampleClass contravarianceExample = new СontravarianceExampleClass();
contravarianceExample.ShowExample();

то мы увидим следующий результат:

Вызван MyMethodForDelegate, тип параметра inputObject: DelegatesExample.Child1OfBaseClass
Вызван MyMethodForDelegate, тип параметра inputObject: DelegatesExample.Child2OfBaseClass

Как видим по выводу в консоль, реальный тип данных, который передаётся во время выполнения программы при вызове каждого из событий во второй параметр inputObject метода MyMethodForDelegate, равен Child1OfBaseClass в первом случае и Child2OfBaseClass во втором случае.

Повторно проводя параллель с реализацией системы событий в WindowsForms: BaseClass в этом синтетическом примере мог бы являться аналогом стандартного класса System.EventArgs, а классы-наследники Child1OfBaseClass, Child2OfBaseClass - могли бы представлять аналоги каких-то классов, производных от System.EventArgs, например, KeyEventArgs или MouseEventArgs. 

Многоадресность делегатов

Ещё одной интересной и полезной особенностью, которая есть у делегатов, является многоадресность. Это означает, что делегат при вызове может вызывать сразу несколько методов, а не один. Для того, чтобы добавить в список методов делегата другой дополнительный метод, нужно просто добавить два делегата при помощи операторов + или +=.

Рассмотрим это на примере. Пусть у нас объявлен делегат LogMessageDelegate, который может использоваться для вызова различных логирующих методов:

public delegate void LogMessageDelegate(string message);

Пусть у нас также есть два простых метода, один из которых выводит обычные сообщения, а другой - сообщения об ошибке:

public void LogError(string message) {
	Console.WriteLine("[Ошибка]: " + message);
}

public void LogMessage(string message) {
	Console.WriteLine("[Сообщение]: " + message);
}

Теперь создадим три переменных с типом нашего делегата и присвоим им ссылки на эти методы, а третьей переменной присвоим лямбда-выражение:

LogMessageDelegate logMethod1 = LogError;
LogMessageDelegate logMethod2 = LogMessage;
LogMessageDelegate logMethod3 = (message) => Console.WriteLine(string.Format("[Лог из лямбда-выражения]: {0}", message));

Посмотрим как работает многоадресность у делегатов: для этого мы создадим ещё одну переменную с типом нашего делегата и именем logAll и присвоим ей результат сложения logMethod1 и logMethod2, а затем при помощи оператора += добавим к ней и logMethod3:

LogMessageDelegate logAll = logMethod1 + logMethod2;
logAll += logMethod3;

В результате в переменной logAll будет сформирован список вызова из трёх методов, поэтому когда мы вызовем этот делегат следующим образом,

logAll("Это единое сообщение будет выведено при вызове сразу трёх методов");

то в консоли мы увидим сообщения от всех трёх методов:

[Ошибка]: Это единое сообщение будет выведено при вызове сразу трёх методов
[Сообщение]: Это единое сообщение будет выведено при вызове сразу трёх методов
[Лог из лямбда-выражения]: Это единое сообщение будет выведено при вызове сразу трёх методов 

Точно таким же образом мы можем убрать из списка вызова для делегата какой-либо из ранее добавленных методов посредством оператора -=, например так:

logAll -= LogError;

Console.WriteLine();
logAll("Теперь другое сообщение без лога для ошибки");

И теперь при запуске на экране консоли мы увидим ожидаемый результат:

[Сообщение]: Теперь другое сообщение без лога для ошибки
[Лог из лямбда-выражения]: Теперь другое сообщение без лога для ошибки

А сейчас давайте ещё раз посмотрим на некоторые особенности делегатов в C#, которые важны для понимания и работы с ними:

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

Заключение 

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

Правильное использование делегатов поможет упростить структуру программы и повысить ее эффективность.

Ссылка на архив с проектом для Microsoft Visual Studio, содержащий примеры кода из статьи: https://allineed.ru/our-products/download/4-allineed-ru-examples/34-csharp-delegates-example

 

Яндекс.Метрика