Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Ključevoe slovo
В этой статье рассматриваются классы в .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. Команда разработчиков имела несколько целей:
Безопасность типов: команда хотела убедиться, что при использовании делегатов язык применяет безопасность типов. Это требует, чтобы делегаты вызывались с правильным типом и количеством аргументов, а возвращаемые типы правильно проверялись на этапе компиляции.
Производительность. Путем создания конкретных классов делегатов, представляющих определенные подписи методов, среда выполнения может оптимизировать вызовы делегатов.
Простота. Делегаты были включены в выпуск .NET версии 1.0, который был до появления универсальных шаблонов. Дизайн должен был работать в рамках временных ограничений.
Решением было создание компилятором конкретных классов делегатов, которые соответствуют подписям метода, обеспечивая безопасность типов при скрытии сложности от вас.
Работа с методами делегата
Несмотря на то, что вы не можете создавать производные классы напрямую, иногда используйте методы, определенные для Delegate
и MulticastDelegate
классов. Ниже приведены наиболее важные из них, о которых нужно знать:
Каждый делегат, с которым вы работаете, является производным от MulticastDelegate
. Делегат типа "многократный" означает, что при вызове через делегат можно вызвать более одного целевого метода. В первоначальном проекте рассматривается различие между делегатами, которые могут вызывать только один метод и делегаты, которые могут вызывать несколько методов. На практике это различие оказалось менее полезным, чем первоначально думалось, поэтому все делегаты в .NET поддерживают несколько целевых методов.
Наиболее часто используемые методы при работе с делегатами:
-
Invoke()
: вызывает все методы, подключенные к делегату -
BeginInvoke()
/EndInvoke()
: используется для шаблонов асинхронного вызова (хотяasync
/await
теперь предпочтительнее)
В большинстве случаев вы не будете вызывать эти методы напрямую. Вместо этого вы будете использовать синтаксис вызова метода в переменной делегата, как показано в приведенных выше примерах. Однако, как вы увидите далее в этой серии, существуют шаблоны, которые работают непосредственно с этими методами.
Сводка
Теперь, когда вы узнали, как синтаксис языка C# сопоставляется с базовыми классами .NET, вы можете изучить, как строго типизированные делегаты используются, создаются и вызываются в более сложных сценариях.