JavaRush /Курсы /C# SELF /Делегаты: продвинутые сценарии

Делегаты: продвинутые сценарии

C# SELF
54 уровень , 3 лекция
Открыта

1. Колбэк и асинхронное программирование

Колбэк (callback) — это механизм передачи метода, который должен быть вызван по завершению какой-то операции. Очень часто используется в асинхронных операциях, таймерах, обработке данных, пользовательских интерфейсах.

Пример 1: Асинхронная операция с колбэком

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


// Делегат для обратного вызова
public delegate void DataReceivedHandler(string result);

// Механизм загрузки данных асинхронно (имитация)
public void DownloadDataAsync(DataReceivedHandler callback)
{
    // Допустим, загрузка занимает время (имитируем через таймер)
    Task.Delay(1000).ContinueWith(_ =>
    {
        string data = "Результаты поиска: <данные>";
        callback(data); // Вызов делегата-колбэка
    });
}

// Использование:
DownloadDataAsync(result =>
{
    Console.WriteLine("Получено: " + result);
});

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

2. Делегаты как параметры методов: стратегия и компараторы

Частая задача: дать возможность пользователю передать “логику” (функцию) в ваш метод, чтобы он мог сам определить, как сравнивать, фильтровать или преобразовывать элементы.

Пример 2: Реализация паттерна Strategy через делегаты

Допустим, у нас есть сортировка, но мы хотим, чтобы пользователь мог сортировать по-разному — по имени, по дате, по размеру и т.д.


public delegate bool CompareFunc(int a, int b);

public void BubbleSort(int[] arr, CompareFunc compare)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1; j++)
        {
            if (compare(arr[j], arr[j + 1]))
            {
                // Меняем местами
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// Сортировка от большего к меньшему
CompareFunc descending = (a, b) => a < b;

// Использование
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Выведет: 4, 3, 2, 1

Такой приём — универсальный способ внедрять свою “стратегию” в чужой код без изменения его исходников.

3. Анонимные методы, лямбда-выражения и делегаты

С развитием C# стало неудобно для каждой задачи объявлять отдельный класс или метод. К счастью, появились анонимные методы и лямбда-выражения, которые позволяют создавать делегаты "на лету".

Пример 3: Лямбда как делегат


Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15

Пример 4: Выбор операции по имени (switch + Delegates)


Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"

switch (userInput)
{
    case "sum": op = (a, b) => a + b; break;
    case "sub": op = (a, b) => a - b; break;
    case "mul": op = (a, b) => a * b; break;
    case "div": op = (a, b) => a / b; break;
    default: throw new Exception("Неизвестная операция!");
}
Console.WriteLine(op(6, 2));

Валидацию входных данных не забываем — делегаты тут дают гибкость и читаемость.

4. Делегаты и цепочки обработки (“цепочка обязанностей”)

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

Пример 5: Цепочка вызова фильтров

Представим, что у нас есть “фильтры”, которые должны обработать строку.


public delegate string StringFilter(string input);

string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();

StringFilter filters = RemoveDigits;
filters += ToUpper;

// Делегат выведет строку через все фильтры
string text = "Привет123";
foreach (StringFilter filter in filters.GetInvocationList())
{
    text = filter(text);
}
Console.WriteLine(text); // Выведет: "ПРИВЕТ"

Важно: если вызвать просто filters(text), то будет возвращено значение только последнего обработчика, а не всей цепочки! Если нужно “протекание” значения, используйте явную итерацию через GetInvocationList(), как показано выше.

5. Делегаты для динамического связывания поведения на лету

Раньше для замены одного поведения на другое приходилось “придумывать” отдельные классы и интерфейсы. С делегатами и лямбда-выражениями можно значительную часть “мелкого” полиморфизма выразить функциями.

Пример 6: Поведение робота с динамической командой


public class Robot
{
    public event Action<string>? OnCommandReceived;

    public void ReceiveCommand(string command)
    {
        OnCommandReceived?.Invoke(command);
    }
}

// Использование:
var robot = new Robot();

robot.OnCommandReceived += cmd => Console.WriteLine($"Робот выполняет: {cmd}");
robot.OnCommandReceived += cmd =>
{
    if (cmd == "Включиться")
        Console.WriteLine("Загрузка системы...");
};
// Пробуем
robot.ReceiveCommand("Включиться");
robot.ReceiveCommand("Переместиться вперед");

Такой приём часто используют в тестах, прототипах, в DI (dependency injection) контейнерах и для передачи бизнес-логики через параметры.

6. Делегаты как подписки на состояние

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

Пример 7: Класс с подпиской на изменение


public class Notifier<T>
{
    private T _value = default!;
    public event Action<T>? ValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                ValueChanged?.Invoke(_value);
            }
        }
    }
}

// Использование:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Новое значение: {v}");
intValue.Value = 5; // Триггерится событие
intValue.Value = 10;

Такой подход — практически "реактивное программирование на минималках", основа для MVVM, data binding, и многих современных UI-фреймворков.

7. Делегаты, замыкания и лексическая область видимости

Лямбда-выражения и анонимные методы могут захватывать переменные из окружающего контекста (closure). Это удобно, но иногда приводит к неожиданным ошибкам.

Пример 8: Захват переменной и "ловушка" цикла


Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var a in actions)
    a(); // Выведет три раза 3 (!)

Почему? Потому что замыкание ссылается на одну и ту же переменную i, которая после цикла равна 3. А если нужно запомнить именно значения 0, 1, 2?


