20 делегатов
20.1 Общие
Объявление делегата определяет класс, производный от класса System.Delegate
. Экземпляр делегата инкапсулирует список вызовов, который является списком одного или нескольких методов, каждый из которых называется вызываемой сущностью. Например, вызываемая сущность состоит из экземпляра и метода в этом экземпляре. Для статических методов вызываемая сущность состоит только из метода. Вызов экземпляра делегата с соответствующим набором аргументов приводит к вызову каждой вызываемой сущности делегата с заданным набором аргументов.
Примечание. Интересное и полезное свойство экземпляра делегата заключается в том, что он не знает или заботится о классах методов, которые он инкапсулирует. Все, что имеет значение, заключается в том, что эти методы совместимы (§20.4) с типом делегата. Это делает делегатов идеально подходящими для вызова "анонимных". конечная заметка
Объявления делегатов 20.2
Delegate_declaration — это type_declaration (§14.7), который объявляет новый тип делегата.
delegate_declaration
: attributes? delegate_modifier* 'delegate' return_type delegate_header
| attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
delegate_header
;
delegate_header
: identifier '(' parameter_list? ')' ';'
| identifier variant_type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause* ';'
;
delegate_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| unsafe_modifier // unsafe code support
;
unsafe_modifier определен в §23.2.
Это ошибка во время компиляции для одного модификатора, который будет отображаться несколько раз в объявлении делегата.
Объявление делегата , предоставляющее variant_type_parameter_list , является универсальным объявлением делегата. Кроме того, любой делегат, вложенный в объявление универсального класса или универсальное объявление структуры, является универсальным объявлением делегата, так как аргументы типов для содержащего типа должны быть предоставлены для создания созданного типа (§8.4).
Модификатор new
разрешен только для делегатов, объявленных в другом типе, в этом случае он указывает, что такой делегат скрывает унаследованный член по тому же имени, как описано в разделе 15.3.5.
protected
Модификаторы public
, internal
и private
модификаторы управляют специальными возможностями типа делегата. В зависимости от контекста, в котором происходит объявление делегата, некоторые из этих модификаторов могут быть запрещены (§7.5.2).
Имя типа делегата — идентификатор.
Как и методы (§15.6.1), если он присутствует, делегат возвращает по ссылке; в противном случае, если ref
return_type , void
делегат возвращает значение без значения; в противном случае делегат возвращает по значению.
Необязательный parameter_list задает параметры делегата.
Return_type объявления делегата return-by-value или return-no-value указывает тип результата, если таковой имеется, возвращаемый делегатом.
Ref_return_type объявления делегата return-by-ref указывает тип переменной, на которую ссылается variable_reference (§9.5), возвращаемой делегатом.
Необязательный variant_type_parameter_list (§18.2.3) указывает параметры типа для самого делегата.
Возвращаемый тип делегата должен быть void
либо типом выходных данных (§18.2.3.2.2).
Все типы параметров типа делегата должны быть входобезопасны (§18.2.3.2). Кроме того, все типы выходных или ссылочных параметров также должны быть выходными.
Примечание. Выходные параметры должны быть входобезопасны из-за распространенных ограничений реализации. конечная заметка
Кроме того, каждое ограничение типа класса, ограничение типа интерфейса и ограничение параметра типа для любых параметров типа делегата должно быть безопасным для ввода.
Типы делегатов в C# эквивалентны имени, а не структурно эквивалентны.
Пример:
delegate int D1(int i, double d); delegate int D2(int c, double d);
Типы делегатов
D1
иD2
два разных типа, поэтому они не являются взаимозаменяемыми, несмотря на их идентичные подписи.пример конца
Как и другие объявления универсальных типов, аргументы типов должны быть предоставлены для создания созданного типа делегата. Типы параметров и возвращаемый тип созданного типа делегата создаются путем подстановки для каждого параметра типа в объявлении делегата, соответствующего аргумента типа созданного типа делегата.
Единственным способом объявления типа делегата является delegate_declaration. Каждый тип делегата — это ссылочный тип, производный от System.Delegate
. Элементы, необходимые для каждого типа делегата, подробно описаны в разделе §20.3. Типы делегатов неявно sealed
, поэтому не допускается наследование любого типа от типа делегата. Кроме того, не допускается объявить тип класса, производный от System.Delegate
не делегата. System.Delegate
не является типом делегата; это тип класса, из которого производны все типы делегатов.
20.3 Делегаты
Каждый тип делегата наследует элементы из Delegate
класса, как описано в разделе 15.3.4. Кроме того, каждый тип делегата должен предоставлять не универсальный Invoke
метод, список параметров которого соответствует parameter_list в объявлении делегата, возвращаемый тип которого соответствует return_type или ref_return_type в объявлении делегата, а также для делегатов, которые ref_kind совпадают с этим в объявлении делегата. Метод Invoke
должен быть по крайней мере доступен как содержащий тип делегата. Invoke
Вызов метода для типа делегата семантически эквивалентен использованию синтаксиса вызова делегата (§20.6).
Реализации могут определять дополнительные члены в типе делегата.
За исключением экземпляра, любая операция, которую можно применить к экземпляру класса или класса, также может применяться к классу делегата или экземпляру соответственно. В частности, можно получить доступ к членам типа с помощью обычного синтаксиса доступа к членам System.Delegate
.
Совместимость делегатов 20.4
Тип M
метода или делегата совместим с типом D
делегата, если все из следующих значений имеют значение true:
D
иM
имеет одинаковое количество параметров, и каждый параметр имеетD
один и тот же модификатор параметров по ссылке, что и соответствующий параметр вM
.- Для каждого параметра значения преобразование удостоверений (§10.2.2) или неявное преобразование ссылок (§10.2.8) существует из типа параметра в
D
соответствующий тип параметра.M
- Для каждого параметра по ссылке тип параметра совпадает с типом
D
параметра вM
. - Одно из следующих значений:
D
иM
оба возвращает значение без значенияD
иM
возвращаются по значению (§15.6.1, §20.2), а удостоверение или неявное преобразование ссылок существует из возвращаемого типаM
в возвращаемый типD
.D
иM
оба возвращается по ссылке, преобразование удостоверений существует между типом возвращаемого значения и типомM
возвращаемого значенияD
, и оба имеют одинаковые ref_kind.
Это определение совместимости позволяет ковариации в возвращаемом типе и контравариации в типах параметров.
Пример:
delegate int D1(int i, double d); delegate int D2(int c, double d); delegate object D3(string s); class A { public static int M1(int a, double b) {...} } class B { public static int M1(int f, double g) {...} public static void M2(int k, double l) {...} public static int M3(int g) {...} public static void M4(int g) {...} public static object M5(string s) {...} public static int[] M6(object o) {...} }
Методы
A.M1
и совместимы как с типами делегатовD1
,B.M1
так иD2
с тем, что они имеют одинаковый тип возврата и список параметров. МетодыB.M2
,B.M3
иB.M4
несовместимы с типами делегатов иD2
так как они имеют разные типы возвращаемыхD1
или списков параметров. МетодыB.M5
иB.M6
совместимы с типомD3
делегата.пример конца
Пример:
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} }
Метод
X.F
совместим с типомPredicate<int>
делегата и методX.G
совместим с типомPredicate<string>
делегата.пример конца
Примечание. Интуитивно понятное значение совместимости делегатов заключается в том, что метод совместим с типом делегата, если каждое вызов делегата может быть заменено вызовом метода без нарушения безопасности типов, рассматривая необязательные параметры и массивы параметров как явные параметры. Например, в следующем коде:
delegate void Action<T>(T arg); class Test { static void Print(object value) => Console.WriteLine(value); static void Main() { Action<string> log = Print; log("text"); } }
Метод
Action<string>
делегатаAction<string>
, так как любой вызов делегата также является допустимым вызовомЕсли подпись приведенного
Print(object value, bool prependTimestamp = false)
, например,Action<string>
правилами этого предложения.конечная заметка
Экземпляр делегата 20.5
Экземпляр делегата создается delegate_creation_expression (§12.8.16.6), преобразование в тип делегата, сочетание делегатов или удаление делегата. Затем созданный экземпляр делегата ссылается на один или несколько:
- Статический метод, на который ссылается delegate_creation_expression, или
- Целевой объект (который не может быть) и метод экземпляра, на который ссылается
null
delegate_creation_expression, или - Другой делегат (§12.8.16.6).
Пример:
delegate void D(int x); class C { public static void M1(int i) {...} public void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // Static method C t = new C(); D cd2 = new D(t.M2); // Instance method D cd3 = new D(cd2); // Another delegate } }
пример конца
Набор методов, инкапсулированных экземпляром делегата, называется списком вызовов. При создании экземпляра делегата из одного метода он инкапсулирует этот метод, а его список вызовов содержит только одну запись. Однако при объединении двух экземпляров, не являющихсяnull
делегатами, их списки вызовов объединяются (в порядке левого операнда, а затем правый операнд), чтобы сформировать новый список вызовов, содержащий два или более записей.
При создании нового делегата из одного делегата результирующий список вызовов имеет только одну запись, которая является исходным делегатом (§12.8.16.6).
Делегаты объединяются с помощью двоичного файла +
(§12.10.5) и +=
операторов (§12.21.4). Делегат можно удалить из сочетания делегатов с помощью двоичного -
файла (§12.10.6) и -=
операторов (§12.21.4). Делегаты можно сравнить с равенством (§12.12.9).
Пример. В следующем примере показано создание экземпляра нескольких делегатов и соответствующие списки вызовов:
delegate void D(int x); class C { public static void M1(int i) {...} public static void M2(int i) {...} } class Test { static void Main() { D cd1 = new D(C.M1); // M1 - one entry in invocation list D cd2 = new D(C.M2); // M2 - one entry D cd3 = cd1 + cd2; // M1 + M2 - two entries D cd4 = cd3 + cd1; // M1 + M2 + M1 - three entries D cd5 = cd4 + cd3; // M1 + M2 + M1 + M1 + M2 - five entries D td3 = new D(cd3); // [M1 + M2] - ONE entry in invocation // list, which is itself a list of two methods. D td4 = td3 + cd1; // [M1 + M2] + M1 - two entries D cd6 = cd4 - cd2; // M1 + M1 - two entries in invocation list D td6 = td4 - cd2; // [M1 + M2] + M1 - two entries in invocation list, // but still three methods called, M2 not removed. } }
Когда
cd1
иcd2
создаются экземпляры, каждый из них инкапсулирует один метод. Приcd3
создании экземпляра он содержит список вызовов двух методовM1
иM2
в этом порядке.cd4
Список вызовов содержитM1
,M2
иM1
, в этом порядке. Дляcd5
этого списка вызововM1
содержится ,M2
M1
,M1
и , иM2
, в этом порядке.При создании делегата из другого делегата с delegate_creation_expression результат имеет список вызовов с другой структурой, но это приводит к тому же методу, вызываемому в том же порядке. При
td3
создании изcd3
списка вызовов имеет только один член, но этот член является списком методовM1
иM2
эти методы вызываются в том же порядке, что и они вызываютсяtd3
cd3
. Аналогично, когдаtd4
создается экземпляр списка вызовов, есть только две записи, но вызывается три методаM1
,M2
иM1
в этом порядке так же, какcd4
и.Структура списка вызовов влияет на вычитание делегатов. Делегат
cd6
, созданный путем вычитанияcd2
(который вызываетM2
) изcd4
(который вызываетM1
M2
, и ) вызываетM1
иM1
M1
. Однако делегатtd6
, созданный путем вычитанияcd2
(который вызываетM2
) изtd4
(которыйM2
M1
вызывает, иM1
) по-прежнему вызываетM1
M2
, аM1
в этом порядке не является одной записью в списке,M2
но членом вложенного списка. Дополнительные примеры объединения (а также удаления) делегатов см. в разделе §20.6.пример конца
После создания экземпляра экземпляр делегата всегда ссылается на тот же список вызовов.
Примечание. Помните, что при объединении двух делегатов или удалении одного из них новые результаты делегата с собственным списком вызовов; списки вызовов делегатов, объединенных или удаленных, остаются неизменными. конечная заметка
Вызов делегата 20.6
C# предоставляет специальный синтаксис для вызова делегата. При вызове экземпляра, не являющегосяnull
делегатом, список вызовов которого содержит одну запись, вызывается один метод с теми же аргументами, которые он был задан, и возвращает то же значение, что и метод. (Подробные сведения о вызове делегата см. в статье 12.8.9.4 . Если исключение возникает во время вызова такого делегата, и это исключение не перехватывается в вызываемом методе, поиск предложения catch исключений продолжается в методе, который вызывается делегатом, как если бы этот метод напрямую вызвал метод, к которому ссылается этот делегат.
Вызов экземпляра делегата, список вызовов которого содержит несколько записей, продолжается путем вызова каждого из методов в списке вызовов, синхронно, в порядке. Каждый метод, так называемый, передает тот же набор аргументов, что и для экземпляра делегата. Если такой вызов делегата включает ссылочные параметры (§15.6.2.3.3), вызов каждого метода будет выполняться со ссылкой на ту же переменную; изменения этой переменной по одному методу в списке вызовов будут видимы для методов, дальнейших вниз по списку вызовов. Если вызов делегата включает выходные параметры или возвращаемое значение, их окончательное значение будет поступать из вызова последнего делегата в списке. Если исключение возникает во время обработки вызова такого делегата, и это исключение не перехватывается в вызываемом методе, поиск предложения перехвата исключений продолжается в методе, вызываемом делегатом, и все методы, дальнейшие вниз по списку вызовов, не вызываются.
Попытка вызова экземпляра делегата, значение которого приводит null
к исключению типа System.NullReferenceException
.
Пример. В следующем примере показано, как создать экземпляр, объединить, удалить и вызвать делегатов:
delegate void D(int x); class C { public static void M1(int i) => Console.WriteLine("C.M1: " + i); public static void M2(int i) => Console.WriteLine("C.M2: " + i); public void M3(int i) => Console.WriteLine("C.M3: " + i); } class Test { static void Main() { D cd1 = new D(C.M1); cd1(-1); // call M1 D cd2 = new D(C.M2); cd2(-2); // call M2 D cd3 = cd1 + cd2; cd3(10); // call M1 then M2 cd3 += cd1; cd3(20); // call M1, M2, then M1 C c = new C(); D cd4 = new D(c.M3); cd3 += cd4; cd3(30); // call M1, M2, M1, then M3 cd3 -= cd1; // remove last M1 cd3(40); // call M1, M2, then M3 cd3 -= cd4; cd3(50); // call M1 then M2 cd3 -= cd2; cd3(60); // call M1 cd3 -= cd2; // impossible removal is benign cd3(60); // call M1 cd3 -= cd1; // invocation list is empty so cd3 is null // cd3(70); // System.NullReferenceException thrown cd3 -= cd1; // impossible removal is benign } }
Как показано в инструкции
cd3 += cd1;
, делегат может присутствовать в списке вызовов несколько раз. В этом случае он просто вызывается один раз для каждого вхождения. В списке вызовов, например при удалении этого делегата, последний вхождения в списке вызовов является фактически удаленным.Непосредственно перед выполнением окончательной инструкции
cd3 -= cd1
; делегатcd3
ссылается на пустой список вызовов. Попытка удалить делегат из пустого списка (или удалить несуществующий делегат из непустого списка) не является ошибкой.Выходные данные создаются:
C.M1: -1 C.M2: -2 C.M1: 10 C.M2: 10 C.M1: 20 C.M2: 20 C.M1: 20 C.M1: 30 C.M2: 30 C.M1: 30 C.M3: 30 C.M1: 40 C.M2: 40 C.M3: 40 C.M1: 50 C.M2: 50 C.M1: 60 C.M1: 60
пример конца
ECMA C# draft specification