第 17 章 委托
第 17 章 委托
NyxX第 17 章 委托
本章内容:
17.2 用委托回调静态方法
在一个类型中通过委托来调用另一个类型的私有成员,只要委托对象是由具有足够安全性/可访问性的代码创建的,便没有问题。
将方法绑定到委托时,C# 和 CLR 都允许引用类型的协变性(covariance)和逆变性(contravariance)。协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。例如下面这个委托:delegate Object MyCallback(FileStream s);
完全可以构造该委托类型的一个实例并绑定具有以下原型的方法:String SomeMethod(Stream s);
在这里,SomeMethod 的返回类型(String)派生自委托的返回类型(Object);这种协变性是允许的。SomeMethod的参数类型(Stream)是委托的参数类型(FileStream)的基类;这种逆变性是允许的。
注意,只有引用类型才支持协变性与逆变性,值类型或void不支持。所以,不能把下面的方法绑定到MyCallback委托:
Int32 SomeOtherMethod(Stream s);
虽然SomeOtherMethod 的返回类型(Int32)派生自(MyCallback)的返回类型(Object),但这种形式的协变性是不允许的,因为Int32是值类型。
17.3 用委托回调实例方法
如果是实例方法,委托要知道方法操作的是具体哪个对象实例。包装实例方法很有用,因为对象内部的代码可以访问对象的实例成员。这意味着对象可以维护一些状态,并在回调方法执行期间利用这些状态信息。
17.4 委托揭秘
编译器和 CLR 在幕后做了大量工作来隐藏复杂性。
首先重新审视这一行代码:
internal delegate void Feedback(Int32 value);
看到这行代码后,编译器实际会像下面这样定义一个完整的类:
1 | internal class Feedback : System.MulticastDelegate { |
表 17-1 MulticastDelegate 的三个重要的非公共字段
| 字段 | 类型 | 说明 |
|---|---|---|
_target |
System.Object |
当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐式参数 this 的值 |
_methodPtr |
System.IntPtr |
一个内部的整数值,CLR用它标识要回调的方法 |
_invocationList |
System.Object |
该字段通常为 null。构造委托链时它引用一个委托数组(详情参见下一节) |
1 | private static void Counter(Int32 from, Int32 to, Feedback fb) { |
这段代码看上去像是调用了一个名为fb的函数,并向它传递一个参数(val)。但事实上,这里没有名为 fb 的函数。因为编译器知道 fb 是引用了委托对象的变量,所以会生成代码调用该委托对象的Invoke 方法。也就是说,编译器在看到以下代码时:
fb(val);
它将生成以下代码,好像源代码本来就是这么写的一样:
fb.Invoke(val);
17.5 用委托回调多个方法(委托连)
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);fbChain = (Feedback) Delegate.Combine(fbChain, fb2);fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
以伪代码的形式,Feedback 的 Invoke 方法基本上是像下面这样实现的:
1 | public void Invoke(Int32 value) { |
数组中的每个委托被调用时,其返回值被保存到 result 变量中。循环完成后,result 变量只包含调用的最后一个委托的结果(前面的返回值会被丢弃)
17.5.1 C# 对委托链的支持
为方便 C# 开发人员,C# 编译器自动为委托类型的实例重载了 += 和 -=操作符。这些操作符分别调用 Delegate.Combine 和 Delegate.Remove。可用这些操作符简化委托链的构造。
17.5.2 取得对委托链调用的更多控制
MulticastDelegate 类提供了一个实例方法 GetInvocationList,用于显式调用链中的每一个委托,并允许你使用需要的任何算法:
1 | public abstract class MulticastDelegate : Delegate { |
17.6 委托定义不要太多(泛型委托)
.NET Framework 现在支持泛型,所以实际只需几个泛型委托(在 System 命名空间中定义)就能表示需要获取多达 16 个参数的方法:
1 | public delegate void Action(); // OK,这个不是泛型 |
除了 Action 委托,.NET Framework 还提供了 17 个 Func 函数,允许回调方法返回值:
1 | public delegate TResult Func<TResult>(); |
如需使用ref或out关键字以传引用的方式传递参数,就可能不得不定义自己的委托:
delegate void Bar(ref Int32 z);
如果委托要通过 C#的 params 关键字获取数量可变的参数,要为委托的任何参数指定默认值,或者要对委托的泛型类型参数进行约束,也必须定义自己的委托类型。
获取泛型实参并返回值的委托支持逆变和协变
17.7 C#为委托提供的简化语法
button1.Cilck += new EventHandler(button1_Click);
其中的button1_CLick是方法,看起来像下面这样:
1 | void button1_Click(Object sender, EventArgs e) { |
构造 EventHandler 委托对象是 CLR 要求的,因为这个对象提供了一个包装器,可确保(被包装的)方法只能以类型安全的方式调用。这个包装器还支持调用实例方法和委托链。
语法糖:button1.Click += button1_Click;
17.7.1 简化语法 1: 不需要构造委托对象
如前所述,C# 允许指定回调方法的名称,不必构造委托对象包装器。
17.7.2 简化语法2:不需要定义回调方法(lambda 表达式)
1 | internal sealed class AClass { |
编译器看到这个 lambda 表达式之后,会在类(本例是 AClass)中自定义一个新的私有方法。这个新方法称为匿名函数,因为方法名称由编译器自动创建,而且你一般不知道这个名称。
编译器生成的匿名函数总是私有方法
=>操作符左侧供指定传给 lambda 表达式的参数的名称。下例总结了一些规则:
1 | // 如果委托不获取任何参数,就使用 () |
对于最后一个例子,假定 Bar 的定义如下:
delegate void Bar(out Int32 z);








