Если вы когда‑либо изучали C# и дошли до делегатов, вы наверняка испытали когнитивный диссонанс. До этого момента всё было относительно логично: классы — это чертежи объектов, методы — действия, переменные — данные. А тут появляется какая‑то странная конструкция, которая умеет хранить в себе методы и вызывать их как обычные переменные.

В этой статье мы не просто посмотрим синтаксис — мы поймём зачем это нужно и как работает под капотом. Мы пройдём путь от указателей на функции в C/C++ до современных лямбда‑выражений в C#, разберёмся с делегатами и научимся использовать всю мощь функциональных возможностей языка.

От указателей к делегатам

Чтобы понять работу с делегатами, нужно заглянуть в «доисторические» времена программирования на C. Там была концепция указателя на функцию — переменной, которая хранит адрес функции в памяти.

// Объявление типа указателя на функцию, принимающую int и возвращающую int

typedef int (*MathOperation)(int);

// Функции, соответствующие этому типу

int Double(int x) { return x * 2; }
int Triple(int x) { return x * 3; }

// Использование

MathOperation op = Double;
int result = op(5);  // 10
op = Triple;
result = op(5);      // 15

Однако здесь возникает ряд проблем, связанных с указателями на функции. Прежде всего, они не типобезопасны — можно случайно присвоить указатель на функцию с несовместимой сигнатурой. Также нет информации о количестве и типах параметров на уровне компилятора. И нет поддержки объектно‑ориентированных концепций (методов классов)

Разработчики C# взяли идею указателей на функции, но сделали её типобезопасной и объектно‑ориентированной. Делегат в C# — это класс, который знает сигнатуру метода (типы параметров и возвращаемого значения), ссылку на объект (для методов экземпляра) и сам адрес метода.

// Объявление типа делегата

public delegate int MathOperation(int x);

// Методы, соответствующие делегату

public static int Double(int x) => x * 2;
public static int Triple(int x) => x * 3;

// Использование
MathOperation op = Double;
int result = op(5);  // 10

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

Делегат как контракт

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

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

Ниже представлен пример интерфейса с одним методом.

public interface ILogger

{
    void Log(string message);
}

Делегат с той же сигнатурой будет иметь следующий вид:

public delegate void LogHandler(string message);

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

Многоадресные делегаты (Multicast Delegates)

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

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

Многоадресные делегаты работают по следующему принципу:

  • Методы добавляются к делегату с помощью операторов + или +=.

  • Для удаления метода из цепочки используется оператор — или =‑.

  • При вызове многоадресного делегата все связанные методы вызываются последовательно.

  • Делегаты, хранящие несколько ссылок, должны иметь тип возвращаемого значения void.

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

public delegate void Notify(string message);

public class Logger
{
    public void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
    public void LogToFile(string msg) => Console.WriteLine($"File: {msg} (имитация записи)");
}

 

// Использование

var logger = new Logger();

Notify notifier = logger.LogToConsole;
notifier += logger.LogToFile;  // Добавляем ещё один метод
notifier("Hello!");  // Вызываем оба метода

// Console: Hello!
// File: Hello! (имитация записи)

Как это работает под капотом: делегат на самом деле хранит список вызовов (invocation list). При вызове все методы в этом списке выполняются последовательно.

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

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

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

В целом, использование обобщенных делегатов делает код более читаемым и избавляет от необходимости объявлять свои типы делегатов.

// Action — метод без возвращаемого значения

Action<string> log = message => Console.WriteLine(message);

log("Hello");

// Func — метод с возвращаемым значением

Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 5);  // 8

// Predicate — проверка условия

Predicate<int> isPositive = x => x > 0;
bool positive = isPositive(10);  // true

Рассмотрев работу с делегатами в C# мы перейдем к лямбда выражениям.

От методов к лямбдам

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

Для лучшего понимания давайте посмотрим эволюцию синтаксиса передачи поведения:

Изначально был отдельный метод

bool IsEven(int x) => x % 2 == 0;

var evens = numbers.Where(IsEven);

В C# 2.0 появился анонимный метод

var evens = numbers.Where(delegate(int x) { return x % 2 == 0; });

А начиная с C# 3.0 появились лямбда‑выражения

var evens = numbers.Where(x => x % 2 == 0);

Приведем некоторые особенности лямбда‑выражений. Лямбда‑оператор => разделяет выражение на две части:

  • в левой части указывается входной параметр (или несколько параметров);

  • в правой — тело лямбда‑выражения.

Так, в примере выше лямбда‑выражение принимает один входной параметр x, проверяет, является ли x четным числом и возвращает true или false.

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

Заключение

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

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

Если делегаты и лямбды до сих пор казались вам темой «для потом», курс «C++‑разработчик» поможет разобрать их без магии и встроить в нормальную практику разработки на C#. Так язык начинает восприниматься не набором сложных конструкций, а понятным рабочим инструментом.

Если хотите сначала посмотреть на формат обучения и понять, насколько вам подходит C#, начните с открытых уроков: