Udostępnij za pośrednictwem


20 delegatów

20.1 Ogólne

Deklaracja delegata definiuje klasę pochodzącą z klasy System.Delegate. Wystąpienie delegata hermetyzuje listę wywołań, która jest listą co najmniej jednej metody, z których każda jest nazywana jednostką wywoływaną. Na przykład wywoływana jednostka składa się z wystąpienia i metody w tym wystąpieniu. W przypadku metod statycznych jednostka z możliwością wywołania składa się tylko z metody. Wywołanie wystąpienia delegata z odpowiednim zestawem argumentów powoduje wywołanie każdej z jednostek z możliwością wywołania delegata przy użyciu danego zestawu argumentów.

Uwaga: Interesująca i przydatna właściwość wystąpienia delegata polega na tym, że nie wie ani nie dba o klasy metod, które hermetyzuje; wszystko, co ma znaczenie, że te metody są zgodne (§20.4) z typem delegata. Dzięki temu delegaci doskonale nadają się do wywołania "anonimowego". notatka końcowa

20.2 Deklaracje delegatów

Delegate_declaration jest type_declaration (§14.7), który deklaruje nowy typ delegata.

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 jest zdefiniowany w §23.2.

Jest to błąd czasu kompilacji dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji delegata.

Deklaracja delegata dostarczająca variant_type_parameter_list jest ogólną deklaracją delegata. Ponadto każdy delegat zagnieżdżony wewnątrz deklaracji klasy ogólnej lub deklaracji ogólnej struktury jest samą deklaracją delegata ogólnego, ponieważ argumenty typu dla typu zawierającego należy podać w celu utworzenia skonstruowanego typu (§8.4).

Modyfikator new jest dozwolony tylko dla delegatów zadeklarowanych w ramach innego typu, w którym przypadku określa, że taki delegat ukrywa dziedziczony element członkowski o tej samej nazwie, jak opisano w §15.3.5.

Modyfikatory public, protectedinternal, i private kontrolują dostępność typu delegata. W zależności od kontekstu, w którym występuje deklaracja delegata, niektóre z tych modyfikatorów mogą nie być dozwolone (§7.5.2).

Nazwa typu delegata to identyfikator.

Podobnie jak w przypadku metod (§15.6.1), jeśli ref jest obecny, delegat zwraca wartość-by-ref; w przeciwnym razie, jeśli return_type jest void, delegat zwraca wartość-no-value; w przeciwnym razie delegat zwraca wartość-by-value.

Opcjonalny parameter_list określa parametry delegata.

Return_type deklaracji delegata return-by-value lub return-no-value określa typ wyniku, jeśli istnieje, zwracany przez delegata.

Ref_return_type deklaracji delegata return-by-ref określa typ zmiennej przywoływanej przez variable_reference (§9.5) zwrócony przez delegata.

Opcjonalny variant_type_parameter_list (§18.2.3) określa parametry typu do samego delegata.

Zwracany typ typu delegata to void, lub wyjściowy bezpieczny (§18.2.3.2).

Wszystkie typy parametrów typu delegata muszą być bezpieczne wejściowe (§18.2.3.2). Ponadto wszystkie typy parametrów wyjściowych lub referencyjnych również są bezpieczne dla danych wyjściowych.

Uwaga: Parametry wyjściowe muszą być bezpieczne dla danych wejściowych ze względu na typowe ograniczenia implementacji. notatka końcowa

Ponadto każde ograniczenie typu klasy, ograniczenie typu interfejsu i ograniczenie parametru typu dla dowolnych parametrów typu delegata musi być bezpieczne dla danych wejściowych.

Typy delegatów w języku C# są równoważne, a nie równoważne strukturalnie.

Przykład:

delegate int D1(int i, double d);
delegate int D2(int c, double d);

Typy delegatów D1 i D2 są dwoma różnymi typami, więc nie są zamienne, pomimo identycznych podpisów.

