Полное руководство по языку программирования С# 14 и платформе .NET 10 / Делегаты, события и лямбды

Делегаты

Начальный ⏱ 11 мин чтения 👁 2 🕓 обновлено 11.08.2026

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

Определение делегатов

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


delegate void Message();

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

Рассмотрим применение этого делегата:


Message mes;            // 2. Создаем переменную делегата
mes = Hello;            // 3. Присваиваем этой переменной адрес метода
mes();                  // 4. Вызываем метод

void Hello() => Console.WriteLine("Hello METANIT.COM");

delegate void Message(); // 1. Объявляем делегат

Прежде всего сначала необходимо определить сам делегат:

delegate void Message(); // 1. Объявляем делегат

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

Message mes; // 2. Создаем переменную делегата

Далее в делегат передается адрес определенного метода (в нашем случае метода Hello). Обратите внимание, что данный метод имеет тот же возвращаемый тип и тот же набор параметров (в данном случае отсутствие параметров), что и делегат.

mes = Hello; // 3. Присваиваем этой переменной адрес метода

Затем через делегат вызываем метод, на который ссылается данный делегат:

mes(); // 4. Вызываем метод

Вызов делегата производится подобно вызову метода.

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


Message message1 = Welcome.Print;
Message message2 = new Hello().Display;

message1(); // Welcome
message2(); // Привет

delegate void Message();

class Welcome
{
    public static void Print() => Console.WriteLine("Welcome");
}
class Hello
{
    public void Display() => Console.WriteLine("Привет");
}

Место определения делегата

Если мы определяем делегат в прогаммах верхнего уровня (top-level program), которую по умолчанию представляет файл Program.cs начиная с версии C# 10, как в примере выше, то, как и другие типы, делегат определяется в конце кода. Но в принцие делегат можно определять внутри класса:


class Program
{
    delegate void Message(); // 1. Объявляем делегат
    static void Main()
    {
        Message mes;            // 2. Создаем переменную делегата
        mes = Hello;            // 3. Присваиваем этой переменной адрес метода
        mes();                  // 4. Вызываем метод

        void Hello() => Console.WriteLine("Hello METANIT.COM");
    }
}

Либо вне класса:


delegate void Message(); // 1. Объявляем делегат
class Program
{
    static void Main()
    {
        Message mes;            // 2. Создаем переменную делегата
        mes = Hello;            // 3. Присваиваем этой переменной адрес метода
        mes();                  // 4. Вызываем метод

        void Hello() => Console.WriteLine("Hello METANIT.COM");
    }
}

Параметры и результат делегата

Рассмотрим определение и применение делегата, который принимает параметры и возвращает результат:


Operation operation = Add;      // делегат указывает на метод Add
int result = operation(4, 5);   // фактически Add(4, 5)
Console.WriteLine(result);      // 9
    
operation = Multiply;           // теперь делегат указывает на метод Multiply
result = operation(4, 5);       // фактически Multiply(4, 5)
Console.WriteLine(result);      // 20

int Add(int x, int y) => x + y;

int Multiply(int x, int y) => x * y;

delegate int Operation(int x, int y);

В данном случае делегат Operation возвращает значение типа int и имеет два параметра типа int. Поэтому этому делегату соответствует любой метод, который возвращает значение типа int и принимает два параметра типа int. В данном случае это методы Add и Multiply. То есть мы можем присвоить переменной делегата любой из этих методов и вызывать.

Поскольку делегат принимает два параметра типа int, то при его вызове необходимо передать значения для этих параметров: operation(4,5).

Присвоение ссылки на метод

Выше переменной делегата напрямую присваивался метод. Есть еще один способ - создание объекта делегата с помощью конструктора, в который передается нужный метод:


Operation operation1 = Add;
Operation operation2 = new Operation(Add);

int Add(int x, int y) => x + y;

delegate int Operation(int x, int y);

Оба способа равноценны.

Соответствие методов делегату

Как было написано выше, методы соответствуют делегату, если они имеют один и тот же возвращаемый тип и один и тот же набор параметров. Но надо учитывать, что во внимание также принимаются модификаторы ref, in и out. Например, пусть у нас есть делегат:

delegate void SomeDel(int a, double b);

Этому делегату соответствует, например, следующий метод:

void SomeMethod1(int g, double n) { }

А следующие методы НЕ соответствуют:


double SomeMethod2(int g, double n) { return g + n; }
void SomeMethod3(double n, int g) { }
void SomeMethod4(ref int g, double n) { }
void SomeMethod5(out int g, double n) { g = 6; }

Здесь метод SomeMethod2 имеет другой возвращаемый тип, отличный от типа делегата. SomeMethod3 имеет другой набор параметров. Параметры SomeMethod4 и SomeMethod5 также отличаются от параметров делегата, поскольку имеют модификаторы ref и out.

Добавление методов в делегат

В примерах выше переменная делегата указывала на один метод. В реальности же делегат может указывать на множество методов, которые имеют ту же сигнатуру и возвращаемые тип. Все методы в делегате попадают в специальный список - список вызова или invocation list. И при вызове делегата все методы из этого списка последовательно вызываются. И мы можем добавлять в этот список не один, а несколько методов. Для добавления методов в делегат применяется операция +=:


Message message = Hello;
message += HowAreYou;  // теперь message указывает на два метода
message();              // вызываются оба метода - Hello и HowAreYou

void Hello() => Console.WriteLine("Hello");
void HowAreYou() => Console.WriteLine("How are you?");

delegate void Message();

В данном случае в список вызова делегата message добавляются два метода - Hello и HowAreYou. И при вызове message вызываются сразу оба этих метода.

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

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


Message message = Hello;
message += HowAreYou;
message += Hello;
message += Hello;

message();

Консольный вывод:

Hello
How are you?
Hello
Hello

Подобным образом мы можем удалять методы из делегата с помощью операций -=:


Message? message = Hello; 
message += HowAreYou;
message();	// вызываются все методы из message
message -= HowAreYou;	// удаляем метод HowAreYou
if (message != null) message();	// вызывается метод Hello

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

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

Кроме того, перед вторым вызовом мы проверяем переменную на значение null.

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

Объединение делегатов

Делегаты можно объединять в другие делегаты. Например:


Message mes1 = Hello;
Message mes2 = HowAreYou;
Message mes3 = mes1 + mes2; // объединяем делегаты
mes3(); // вызываются все методы из mes1 и mes2

void Hello() => Console.WriteLine("Hello");
void HowAreYou() => Console.WriteLine("How are you?");

delegate void Message();

В данном случае объект mes3 представляет объединение делегатов mes1 и mes2. Объединение делегатов значит, что в список вызова делегата mes3 попадут все методы из делегатов mes1 и mes2. И при вызове делегата mes3 все эти методы одновременно будут вызваны.

Вызов делегата

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


Message mes = Hello;
mes();
Operation op = Add;
int n = op(3, 4);
Console.WriteLine(n);

void Hello() => Console.WriteLine("Hello");
int Add(int x, int y) => x + y;

delegate int Operation(int x, int y);
delegate void Message();

Другой способ вызова делегата представляет метод Invoke():


Message mes = Hello;
mes.Invoke(); // Hello
Operation op = Add;
int n = op.Invoke(3, 4);
Console.WriteLine(n);   // 7

void Hello() => Console.WriteLine("Hello");
int Add(int x, int y) => x + y;

delegate int Operation(int x, int y);
delegate void Message();

Если делегат принимает параметры, то в метод Invoke передаются значения для этих параметров.

Следует учитывать, что если делегат пуст, то есть в его списке вызова нет ссылок ни на один из методов (то есть делегат равен Null), то при вызове такого делегата мы получим исключение, как, например, в следующем случае:


Message? mes;
//mes();        // ! Ошибка: делегат равен null

Operation? op = Add;
op -= Add;      // делегат op пуст
int n = op(3, 4);       // !Ошибка: делегат равен null

Поэтому при вызове делегата всегда лучше проверять, не равен ли он null. Либо можно использовать метод Invoke и оператор условного null:


Message? mes = null;
mes?.Invoke();        // ошибки нет, делегат просто не вызывается

Operation? op = Add;
op -= Add;      	// делегат op пуст
int? n = op?.Invoke(3, 4);   // ошибки нет, делегат просто не вызывается, а n = null

Если делегат возвращает некоторое значение, то возвращается значение последнего метода из списка вызова (если в списке вызова несколько методов). Например:


Operation op = Subtract;
op += Multiply;
op += Add;
Console.WriteLine(op(7, 2));    // Add(7,2) = 9

int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x * y;

delegate int Operation(int x, int y);

Обобщенные делегаты

Делегаты, как и другие типы, могут быть обобщенными, например:


Operation<decimal, int> squareOperation = Square;
decimal result1 = squareOperation(5);
Console.WriteLine(result1);  // 25

Operation<int, int> doubleOperation = Double;
int result2 = doubleOperation(5);
Console.WriteLine(result2);  // 10

decimal Square(int n) => n * n;
int Double(int n) => n + n;

delegate T Operation<T, K>(K val);

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

В прогамме мы можем определить переменные делегата под определенный метод. Например, делегату Operation<decimal, int> соответствует метод, который принимает число int и возвращает число типа decimal. А делегату Operation<int, int> соответствует метод, который принимает и возвращает число типа int.

Делегаты как параметры методов

Также делегаты могут быть параметрами методов. Благодаря этому один метод в качестве параметров может получать действия - другие методы. Например:


DoOperation(5, 4, Add);         // 9
DoOperation(5, 4, Subtract);    // 1
DoOperation(5, 4, Multiply);    // 20

void DoOperation(int a, int b, Operation op)
{
    Console.WriteLine(op(a,b));
}
int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x * y;

delegate int Operation(int x, int y);

Здесь метод DoOperation в качестве параметров принимает два числа и некоторое действие в виде делегата Operation. В внутри метода вызываем делегат Operation, передавая ему числа из первых двух параметров.

