System.Delegate и ключевое слово delegate
В этой статье описываются классы в .NET, поддерживающие делегаты, и рассматриваются способы их сопоставления с ключевым словом delegate
.
Определение типов делегатов
Начнем с ключевого слова "delegate", потому что это основной элемент, используемый при работе с делегатами. Код, который компилятор создает при использовании ключевого слова delegate
, будет сопоставляться с вызовами методов, вызывающих члены классов Delegate и MulticastDelegate.
Тип делегата определяется с помощью синтаксиса, подобному синтаксису определения сигнатуры метода. К определению нужно просто добавить ключевое слово delegate
.
В качестве примера будем по-прежнему использовать метод List.Sort(). Первым шагом является создание типа для делегата сравнения.
// From the .NET Core library
// Define the delegate type:
public delegate int Comparison<in T>(T left, T right);
Компилятор создает класс, производный от System.Delegate
, соответствующий используемой сигнатуре (в данном случае — метод, который возвращает целое число и имеет два аргумента). Тип делегата — Comparison
. Тип делегата Comparison
является универсальным типом. Подробные сведения об универсальных типах см. здесь.
Обратите внимание, что синтаксис может отображаться так, будто он объявляет переменную, но на самом деле он объявляет тип. Можно определить типы делегатов внутри классов, непосредственно внутри пространств имен или даже в глобальном пространстве имен.
Примечание.
Не рекомендуется объявлять типы делегатов (или другие типы) непосредственно в глобальном пространстве имен.
Компилятор также создает обработчики добавления и удаления для этого нового типа, чтобы клиенты этого класса могли добавлять и удалять методы из списка вызовов экземпляра. Компилятор будет обеспечивать соответствие подписи добавляемого или удаляемого метода подписи, используемой при объявлении метода.
Объявление экземпляров делегатов
После определения делегата можно создать экземпляр этого типа. Как и все переменные в C#, экземпляры делегата нельзя объявлять непосредственно в пространстве имен или в глобальном пространстве имен.
// inside a class definition:
// Declare an instance of that type:
public Comparison<T> comparator;
Тип переменной — Comparison<T>
, тип делегата определен ранее. Имя переменной — comparator
.
В приведенном выше фрагменте кода была объявлена переменная-член в классе. Можно также объявить переменные делегатов, локальные переменные или аргументов для методов.
Вызов делегатов
Чтобы вызвать методы, которые находятся в списке вызова делегата, нужно вызвать этот делегат. В методе Sort()
код вызовет метод сравнения, чтобы определить порядок размещения объектов:
int result = comparator(left, right);
В строке выше код вызывает метод, подключенный к делегату. Переменная считается именем метода и вызывается с помощью обычного синтаксиса вызова метода.
Эта строка кода содержит небезопасное условие: нет никакой гарантии, что целевой объект было добавлен к делегату. Если целевые объекты не были вложены, строка выше приведет к возникновению исключения NullReferenceException
. Идиомы, используемые для решения этой проблемы, более сложны, чем простые проверки значений NULL, и рассматриваются далее в этой серии материалов.
Назначение, добавление и удаление целевых объектов вызова
Сведения о том, как определяется тип делегата и как объявляются и вызываются экземпляры делегата.
Разработчикам, которые хотят использовать метод List.Sort()
, нужно определить метод, сигнатура которого совпадает с определением типа делегата, и назначить его делегату, используемому методом sort. Это назначение добавляет метод в список вызовов данного делегата объекта.
Предположим, требуется отсортировать список строк по их длине. Функция сравнения может выглядеть следующим образом:
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
Метод объявляется как закрытый метод. Все правильно. Вам может быть не нужно, чтобы этот метод был частью общедоступного интерфейса. Этот метод, присоединенный к делегату, по-прежнему можно использовать в качестве метода сравнения. В вызывающем коде этот метод будет присоединен к целевому списку объекта делегата и будет доступен через этот делегат.
Чтобы создать эту связь, передайте метод в метод List.Sort()
:
phrases.Sort(CompareLength);
Обратите внимание, что имя метода используется без скобок. Использование метода как аргумента указывает компилятору преобразовать ссылку на метод в ссылку, которая может применяться как целевой объект вызова делегата, и присоединить этот метод в качестве целевого объекта вызова.
Вы также явно объявили переменную типа Comparison<string>
и выполнили назначение:
Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);
Если в качестве объекта делегата используется небольшой метод, для назначения обычно применяется синтаксис лямбда-выражения:
Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);
Использование лямбда-выражений для целевых объектов делегатов рассматривается более подробно в следующем разделе.
В примере Sort() к делегату обычно подключается один целевой метод. Однако объекты делегатов поддерживают списки вызовов, где к объекту делегата присоединено несколько целевых методов.
Классы Delegate и MulticastDelegate
Описанная выше поддержка языка предоставляет функции и поддержку, которые обычно необходимы для работы с делегатами. Эти возможности основаны на двух классах в платформе .NET Core: Delegate и MulticastDelegate.
Класс System.Delegate
и его прямой вложенный класс System.MulticastDelegate
обеспечивают поддержку платформы для создания делегатов, регистрации методов в качестве целевых объектов делегатов и вызова всех методов, которые зарегистрированы как целевые объекты делегатов.
Что интересно, сами классы System.Delegate
и System.MulticastDelegate
не являются типами делегатов. Они являются основой для всех конкретных типов делегатов. Тот же процесс конструкции языка требует, что нельзя объявить класс, который является производным от Delegate
или MulticastDelegate
. Это запрещено правилами языка C#.
Вместо этого компилятор C# создает экземпляры класса, производного от MulticastDelegate
, при использовании ключевого слова языка C# для объявления типов делегатов.
Такая схема впервые появилась в первом выпуске C# и .NET. Одной из целей команды разработки было обеспечение соблюдения типобезопасности при использовании делегатов. Это значило, что делегаты вызывались с помощью правильного типа и числа аргументов. И что любой возвращаемый тип был правильно указан во время компиляции. Делегаты входили в выпуск 1.0 .NET, который действовал до универсальных типов.
Наилучший способ обеспечения безопасности типа заключался в том, что компилятор должен был создать конкретные классы делегатов, представляющих используемую сигнатуру метода.
Несмотря на то, что невозможно создать производные классы напрямую, вы будете использовать методы, определенные в этих классах. Разберем наиболее распространенные методы, которые будут использоваться при работе с делегатами.
Первый и самый важный момент состоит в том, что каждый делегат является производным от MulticastDelegate
. Многоадресный делегат означает, что при вызове с помощью делегата может быть вызвано несколько целевых объектов метода. В исходной структуре планировалось проводить различие между делегатами, где можно было прикреплять и вызывать только один целевой метод, и делегатами, где можно было прикреплять и вызывать несколько целевых методов. На практике это различие оказалась менее полезным, чем предполагалось изначально. Уже были созданы два разных класса, которые находились в платформе с момента первого общедоступного выпуска.
Методы, которые чаще всего будут использоваться с делегатами, — Invoke()
и BeginInvoke()
/ EndInvoke()
. Invoke()
будет вызывать все методы, которые были прикреплены к определенному экземпляру делегата. Как было показано выше, для вызова делегатов обычно используется синтаксис вызова метода в переменной делегата. Далее в этой серии материалов вы узнаете о шаблонах, которые работают непосредственно с этими методами.
Ознакомившись с синтаксисом языка и классами, поддерживающими делегаты, давайте рассмотрим способы использования, создания и вызова строго типизированных делегатов.