przykład końcowy

Podobnie jak w przypadku innych deklaracji typów ogólnych, argumenty typu należy podać w celu utworzenia skonstruowanego typu delegata. Typy parametrów i typ zwracany typu skonstruowanego delegata są tworzone przez podstawianie, dla każdego parametru typu w deklaracji delegata, odpowiadający typ argumentu skonstruowanego typu delegata.

Jedynym sposobem deklarowania typu delegata jest użycie delegate_declaration. Każdy typ delegata jest typem referencyjnym pochodzącym z System.Delegateklasy . Składowe wymagane dla każdego typu delegata są szczegółowo opisane w §20.3. Typy delegatów są niejawnie sealed, więc nie można uzyskać dowolnego typu z typu delegata. Nie można również zadeklarować typu klasy innej niż delegacyjnej pochodzącego z System.Delegateklasy . System.Delegate nie jest typem delegata; jest to typ klasy, z którego pochodzą wszystkie typy delegatów.

20.3 Delegowanie członków

Każdy typ delegata dziedziczy składowe z klasy zgodnie z Delegate opisem w §15.3.4. Ponadto każdy typ delegata udostępnia metodę niegeneryjną Invoke , której lista parametrów pasuje do parameter_list w deklaracji delegata, której typ zwracany odpowiada return_type lub ref_return_type w deklaracji delegata, oraz dla delegatów zwracanych przez ref, których ref_kind pasuje do tego w deklaracji delegata. Invoke Metoda jest co najmniej tak dostępna, jak typ delegata zawierającego. Invoke Wywoływanie metody w typie delegata jest semantycznie równoważne użyciu składni wywołania delegata (§20.6) .

Implementacje mogą definiować dodatkowe elementy członkowskie w typie delegata.

Z wyjątkiem wystąpienia, każda operacja, którą można zastosować do klasy lub wystąpienia klasy, może być również stosowana do klasy delegata lub wystąpienia, odpowiednio. W szczególności można uzyskać dostęp do elementów członkowskich System.Delegate typu za pośrednictwem zwykłej składni dostępu do składowych.

20.4 Delegowanie zgodności

Typ M metody lub delegata jest zgodny z typem D delegata, jeśli spełnione są wszystkie następujące warunki:

  • D i M mają taką samą liczbę parametrów, a każdy parametr w pliku D ma ten sam modyfikator parametru by-reference co odpowiedni parametr w elemencie M.
  • Dla każdego parametru wartości istnieje konwersja tożsamości (§10.2.2) lub niejawna konwersja odwołania (§10.2.8) z typu parametru w D pliku do odpowiedniego typu parametru w .M
  • Dla każdego parametru by-reference typ parametru w pliku D jest taki sam jak typ parametru w pliku M.
  • Jedną z następujących wartości jest prawda:
    • D wartości i M są zwracane bez wartości
    • D i M są zwracane według wartości (§15.6.1, §20.2), a tożsamość lub niejawna konwersja odwołania istnieje z typu M zwracanego do zwracanego typu D.
    • D i M są zarówno zwracane przez ref, istnieje konwersja tożsamości między typem zwracanym i zwracanym typem M D, a oba mają ten sam ref_kind.

Ta definicja zgodności umożliwia wariancję w typach zwracanych i kontrawariancji w typach parametrów.

Przykład:

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) {...}
}

Metody A.M1 i B.M1 są zgodne zarówno z typami delegatów D1 , jak i D2, ponieważ mają ten sam typ zwracany i listę parametrów. Metody B.M2, B.M3i B.M4 są niezgodne z typami delegatów D1 i D2, ponieważ mają różne typy zwracane lub listy parametrów. Metody B.M5 i B.M6 są zgodne z typem D3delegata .

przykład końcowy

Przykład:

delegate bool Predicate<T>(T value);

class X
{
    static bool F(int i) {...}
    static bool G(string s) {...}
}

