第 17 章 委托

第 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
2
3
4
5
6
7
8
9
10
11
internal class Feedback : System.MulticastDelegate {
// 构造器
public Feedback(Object @object, IntPtr method);

// 这个方法的原型和源代码指定的一样
public virtual void Invoke(Int32 value);

// 以下方法实现对回调方法的异步问题
public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object @object);
public virtual void EndInvoke(IAsyncResult result);
}

表 17-1 MulticastDelegate 的三个重要的非公共字段

字段 类型 说明
_target System.Object 当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐式参数 this 的值
_methodPtr System.IntPtr 一个内部的整数值,CLR用它标识要回调的方法
_invocationList System.Object 该字段通常为 null。构造委托链时它引用一个委托数组(详情参见下一节)
1
2
3
4
5
6
7
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// 如果指定了任何回调,就调用它们
if (fb != null)
fb(val);
}
}

这段代码看上去像是调用了一个名为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);

17-4
17-5
17-6

以伪代码的形式,FeedbackInvoke 方法基本上是像下面这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void Invoke(Int32 value) {
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null ) {
// 这个委托数组指定了应该调用的委托
foreach (Feedback d in delegates)
d(value); // 调用每个委托
} else { // 否则就不是委托链
// 该委托标识了要回调的单个方法,
// 在指定的目标对象上调用这个回调方法
_methodPtr.Invoke(_target, value);
// 上面这行代码接近实际的代码,
// 实际发生的事情用 C# 是表示不出来的
}
}

数组中的每个委托被调用时,其返回值被保存到 result 变量中。循环完成后,result 变量只包含调用的最后一个委托的结果(前面的返回值会被丢弃)

17.5.1 C# 对委托链的支持

为方便 C# 开发人员,C# 编译器自动为委托类型的实例重载了 +=-=操作符。这些操作符分别调用 Delegate.CombineDelegate.Remove。可用这些操作符简化委托链的构造。

17.5.2 取得对委托链调用的更多控制

MulticastDelegate 类提供了一个实例方法 GetInvocationList,用于显式调用链中的每一个委托,并允许你使用需要的任何算法:

1
2
3
4
public abstract class MulticastDelegate : Delegate {
// 创建一个委托数组,其中每个元素都引用链中的一个委托
public sealed override Delegate[] GetInvocationList();
}

17.6 委托定义不要太多(泛型委托)

.NET Framework 现在支持泛型,所以实际只需几个泛型委托(在 System 命名空间中定义)就能表示需要获取多达 16 个参数的方法:

1
2
3
4
5
6
public delegate void Action();   // OK,这个不是泛型
public delegate void Action<T>(T obj);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
...
public delegate void Action<T1, ..., T16>(T1 arg1, ..., T16 arg16);

除了 Action 委托,.NET Framework 还提供了 17 个 Func 函数,允许回调方法返回值:

1
2
3
4
5
6
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
...
public delegate TResult Func<T1,..., T16, TResult>(T1 arg1, ..., T16 arg16);

如需使用refout关键字以传引用的方式传递参数,就可能不得不定义自己的委托:

delegate void Bar(ref Int32 z);

如果委托要通过 C#的 params 关键字获取数量可变的参数,要为委托的任何参数指定默认值,或者要对委托的泛型类型参数进行约束,也必须定义自己的委托类型。

获取泛型实参并返回值的委托支持逆变和协变

17.7 C#为委托提供的简化语法

button1.Cilck += new EventHandler(button1_Click);
其中的button1_CLick是方法,看起来像下面这样:

1
2
3
void button1_Click(Object sender, EventArgs e) {
// 按钮单击后要做的事情...
}

构造 EventHandler 委托对象是 CLR 要求的,因为这个对象提供了一个包装器,可确保(被包装的)方法只能以类型安全的方式调用。这个包装器还支持调用实例方法和委托链。

语法糖:
button1.Click += button1_Click;

17.7.1 简化语法 1: 不需要构造委托对象

如前所述,C# 允许指定回调方法的名称,不必构造委托对象包装器。

17.7.2 简化语法2:不需要定义回调方法(lambda 表达式)

1
2
3
4
5
internal sealed class AClass {
public static void CallbackWithoutNewingADelegateObject() {
ThreadPool.QueueUserWorkItem( obj => Console.WriteLine(obj), 5);
}
}

编译器看到这个 lambda 表达式之后,会在类(本例是 AClass)中自定义一个新的私有方法。这个新方法称为匿名函数,因为方法名称由编译器自动创建,而且你一般不知道这个名称。

编译器生成的匿名函数总是私有方法

=>操作符左侧供指定传给 lambda 表达式的参数的名称。下例总结了一些规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果委托不获取任何参数,就使用 ()
Func<String> f = () => "Jeff";

// 如果委托获取 1 个或更多参数,编译器可推断类型
Func<Int32, String> f2 = (Int32 n) => n.ToString();
Func<Int32, Int32, String> f3 = (Int32 n1, Int32 n2) => (n1 + n2).ToString();

// 如果委托获取 1 个或更多参数,编译器可推断类型
Func<Int32, String> f4 = (n) => n.ToString();
Func<Int32, Int32, String> f5 = (n1, n2) => (n1 + n2).ToString();

// 如果委托获取 1 个参数,可省略(和)
Func<Int32, String> f6 = n => n.ToString();

// 如果委托有 ref/out 参数,必须显式指定 ref/out 和类型
Bar b = (out Int32 n) => n = 5;

对于最后一个例子,假定 Bar 的定义如下:

delegate void Bar(out Int32 z);

17.7.3 简化语法 3:局部变量不需要手动包装到类中即可传给回调方法

17.8 委托和反射