20 名代理人
20.1 常规
委托声明定义派生自类的类 System.Delegate
。 委托实例封装调用列表,该列表是一个或多个方法的列表,每个方法称为可调用实体。 对于实例方法,可调用实体由实例和该实例上的方法组成。 对于静态方法,可调用实体仅包含一个方法。 调用具有适当参数集的委托实例会导致每个委托的可调用实体使用给定的参数集调用。
注意:委托实例的一个有趣且有用的属性是,它不知道或关心它封装的方法的类;重要的是,这些方法与委托的类型兼容(§20.4)。 这使得委托非常适合“匿名”调用。 end note
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 中所述。
和public
protected
internal
private
修饰符控制委托类型的可访问性。 根据委托声明的发生上下文,可能不允许其中一些修饰符(§7.5.2)。
委托的类型名称是 标识符。
与方法(§15.6.1)一样,如果 ref
存在,则委托按 ref 返回;否则,如果 return_type 为 void
,则委托返回 no-value;否则,委托将按值返回。
可选 parameter_list 指定委托的参数。
returns-by-value 或 returns-no-value 委托声明的return_type指定委托返回的结果类型(如果有)。
returns-by-ref 委托声明的ref_return_type指定委托返回的variable_reference (§9.5) 所引用的变量的类型。
可选 variant_type_parameter_list (§18.2.3)指定委托本身的类型参数。
委托类型的返回类型应为 void
输出安全(§18.2.3.2)。
委托类型的所有参数类型应为输入安全(§18.2.3.2)。 此外,任何输出或引用参数类型也应是输出安全的。
注意:由于常见的实现限制,输出参数必须安全输入。 end note
此外,委托的任何类型的参数上每个类类型约束、接口类型约束和类型参数约束应是输入安全的。
C# 中的委托类型是名称等效的,在结构上不是等效的。
示例:
delegate int D1(int i, double d); delegate int D2(int c, double d);
委托类型
D1
是D2
两种不同的类型,因此它们不可互换,尽管它们具有相同的签名。end 示例
与其他泛型类型声明一样,应提供类型参数来创建构造的委托类型。 构造委托类型的参数类型和返回类型是通过替换委托声明中每个类型参数(构造的委托类型的对应类型参数)创建的。
声明委托类型的唯一 方法是通过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与委托声明中的ref_kind匹配的返回者。 该方法 Invoke
应至少与包含委托类型一样可访问。 Invoke
对委托类型调用方法在语义上等效于使用委托调用语法(§20.6)。
实现可以在委托类型中定义其他成员。
除了实例化之外,可应用于类或类实例的任何操作也可以分别应用于委托类或实例。 具体而言,可以通过通常的成员访问语法访问类型的成员 System.Delegate
。
20.4 委托兼容性
如果以下所有内容均为 true,则方法或委托类型M
与委托类型D
兼容:
D
并M
具有相同数量的参数,并且每个参数的D
按引用参数修饰符与相应的M
参数相同。- 对于每个值参数,标识转换(§10.2.2)或隐式引用转换(§10.2.8)从参数类型
D
到相应的参数类型M
存在。 - 对于每个按引用参数,参数类型与
D /> 中的 M
参数类型相同。 - 以下情况之一为 true:
此兼容性定义允许返回类型中的协变和参数类型的逆变。
示例:
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
B.M1
与委托类型D1
兼容,并且D2
它们具有相同的返回类型和参数列表。 这些方法B.M2
B.M3
与B.M4
委托类型D1
不兼容,因为它们D2
具有不同的返回类型或参数列表。 方法和B.M5
B.M6
两者都与委托类型D3
兼容。end 示例
示例:
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>
兼容。end 示例
注意:委托兼容性的直观含义是,如果委托的每个调用都可以替换为方法的调用,而不违反类型安全性,将可选参数和参数数组视为显式参数,则该方法与委托类型兼容。 例如,在以下代码中:
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>
此子句的规则兼容。end note
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 } }
end 示例
委托实例封装的方法集称为 调用列表。 从单个方法创建委托实例时,它将封装该方法,并且其调用列表仅包含一个条目。 但是,当两个非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的另一个委托创建委托时, 结果具有与原始结构不同的调用列表,但这会导致以相同顺序调用相同的方法。 从
cd3
调用列表中创建时td3
只有一个成员,但该成员是方法M1
M2
的列表,并且这些方法td3
的调用顺序与调用cd3
的顺序相同。 同样,当td4
实例化其调用列表只有两个条目,但它调用三个方法M1
M2
,并M1
按同样的顺序cd4
。调用列表的结构会影响委托减法。 委托
cd6
,通过减去cd2
(调用M2
) 从cd4
(调用M1
,M2
和M1
) 调用M1
和M1
。 但是td6
,委托是通过从cd2
(调用M1
调用、调用和M1
)M2
M1
M1
中减去(td4
M2
调用)M2
创建的,并且,按该顺序,该M2
顺序不是列表中的单个条目,而是嵌套列表的成员。 有关合并(以及删除)委托的更多示例,请参阅 §20.6。end 示例
实例化后,委托实例始终引用相同的调用列表。
注意:请记住,当两个委托组合在一起或从另一个委托中删除时,具有其自己的调用列表的新委托结果;合并或删除的委托的调用列表保持不变。 end note
20.6 委托调用
C# 提供用于调用委托的特殊语法。 调用其调用列表包含一个条目的非null
委托实例时,它将调用具有给定参数的一个方法,并返回与所引用的方法相同的值。 (有关委托调用的详细信息,请参阅 §12.8.9.4 。如果在调用此类委托期间发生异常,并且未在调用的方法中捕获该异常,则调用委托的方法将继续搜索异常 catch 子句,就好像该方法直接调用了该委托引用的方法一样。
调用其调用列表包含多个条目的委托实例,方法是按顺序调用调用列表中的每个方法。 调用的每个方法都传递与向委托实例提供的参数集相同。 如果此类委托调用包含引用参数(§15.6.2.3.3.3),则每个方法调用都将与对同一变量的引用一起发生;调用列表中的一种方法对变量的更改对调用列表中的其他方法可见。 如果委托调用包括输出参数或返回值,则其最终值将来自列表中最后一个委托的调用。 如果在处理此类委托的调用期间发生异常,并且未在调用的方法中捕获该异常,则调用委托的方法中将继续搜索异常 catch 子句,并且不会调用调用列表后面的任何方法。
尝试调用其值 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
end 示例