for (int i = 0; i < 3; i++)
{
    int loopValue = i; // "замораживаем" текущее значение
    actions[i] = () => Console.WriteLine(loopValue);
}

Теперь код работает, как ожидается. Такие ловушки — одна из самых частых ошибок у новичков при использовании лямбда-выражений!

8. Комбинирование делегатов

Многоадресные делегаты (multicast) содержат список методов, и вы можете добавлять (+=) или удалять (-=) обработчики.

Особенность: удаляются по ссылке и сигнатуре


void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");

Action a = Handler1;
a += Handler2;
a -= Handler1; // Оставит только Handler2
a?.Invoke();   // Выведет "2"

Пример: динамическое управление обработчиками


Action a = Handler1;
a += Handler1;
a -= Handler1; // Теперь остается ОДИН Handler1 в списке!

9. Делегаты расширяемости и инверсии управления (IoC)

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

Пример: Инъекция поведения в конструктор


public class Greeter
{
    private readonly Func<string> _getName;

    public Greeter(Func<string> getName)
    {
        _getName = getName;
    }

    public void Greet() => Console.WriteLine($"Привет, {_getName()}!");
}

// Инъекция разного поведения:
var greeter1 = new Greeter(() => "Аня");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());

greeter1.Greet(); // "Привет, Аня!"
greeter2.Greet(); // "Привет, 14:35!"

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

10. Полезные нюансы

Делегаты в стандартных интерфейсах и LINQ

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

  • Многие методы вроде List<T>.Find, Array.Sort, Where, Select принимают делегаты (Func<T, bool>, Comparison<T> и др.).
  • LINQ-методы позволяют передавать логику фильтрации, преобразования, агрегации — без создания отдельных классов.

Пример: Компаратор для сортировки объектов


var people = new[] { "Иван", "Мария", "Петр" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Иван, Петр, Мария

Делегаты и каррирование (частичное применение аргументов)

С помощью анонимных методов/лямбда-выражений вы можете “зафиксировать” часть параметров и получить новую функцию.

Пример: Частичное применение


Func<int, int, int> sum = (x, y) => x + y;

// Создаём функцию, которая всегда прибавляет 10
Func<int, int> add10 = y => sum(10, y);

Console.WriteLine(add10(5)); // 15

Особенность сравнения делегатов

В C# делегаты можно сравнивать между собой на равенство (==), если у них одинаковый список вызовов (invocation list).


void Handler1() { }
void Handler2() { }

Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True

Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True

Но если делегат построен на анонимном методе или лямбде — сравниваются именно экземпляры.

Сериализация делегатов

Делегаты можно сериализовать, но только если методы, на которые они ссылаются, определены в сериализуемых классах и все типы доступны. Начиная с .NET 8, BinaryFormatter по умолчанию отключён и считается устаревшим, а в будущих версиях будет полностью удалён; сериализация делегатов в производственных задачах практически не используется.

Взаимодействие делегатов и событий: где делегат, а где событие?

  • Делегат — это тип/переменная, которую можно вызвать явно.
  • Событие (event) — способ ограничить доступ к делегату: извне можно только подписываться/отписываться (+=/-=), но вызывать — только изнутри класса.
  • Событие всегда делегатного типа, но не любой делегат — событие.

Как применять? Если хотите, чтобы логика могла быть определена “за пределами” класса, используйте делегаты. Если нужно контролировать подписку/отписку и защищать переменную — используйте событие.

11. Типичные ошибки при работе с делегатами

Ошибка №1: Путаница между делегатом и событием.
Использование публичного поля-делегата (public Action MyAction;) вместо события (public event Action MyAction;) ломает инкапсуляцию. Внешний код может случайно или намеренно перезаписать всех подписчиков (instance.MyAction = null;) или вызвать их напрямую, что нарушает логику класса.

Ошибка №2: Неправильная работа с возвращаемыми значениями у многоадресных делегатов.
Если делегат возвращает значение (например, Func<string, int>), при обычном вызове (myDelegate("test")) вернётся результат только последнего метода в цепочке. Чтобы получить результаты всех подписчиков, нужно итерироваться по списку вызовов через GetInvocationList().


// Пример перебора результатов всех подписчиков
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
    var r = ((Func<string, int>)d)("test");
    Console.WriteLine(r);
}

Ошибка №3: Захват переменной цикла в замыкании.
Классическая ловушка для новичков: лямбда, созданная внутри цикла for, захватывает саму переменную-итератор, а не её текущее значение.


// Неправильно: все действия выведут последнее значение i
for (int i = 0; i < 3; i++) 
{
    actions[i] = () => Console.WriteLine(i);
}

// Правильно: создаём локальную копию для каждой итерации
for (int i = 0; i < 3; i++) 
{
    int copy = i;
    actions[i] = () => Console.WriteLine(copy);
}

Ошибка №4: Создание утечек памяти.
Если метод экземпляра подписывается на делегат долгоживущего объекта, но не отписывается, возникает утечка. Долгоживущий объект держит ссылку на подписчика, и сборщик мусора не может его удалить. Следите за отпиской, особенно в классах с ограниченным жизненным циклом (например, UI-компонентах).

2
Задача
C# SELF, 54 уровень, 3 лекция
Недоступна
Цепочка обработки текста с использованием делегатов
Цепочка обработки текста с использованием делегатов
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Slevin Уровень 56
5 марта 2026
В задаче противоречение в input строке в условии и требованиях.