Metoda X.F jest zgodna z typem Predicate<int> delegata, a metoda X.G jest zgodna z typem Predicate<string>delegata .

przykład końcowy

Uwaga: Intuicyjne znaczenie zgodności delegata polega na tym, że metoda jest zgodna z typem delegata, jeśli każde wywołanie delegata może zostać zastąpione wywołaniem metody bez naruszenia bezpieczeństwa typu, traktowanie opcjonalnych parametrów i tablic parametrów jako jawnych parametrów. Na przykład w poniższym kodzie:

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

Metoda Print jest zgodna z typem delegata Action<string> , ponieważ każde wywołanie Action<string> delegata będzie również prawidłowym wywołaniem Print metody.

Jeśli podpis powyższej Print metody został zmieniony na Print(object value, bool prependTimestamp = false) przykład, Print metoda nie będzie już zgodna z regułami Action<string> tej klauzuli.

notatka końcowa

20.5 Delegowanie wystąpienia

Wystąpienie delegata jest tworzone przez delegate_creation_expression (§12.8.16.6), konwersję na typ delegata, kombinację delegata lub usuwanie delegata. Nowo utworzone wystąpienie delegata odwołuje się do co najmniej jednego:

  • Metoda statyczna, do których odwołuje się delegate_creation_expression, lub
  • Obiekt docelowy (który nie może być null) i metoda wystąpienia, do której odwołuje się delegate_creation_expression, lub
  • Inny pełnomocnik (§12.8.16.6).

Przykład:

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
    }
}

przykład końcowy

Zestaw metod hermetyzowanych przez wystąpienie delegata jest nazywany listą wywołań. Po utworzeniu wystąpienia delegata na podstawie jednej metody hermetyzuje tę metodę, a lista wywołań zawiera tylko jeden wpis. Jednak po połączeniu dwóch wystąpień innych niżnull delegat ich listy wywołań są łączone — w kolejności lewej operand, a następnie prawy operand — w celu utworzenia nowej listy wywołań, która zawiera co najmniej dwa wpisy.

Po utworzeniu nowego delegata na podstawie pojedynczego delegata wynikowa lista wywołań ma tylko jeden wpis, który jest pełnomocnikiem źródłowym (§12.8.16.6).

Delegaty są łączone przy użyciu danych binarnych + (§12.10.5) i += operatorów (§12.21.4). Delegata można usunąć z kombinacji delegatów przy użyciu pliku binarnego - (§12.10.6) i -= operatorów (§12.21.4). Delegatów można porównać pod kątem równości (§12.12.9).

Przykład: W poniższym przykładzie pokazano utworzenie wystąpienia wielu delegatów i odpowiadających im list wywołań:

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.
   }
}

Gdy cd1 i cd2 są tworzone wystąpienia, każda z nich hermetyzuje jedną metodę. Po cd3 utworzeniu wystąpienia ma on listę wywołań dwóch metod i M1 M2, w tej kolejności. cd4Lista wywołań M1zawiera wartości , M2i M1, w tej kolejności. W przypadku cd5metody lista wywołań zawiera M1wartości , M2, M1, M1i M2, w tej kolejności.

Podczas tworzenia delegata z innego delegata z delegate_creation_expression wynik ma listę wywołań z inną strukturą niż oryginalna, ale co powoduje wywołanie tych samych metod w tej samej kolejności. Gdy td3 element jest tworzony na podstawie cd3 listy wywołań, ma tylko jeden element członkowski, ale ten element członkowski jest listą metod M1 i M2 metody te są wywoływane w td3 tej samej kolejności, w której są wywoływane przez cd3element . Podobnie gdy td4 jest tworzone wystąpienie listy wywołań ma tylko dwa wpisy, ale wywołuje trzy metody M1, M2i M1, w tej kolejności tak samo jak cd4 .