При вызове метода DoOperation мы можем передать в него в качестве третьего параметра метод, который соответствует делегату Operation.

Возвращение делегатов из метода

Также делегаты можно возвращать из методов. То есть мы можем возвращать из метода какое-то действие в виде другого метода. Например:


Operation operation = SelectOperation(OperationType.Add);
Console.WriteLine(operation(10, 4));    // 14

operation = SelectOperation(OperationType.Subtract);
Console.WriteLine(operation(10, 4));    // 6

operation = SelectOperation(OperationType.Multiply);
Console.WriteLine(operation(10, 4));    // 40

Operation SelectOperation(OperationType opType)
{
    switch (opType)
    {
        case OperationType.Add: return Add;
        case OperationType.Subtract: return Subtract;
        default: return Multiply;
    }
}

int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x * y;

enum OperationType
{
    Add, Subtract, Multiply
}
delegate int Operation(int x, int y);

В данном случае метод SelectOperation() в качестве параметра принимает перечисление типа OperationType. Это перечисление хранит три константы, каждая из которых соответствует определенной арифметической операции. И в самом методе в зависимости от значения параметра возвращаем определенный метод. Причем поскольку возвращаемый тип метода - делегат Operation, то метод должен возвратить метод, который соответствует этому делегату - в нашем случае это методы Add, Subtract, Multiply. То есть если параметр метода SelectOperation равен OperationType.Add, то возвращается метод Add, который выполняет сложение двух чисел:

case OperationType.Add: return Add;

При вызове метода SelectOperation мы можем получить из него нужное действие в переменную operation:

Operation operation = SelectOperation(OperationType.Add);

И при вызове переменной operation фактически будет вызываться полученный из SelectOperation метод:


Operation operation = SelectOperation(OperationType.Add);	// Здесь operation = Add
Console.WriteLine(operation(10, 4));    // 14

Возвращение делегатом делегата

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


Eat(5)(6)(7);

Hungry Eat(int val)
{
    Console.WriteLine($"Съели число {val}"); 
    return Eat;
};

delegate Hungry Hungry(int value);

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

Для примера определяем метод Eat(), который соответствует делегату Hungry - принимает число типа int и возвращает делегат Hungry. А поскольку метод Eat соответствует делегату Hungry, то из этого метода мы можем возвратить ... сам же этот метод.

В итоге при создании цепочки Eat(5)(6)(7) мы получим три вызова метода Eat:


Съели число 5
Съели число 6
Съели число 7
Дополнительные материалы

Проверь себя

📋 Тест по теме «Делегаты» Тест по теме

Практические задания

Лёгкое Ручная проверка Заполните пропуск в коде (1)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

_____ void Message();
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (2)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Message _____;            // 2. Создаем переменную делегата
mes = Hello;            // 3. Присваиваем этой переменной адрес метода
mes();                  // 4. Вызываем метод

void Hello() => Console.WriteLine("Hello METANIT.COM");

delegate void Message(); // 1. Объявляем делегат
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (3)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

delegate void Message(); // _____. Объявляем делегат
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (4)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

_____ mes; // 2. Создаем переменную делегата
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (5)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

mes = _____; // 3. Присваиваем этой переменной адрес метода
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (1)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

_____ void _____();
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (2)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

_____ mes;            // 2. Создаем переменную делегата
mes = Hello;            // _____. Присваиваем этой переменной адрес метода
mes();                  // 4. Вызываем метод

void Hello() => Console.WriteLine("Hello METANIT.COM");

delegate void Message(); // 1. Объявляем делегат
Войдите, чтобы отправить решение
Среднее Ручная проверка Расставьте строки кода в правильном порядке
+20 XP

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

mes = Hello;            // 3. Присваиваем этой переменной адрес метода
Message mes;            // 2. Создаем переменную делегата
void Hello() => Console.WriteLine("Hello METANIT.COM");
delegate void Message(); // 1. Объявляем делегат
mes();                  // 4. Вызываем метод
Войдите, чтобы отправить решение
Сложное Ручная проверка Объясните и модифицируйте пример из урока «Делегаты»
+30 XP

Опишите своими словами, что делает приведённый пример кода из урока «Делегаты», и предложите одно осмысленное изменение (например, другое входное значение или дополнительное условие). Приведите итоговый код.

delegate void Message();
Войдите, чтобы отправить решение
Сложное Ручная проверка Найдите потенциальную проблему в коде урока «Делегаты»
+30 XP

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

Message mes;            // 2. Создаем переменную делегата
mes = Hello;            // 3. Присваиваем этой переменной адрес метода
mes();                  // 4. Вызываем метод

void Hello() => Console.WriteLine("Hello METANIT.COM");

delegate void Message(); // 1. Объявляем делегат
Войдите, чтобы отправить решение
Мини-проект Ручная проверка Мини-проект по теме «Делегаты»
+50 XP

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

Войдите, чтобы отправить решение
Домашнее задание Ручная проверка Домашнее задание по теме «Делегаты»
+25 XP

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

Войдите, чтобы отправить решение

Обсуждение (0)

Войдите, чтобы оставить комментарий.