Поделиться через


Ključevoe slovo delegate i System.Delegate

Предыдущий

В этой статье рассматриваются классы в .NET, которые поддерживают делегатов и как они сопоставляются с ключевым словом delegate .

Что такое делегаты?

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

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

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

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

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

// Define a simple delegate that can point to methods taking two integers and returning an integer
public delegate int Calculator(int x, int y);

Этот Calculator делегат может содержать ссылки на любой метод, который принимает два int параметра и возвращает объект int.

Рассмотрим более практический пример. Если вы хотите сортировать список, необходимо сообщить алгоритму сортировки, как сравнить элементы. Давайте посмотрим, как делегаты помогают с методом List.Sort() . Первым шагом является создание типа делегата для операции сравнения:

// From the .NET Core library
public delegate int Comparison<in T>(T left, T right);

Этот Comparison<T> делегат может содержать ссылки на любой метод, который:

  • Принимает два параметра типа T
  • int Возвращает значение (обычно значение -1, 0 или 1, указывающее "меньше", "равно" или "больше")

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

Тип Comparison делегата является универсальным типом, что означает, что он может работать с любым типом T. Дополнительные сведения о универсальных классах и методах см. в разделе "Универсальные классы и методы".

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

Замечание

Объявление типов делегатов (или других типов) непосредственно в глобальном пространстве имен не рекомендуется.

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

Объявление экземпляров для делегатов

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

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

// Inside a class definition:
public Comparison<T> comparator;

Тип этой переменной ( Comparison<T> тип делегата, определенный ранее), а имя переменной — comparator. На этом этапе comparator пока не указывает на какой-либо метод— это как пустой слот, ожидающий заполнения.

Можно также объявить переменные делегата как локальные переменные или параметры метода, как и любой другой тип переменной.

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

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

Вот как Sort() метод использует делегат сравнения для определения порядка объектов:

int result = comparator(left, right);

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

Однако эта строка кода делает небезопасное предположение: предполагается, что целевой метод был добавлен делегату. Если методы не были присоединены, строка выше вызовет выброс ошибки NullReferenceException. Шаблоны, используемые для решения этой проблемы, более сложны, чем простая проверка null, и рассматриваются далее в этой серии.

Назначение, добавление и удаление целевых объектов вызова

Теперь вы знаете, как определять типы делегатов, объявлять экземпляры делегатов и вызывать делегатов. Как именно вы связываете метод с делегатом? Именно в этом случае происходит назначение делегата.

Чтобы использовать делегат, требуется присвоить ему метод. Назначенный метод должен иметь ту же сигнатуру (те же параметры и тип возвращаемого значения), что и тип делегата.

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

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

Этот метод принимает две строки и возвращает целое число, указывающее, какая строка "больше" (длиннее в данном случае). Метод объявлен как приватный, что совершенно нормально. Вам не нужно, чтобы метод был частью общедоступного интерфейса, чтобы использовать его с делегатом.

Теперь этот метод можно передать в метод List.Sort() :

phrases.Sort(CompareLength);

Обратите внимание, что вы используете имя метода без скобок. Это сообщает компилятору преобразовать ссылку на метод в делегат, который можно вызвать позже. Метод Sort() вызывает CompareLength метод всякий раз, когда требуется сравнить две строки.

Кроме того, можно быть более явным, объявив переменную делегата и назначив метод для него:

Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

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

Для простых методов обычно используются лямбда-выражения вместо определения отдельного метода:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);

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

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

Классы Делегат и MulticastDelegate

За кулисами функции делегата, которые вы использовали, основаны на двух ключевых классах в платформе .NET: Delegate и MulticastDelegate. Обычно вы не работаете с этими классами напрямую, но они предоставляют основу, которая позволяет делегатам функционировать.

Класс System.Delegate и его прямой подкласс System.MulticastDelegate обеспечивают поддержку платформы для создания делегатов, регистрации методов в качестве целевых объектов делегата и вызова всех методов, зарегистрированных в делегате.

Вот интересная деталь дизайна: System.Delegate и System.MulticastDelegate не являются сами типами делегатов, которые можно использовать. Вместо этого они служат базовыми классами для всех создаваемых типов делегатов. Язык C# не позволяет напрямую наследовать от этих классов. Вместо этого необходимо использовать ключевое delegate слово.

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

Почему этот дизайн?

Эта конструкция имеет свои корни в первом выпуске C# и .NET. Команда разработчиков имела несколько целей:

  1. Безопасность типов: команда хотела убедиться, что при использовании делегатов язык применяет безопасность типов. Это требует, чтобы делегаты вызывались с правильным типом и количеством аргументов, а возвращаемые типы правильно проверялись на этапе компиляции.

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

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

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

Работа с методами делегата

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

Каждый делегат, с которым вы работаете, является производным от MulticastDelegate. Делегат типа "многократный" означает, что при вызове через делегат можно вызвать более одного целевого метода. В первоначальном проекте рассматривается различие между делегатами, которые могут вызывать только один метод и делегаты, которые могут вызывать несколько методов. На практике это различие оказалось менее полезным, чем первоначально думалось, поэтому все делегаты в .NET поддерживают несколько целевых методов.

Наиболее часто используемые методы при работе с делегатами:

  • Invoke(): вызывает все методы, подключенные к делегату
  • BeginInvoke() / EndInvoke(): используется для шаблонов асинхронного вызова (хотя async/await теперь предпочтительнее)

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

Сводка

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

Далее