Struktura listy wywołań ma wpływ na odejmowanie delegatów. Deleguj cd6metodę , utworzoną przez odjęcie cd2 (które wywołuje M2metodę ) z cd4 (która wywołuje M1wywołania , M2i M1) wywołań M1 i M1. Jednak delegat td6, utworzony przez odjęcie cd2 (które wywołuje ) z td4 (który wywołuje M1M2, M2i M1) nadal wywołuje M1M2 , i M1, w tej kolejności, ponieważ M2 nie jest pojedynczym wpisem na liście, ale członkiem zagnieżdżonej listy. Aby uzyskać więcej przykładów łączenia (a także usuwania) delegatów, zobacz §20.6.

przykład końcowy

Po utworzeniu wystąpienia wystąpienie delegata zawsze odwołuje się do tej samej listy wywołań.

Uwaga: pamiętaj, że gdy dwa delegaty są połączone lub jeden jest usuwany z innego, nowy delegat wyniki z własną listą wywołań; listy wywołań delegatów połączone lub usunięte pozostają niezmienione. notatka końcowa

20.6 Delegowanie wywołania

Język C# udostępnia specjalną składnię wywoływania delegata. Gdy wystąpienie inne niżnull delegata, którego lista wywołań zawiera jeden wpis, jest wywoływana, wywołuje jedną metodę z tymi samymi argumentami, które zostały podane, i zwraca tę samą wartość co określona metoda. (Zobacz §12.8.9.4 , aby uzyskać szczegółowe informacje na temat wywołania delegata). Jeśli podczas wywołania takiego delegata wystąpi wyjątek i ten wyjątek nie zostanie przechwycony w metodzie, która została wywołana, wyszukiwanie klauzuli catch wyjątku będzie kontynuowane w metodzie, która nazwała delegata, tak jakby ta metoda bezpośrednio wywołała metodę, do której odwołuje się ten delegat.

Wywołanie wystąpienia delegata, którego lista wywołań zawiera wiele wpisów, następuje wywołanie każdej z metod na liście wywołań, synchronicznie w kolejności. Każda metoda tak zwana jest przekazywana w tym samym zestawie argumentów, co zostało podane do wystąpienia delegata. Jeśli takie wywołanie delegata zawiera parametry odwołania (§15.6.2.3.3), każde wywołanie metody nastąpi z odwołaniem do tej samej zmiennej; zmiany tej zmiennej przez jedną metodę na liście wywołań będą widoczne dla metod dalej na liście wywołań. Jeśli wywołanie delegata zawiera parametry wyjściowe lub wartość zwracaną, ich końcowa wartość będzie pochodzić z wywołania ostatniego delegata na liście. Jeśli podczas przetwarzania wywołania takiego delegata wystąpi wyjątek i ten wyjątek nie zostanie przechwycony w metodzie, która została wywołana, wyszukiwanie klauzuli catch wyjątku będzie kontynuowane w metodzie, która nazwała delegata, a wszystkie metody dalej w dół listy wywołań nie są wywoływane.

Próba wywołania wystąpienia delegata, którego wartość jest null wynikiem wyjątku typu System.NullReferenceException.

Przykład: W poniższym przykładzie pokazano, jak utworzyć wystąpienie, połączyć, usunąć i wywołać delegatów:

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
    }
}

Jak pokazano w instrukcji cd3 += cd1;, delegat może być obecny na liście wywołań wiele razy. W tym przypadku jest on po prostu wywoływany raz na wystąpienie. Na liście wywołań, takiej jak ta, po usunięciu tego delegata ostatnie wystąpienie na liście wywołań jest tym, które zostało rzeczywiście usunięte.

Bezpośrednio przed wykonaniem ostatecznej instrukcji ; cd3 -= cd1delegat cd3 odwołuje się do pustej listy wywołań. Próba usunięcia delegata z pustej listy (lub usunięcia nieistniejącego delegata z niepustej listy) nie jest błędem.

Wygenerowane dane wyjściowe to:

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

przykład końcowy