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


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");
    }
}

Метод Print совместим с типом Action<string> делегата Action<string> , так как любой вызов делегата также является допустимым вызовом Print метода.

Если подпись приведенного Print выше метода была изменена Print(object value, bool prependTimestamp = false) , например, Print метод больше не будет совместим с Action<string> правилами этого предложения.

конечная заметка

Экземпляр делегата 20.5

Экземпляр делегата создается delegate_creation_expression (§12.8.16.6), преобразование в тип делегата, сочетание делегатов или удаление делегата. Затем созданный экземпляр делегата ссылается на один или несколько:

  • Статический метод, на который ссылается delegate_creation_expression, или
  • Целевой объект (который не может быть) и метод экземпляра, на который ссылается nulldelegate_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содержится , M2M1, M1и , и M2, в этом порядке.

При создании делегата из другого делегата с delegate_creation_expression результат имеет список вызовов с другой структурой, но это приводит к тому же методу, вызываемому в том же порядке. При td3 создании из cd3 списка вызовов имеет только один член, но этот член является списком методов M1 и M2 эти методы вызываются в том же порядке, что и они вызываются td3 cd3. Аналогично, когда td4 создается экземпляр списка вызовов, есть только две записи, но вызывается три метода M1, M2и M1в этом порядке так же, как cd4 и.

Структура списка вызовов влияет на вычитание делегатов. Делегатcd6, созданный путем вычитания cd2 (который вызываетM2) из cd4 (который вызываетM1M2, и ) вызывает M1 и M1M1. Однако делегатtd6, созданный путем вычитания cd2 (который вызывает M2) из td4 (который M2M1вызывает, и M1) по-прежнему вызываетM1M2, а 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

пример конца