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
, protected
internal
, 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
iD2
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.Delegate
klasy . 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.Delegate
klasy . 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
iM
mają taką samą liczbę parametrów, a każdy parametr w plikuD
ma ten sam modyfikator parametru by-reference co odpowiedni parametr w elemencieM
.- 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 plikuM
. - Jedną z następujących wartości jest prawda:
D
wartości iM
są zwracane bez wartościD
iM
są zwracane według wartości (§15.6.1, §20.2), a tożsamość lub niejawna konwersja odwołania istnieje z typuM
zwracanego do zwracanego typuD
.D
iM
są zarówno zwracane przez ref, istnieje konwersja tożsamości między typem zwracanym i zwracanym typemM
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
iB.M1
są zgodne zarówno z typami delegatówD1
, jak iD2
, ponieważ mają ten sam typ zwracany i listę parametrów. MetodyB.M2
,B.M3
iB.M4
są niezgodne z typami delegatówD1
iD2
, ponieważ mają różne typy zwracane lub listy parametrów. MetodyB.M5
iB.M6
są zgodne z typemD3
delegata .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 typemPredicate<int>
delegata, a metodaX.G
jest zgodna z typemPredicate<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
Action<string>
, ponieważ każde wywołanieAction<string>
delegata będzie również prawidłowym wywołaniemJeśli podpis powyższej
Print(object value, bool prependTimestamp = false)
przykład,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
icd2
są tworzone wystąpienia, każda z nich hermetyzuje jedną metodę. Pocd3
utworzeniu wystąpienia ma on listę wywołań dwóch metod iM1
M2
, w tej kolejności.cd4
Lista wywołańM1
zawiera wartości ,M2
iM1
, w tej kolejności. W przypadkucd5
metody lista wywołań zawieraM1
wartości ,M2
,M1
,M1
iM2
, 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 podstawiecd3
listy wywołań, ma tylko jeden element członkowski, ale ten element członkowski jest listą metodM1
iM2
metody te są wywoływane wtd3
tej samej kolejności, w której są wywoływane przezcd3
element . Podobnie gdytd4
jest tworzone wystąpienie listy wywołań ma tylko dwa wpisy, ale wywołuje trzy metodyM1
,M2
iM1
, w tej kolejności tak samo jakcd4
.Struktura listy wywołań ma wpływ na odejmowanie delegatów. Deleguj
cd6
metodę , utworzoną przez odjęciecd2
(które wywołujeM2
metodę ) zcd4
(która wywołujeM1
wywołania ,M2
iM1
) wywołańM1
iM1
. Jednak delegattd6
, utworzony przez odjęciecd2
(które wywołuje ) ztd4
(który wywołujeM1
M2
,M2
iM1
) nadal wywołujeM1
M2
, iM1
, 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 -= cd1
delegatcd3
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
ECMA C# draft specification