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

Определение: Делегат - это переменная ссылочного типа, которая может хранить ссылку на метод.

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

Синтаксис: Аналогичен объявлению метода в интерфейсе, за исключением того, что требует ключевое слово delegate.

Представьте, что у нас есть спутниковая система, обрабатывающая космические данные от далеких звезд. После обработки данных системе необходимо уведомить базовую станцию на Земле.

// Объявление делегата, под который подойдут все методы,
// которые принимают строку и ничего не возвращают
public delegate void CosmicDataProcessedCallback(string message);

class Program
{
    static void Main(string[] args)
    {
        var satellite = new Satellite();
        satellite.ProcessStarData(NotifyBaseStation);
    }

    // Обратите внимание, что этот метод как раз 
    // принимает строку и ничего не возвращает
    static void NotifyBaseStation(string notificationMessage)
    {
        Console.WriteLine($"Базовая станция получила: {notificationMessage}");
    }
}

public class Satellite
{
    public void ProcessStarData(CosmicDataProcessedCallback callback)
    {
        // Имитация обработки космических данных
        System.Threading.Thread.Sleep(3000);

        // После обработки данных вызываем наш метод, чтобы уведомить базовую станцию
        callback("Данные звезды из Альфа Центавра были успешно обработаны!");
    }
}

В этом примере:

  • Спутник обрабатывает данные звезды Альфа Центавра.

  • После обработки данных базовая станция на Земле уведомляется с помощью метода обратного вызова NotifyBaseStation.

  • Когда вы запускаете программу, после короткой задержки (имитирующей время, необходимое для обработки данных), вы увидите сообщение: "Базовая станция получила: Данные звезды из Альфа Центавра были успешно обработаны!".

Типы делегатов

  1. Одиночные делегаты (Single-cast delegates)

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

// Объявление делегата
public delegate void LogMessage(string message);

// Метод, соответствующий сигнатуре делегата
public void DisplayStarInfo(string star)
{
    Console.WriteLine($"Наблюдаем: {star}");
}

// Создание экземпляра делегата
LogMessage logger = DisplayStarInfo;
logger("Альфа Центавра");
// Вывод: "Наблюдаем: Альфа Центавра"
  1. Групповые делегаты (Multicast delegates)

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

public delegate void CosmicEvent();

public void DetectAsteroid()
{
    Console.WriteLine("Астероид обнаружен!");
}

public void ActivateShields()
{
    Console.WriteLine("Щиты активированы!");
}

// Создание группового делегата
CosmicEvent spaceEvent = DetectAsteroid;
spaceEvent += ActivateShields;

spaceEvent();
// Вывод:
// "Астероид обнаружен!"
// "Щиты активированы!"
  1. Универсальные делегаты: Func, Action и Predicate

В нашей космической аналогии Func - исследовательское судно, предназначенное для сбора конкретных данных и возврата результата исследований. В сигнатуре Func мы задаем типы входных параметров, а также тип выходного параметра, который всегда идет последним в списке. То есть Func<int, double, string> принимает параметры int и double, а возвращает строку.

Func<double, double, double> CalculateGravity = (mass, radius) => 
    (6.67430e-11 * mass) / (radius * radius);

double earthGravity = CalculateGravity(5.972e24, 6371e3);
Console.WriteLine($"Гравитация Земли примерно равна {earthGravity} м/с^2.");

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

Action<string> BroadcastSignal = (message) => 
{
    Console.WriteLine($"Трансляция: {message}");
};

BroadcastSignal("Мир и процветание всем существам!");
// Вывод: "Трансляция: Мир и процветание всем существам!"

Predicate можно представить как космический сканер, специально разработанный для получения ответа да/нет (правда/ложь) после сканирования.

Predicate<string> IsNebula = (celestialObject) => 
{
    return celestialObject.Contains("Туманность");
};

bool result = IsNebula("Туманность Ориона");
Console.WriteLine(result);  // Вывод: true

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

Где применяются делегаты?

  1. Обработка событий

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

public delegate void StarshipPlacementHandler(int x, int y);

public event StarshipPlacementHandler OnStarshipPlaced;

public void PlaceStarship(int x, int y)
{
    // Логика размещения космического корабля
    Console.WriteLine($"Космический корабль размещен на ({x}, {y})");
}

// Подписка на событие
OnStarshipPlaced += PlaceStarship;

// Симулируем нажатие на экран
OnStarshipPlaced?.Invoke(5, 7);
// Вывод: "Космический корабль на (5, 7)"
  1. Механизмы обратного вызова (Callback)

    Представьте себе искусственный интеллект, обрабатывающий данные со спутника. Как только завершается обработка данных, он должен уведомить другие системы о завершении. С помощью делегата для обратного вызова (callback) это делается легко:

public delegate void DataProcessedCallback(string result);

public void ProcessSatelliteData(DataProcessedCallback callback)
{
    // Логика обработки данных
    string processedData = "ИИ обнаружил потенциальные признаки жизни.";
    callback(processedData);
}

public void NotifySystems(string message)
{
    Console.WriteLine($"Уведомление: {message}");
}

// Обработка данных и использование обратного вызова
ProcessSatelliteData(NotifySystems);
// Вывод: "Уведомление: ИИ обнаружил потенциальные признаки жизни."
  1. Гибкое управление алгоритмами

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

public delegate void MovementPattern();

public void MoveSpaceship(MovementPattern pattern)
{
    pattern();
}

public void SpiralMovement()
{
    Console.WriteLine("Космический корабль движется по спирали.");
}

public void ZigzagMovement()
{
    Console.WriteLine("Космический корабль движется зигзагом.");
}

// Использование пользовательского движения
MoveSpaceship(SpiralMovement);
// Вывод: "Космический корабль движется по спирали."

MoveSpaceship(ZigzagMovement);
// Вывод: "Космический корабль движется зигзагом."

С первого взгляда можно подумать, что такое использование делегатов бессмысленно и можно было бы просто вызвать методы SpiralMovement и ZigzagMovement напрямую. Однако, если представить, что у метода, обрабатывающего действие пользователя, есть целый набор действий, например, получение текущих данных по игроку и игровому полю, а обработка движения космического корабля - лишь малая часть кода, то скорее всего многие напишут if и в зависимости от того, что указал игрок, будут вызывать SpiralMovement или ZigzagMovement напрямую. Однако, если в дальнейшем появится ещё несколько способов движения корабля, то придётся городить кучу if-else или превращать это в switch. Кажется, что намного лучше все же воспользоваться делегатами.

Распространенные ошибки при работе с делегатами

  1. Чрезмерная подписка

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

    Совет: Всегда убедитесь, что код подписки на событие вызывается столько раз, сколько нужно (обычно один).

  2. Утечки памяти с делегатами

    Если объекты подписались на событие и не отписались, то сборщик мусора не сможет их собрать, что приведет к утечкам памяти.

    Совет: Всегда отписывайтесь от событий, когда они больше не нужны, чтобы обеспечить правильный сбор мусора.

  3. Неправильное использование групповых делегатов

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

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

Заключение

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

Если хотите узнать больше про делегаты и не только, жду вас на курсе C# Developer. Professional.

Комментарии (3)


  1. SpiderEkb
    30.09.2023 10:15
    +2

    Мне одному кажется, что ту нет ничего нового, кроме терминов?

    Вызов функции по адресу в С (да и не только) существует уже много лет.

    typedef для объявления указателя на функцию с заданными аргументами возвращающую заданный тип существует уже черт знает сколько лет (еще в винде 3.х был API GetProcAddress для получения адреса функции в dll по ее имени).

    Механизм callback вызовов тоже не содержит ничего нового и активно используется там, где это уместно...

    Еще лет 15 (если не 20) назад в одном из проектов у нас был протокол обмена построенный на датаграммах (фиксированный заголовок + блок данных, определяемый заголовком). И там для каждого типа датаграммы писался обработчик (который знал что и как делать с блоком данных для данного конкретного типа). Адреса обработчиков хранились в таблице где индексом служил тип датаграммы (который указывался в ее заголовке). И диспетчер датаграмм делался в несколько строк - взять код из заголовка, убедиться что для этого кода есть соотв. элемент в таблице обработчиков, вызвать нужный обработчик по его адресу в таблице.

    И все это работало. Вот только про "делегатов" ничего не знали тогда.


    1. ryanl
      30.09.2023 10:15
      +4

      В IT-индустрии, как и в науке, новая терминология вводится для лучшего описания предметной области.
      Сказал "делегат" - и тебя сразу поняли, что речь про строготипизированную ссылку на метод.
      А вообще согласен, статья для совсем зеленых новичков, то есть, по сути, бесполезна для большинства читателей.


      1. SpiderEkb
        30.09.2023 10:15
        +1

        Просто я слишком давно живу и слишком много повидал :-)

        Так что любую "новую идею" всегда рассматриваю с точки зрения - это реально что-то новое, или просто старое, давно известное, в новой обертке.

        Второго, к сожалению, куда больше. Вот сидишь, слушаешь поток новых умных слов и думаешь - неужто все, на пенсию пора уже? А потом вдруг понимаешь что все это уже давно знаешь и используешь. И хочется спросить - ребята, а попроще нельзя все это сказать? Ну давно же уже все известно...