委托是一个知道如何调用方法的对象。
委托类型(delegate type)定义委托实例(delegate instances)可以调用的方法类型。具体来说,它定义了方法的返回类型和参数类型。下面定义了一个名为Transformer的委托类型:
delegate int Transformer (int x);
Transformer兼容任何返回类型为int且只有一个int形参的方法,例如:
static int Square (int x) { return x * x; }
或者:
static int Square (int x) => x * x;
将一个方法赋值给一个委托变量会创建一个委托实例,可以像调用方法一样调用这个委托实例。
delegate int Transformer (int x);
class Test
{
static void Main()
{
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
}
static int Square (int x) => x * x;
}
委托实例实际上充当调用方的委托:调用方调用委托,然后委托调用目标方法。这种间接方法将调用者与目标方法解耦。
语句:
Transformer t = Square;
是下面语句的缩写:
Transformer t = new Transformer (Square);
从技术上讲,当引用不带括号和参数的Square时,指定了一个方法组 (method group)。如果该方法是重载的,C# 将根据赋值给它的委托的签名选择正确的重载。
表达式:
t(3)
是下面语句的缩写:
t.Invoke(3)
委托类似于回调(callback),这是一个捕获构造(如C函数指针)的通用术语。
委托变量在运行时被分配一个方法。这对于编写插件方法非常有用。在本例中,有一个名为Transform的实用方法,该方法将转换应用于整数数组中的每个元素。Transform方法有一个委托参数,用于指定插件转换。
public delegate int Transformer (int x);
class Util
{
public static void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
class Test
{
static void Main()
{
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Hook in the Square method
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
}
static int Square (int x) => x * x;
}
上面的Transform方法是一个高阶函数(higher-order function),因为它是一个以函数为参数的函数。(返回一个委托的方法也是高阶函数。)
所有委托实例都具有多播功能。这意味着一个委托实例不仅可以引用单个目标方法,还可以引用一系列目标方法。+
和 +=
操作符组合了委托实例。例如:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
// d = d + SomeMethod2; // 与上一语句功能相同
调用 d 现在将同时调用 SomeMethod1 和 SomeMethod2。调用委托的顺序与添加委托的顺序相同。
-
和 -=
操作符从左委托操作数中移除右委托操作数。例如:
d -= SomeMethod1;
调用 d 现在只会调用 SomeMethod2。
在一个具有 null
值的委托变量上调用 +
或 +=
是有效的,相当于将该变量赋值为一个新值:
SomeDelegate d = null;
d += SomeMethod1; // Equivalent (when d is null) to d = SomeMethod1;
类似地,在具有单个目标的委托变量上调用 -=
相当于将 null
赋给该变量。
委托是不可变的(immutable),所以当调用 +=
或 -=
时,实际上是在创建一个新的委托实例,并将其赋值给现有的变量。
如果多播委托具有非空返回类型,则调用方将从最后一个要调用的方法接收返回值。前面的方法仍然会被调用,但是它们的返回值会被丢弃。在使用多播委托的大多数场景中,它们都有 void
返回类型,所以不会出现这种微妙的情况。
所有委托类型隐式派生自 System.MulticastDelegate
,它继承自 System.Delegate
。C# 将在委托上的 +
、-
、+=
和 -=
操作编译为 System.Delegate
类的静态 Combine
和 Remove
方法。
假设编写了一个执行时间很长的方法。该方法可以通过调用委托定期向其调用方报告进度。在这个例子中,HardWork 方法有一个ProgressReporter 委托参数,它调用该参数来指示进度:
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
为了监控进程,Main 方法创建了一个多播委托实例 p,这样进程就由两个独立的方法来监控:
class Test
{
static void Main()
{
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;
Util.HardWork (p);
}
static void WriteProgressToConsole (int percentComplete)
=> Console.WriteLine (percentComplete);
static void WriteProgressToFile (int percentComplete)
=> System.IO.File.WriteAllText ("progress.txt", percentComplete.ToString());
}
当将实例(instance)方法赋值给委托对象时,委托对象不仅必须维护对该方法的引用,还必须维护对该方法所属实例的引用。System.Delegate
类的 Target
属性表示此实例 (对于引用静态方法的委托,这个属性将为空)。例如:
public delegate void ProgressReporter (int percentComplete);
class Test
{
static void Main()
{
X x = new X();
ProgressReporter p = x.InstanceProgress;
p(99); // 99
Console.WriteLine (p.Target == x); // True
Console.WriteLine (p.Method); // Void InstanceProgress(Int32)
}
}
class X
{
public void InstanceProgress (int percentComplete)
=> Console.WriteLine (percentComplete);
}
委托类型可以包含泛型类型参数。例如:
public delegate T Transformer<T> (T arg);
根据这个定义,可以编写一个适用于任何类型的通用 Transform 实用方法:
public class Util
{
public static void Transform<T> (T[] values, Transformer<T> t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
class Test
{
static void Main()
{
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Hook in Square
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
}
static int Square (int x) => x * x;
}
使用泛型委托,可以编写一组非常通用的委托类型,它们可以用于任何返回类型和任何(合理的)数量的参数的方法。这些委托是定义在 System
命名空间中的 Func
和 Action
委托 (in
和 out
注释表示变体(variance)):
delegate TResult Func <out TResult> ();
delegate TResult Func <in T, out TResult> (T arg);
delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2);
... and so on, up to T16
delegate void Action ();
delegate void Action <in T> (T arg);
delegate void Action <in T1, in T2> (T1 arg1, T2 arg2);
... and so on, up to T16
这些委托非常通用。前面例子中的 Transformer 委托可以被替换为 Func 委托,Func 委托接受一个 T 类型的参数,并返回一个相同类型的值:
public static void Transform<T> (T[] values, Func<T,T> transformer)
{
for (int i = 0; i < values.Length; i++)
values[i] = transformer (values[i]);
}
可以用委托解决的问题也可以用接口解决。例如,可以用一个叫做 ITransformer 的接口而不是委托来重写最初的例子:
public interface ITransformer
{
int Transform (int x);
}
public class Util
{
public static void TransformAll (int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform (values[i]);
}
}
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
...
static void Main()
{
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Squarer());
foreach (int i in values)
Console.WriteLine (i);
}
如果下列条件中的一个或多个为真,委托设计可能是比接口设计更好的选择:
在 ITransformer 示例中,不需要多播。但是,该接口只定义了一个方法。此外,用户可能需要多次实现 ITransformer,以支持不同的转换,例如平方或立方计算。对于接口,不得不为每个转换编写单独的类型,因为 Test 只能实现 ITransformer 一次。这相当麻烦:
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
class Cuber : ITransformer
{
public int Transform (int x) => x * x * x;
}
...
static void Main()
{
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Cuber());
foreach (int i in values)
Console.WriteLine (i);
}
委托类型彼此不兼容,即使它们的签名相同。
delegate void D1();
delegate void D2();
...
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error
但是,下面语句是允许的:
D2 d2 = new D2 (d1);
如果委托实例具有相同的方法目标,则认为它们是相等的。
delegate void D();
...
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True
如果多播委托以相同的顺序引用相同的方法,则认为它们是相等的。
当调用一个方法时,可以提供比该方法的参数具有更特定类型的参数。这是普通的多态行为。出于完全相同的原因,委托可以拥有比它的方法目标更具体的参数类型。这就是所谓的逆变 (contravariance)。
下面是一个示例:
delegate void StringAction (string s);
class Test
{
static void Main()
{
StringAction sa = new StringAction (ActOnObject);
sa ("hello");
}
static void ActOnObject (object o) => Console.WriteLine (o); // hello
}
与类型参数变体(variance)一样,委托仅对于引用转换(reference conversions)是协变的(variant)。
委托只是代表别人调用一个方法。在这种情况下,StringAction 被调用时,实参是 string 类型。当该实参随后被传递给目标方法时,该实参将隐式地向上转换为一个 object。
标准事件模式旨在通过使用公共 EventArgs 基类来帮助你利用逆变。例如,你可以让两个不同的委托调用一个方法,一个传递 MouseEventArgs,另一个传递 KeyEventArgs。
如果调用一个方法,则可能会得到一个比所要求的更具体的类型。这是普通的多态行为。出于完全相同的原因,委托的目标方法可能返回比委托描述的更具体的类型。这称为协变 (covariance)。例如:
delegate object ObjectRetriever();
class Test
{
static void Main()
{
ObjectRetriever o = new ObjectRetriever (RetrieveString);
object result = o();
Console.WriteLine (result); // hello
}
static string RetrieveString() => "hello";
}
ObjectRetriever 期望返回一个 object,但 object 子类也可以:委托返回类型是协变的 (covariant)。
在第3章中介绍了泛型接口如何支持协变和逆变类型形参。委托也存在相同的功能 (从C# 4.0开始)。
如果要定义泛型委托类型,最好是:
这样做允许转换通过遵循类型之间的继承关系而自然工作。
下面的委托 (定义在 System 命名空间中) 有一个协变的 TResult:
delegate TResult Func<out TResult>();
允许:
Func<string> x = ...;
Func<object> y = x;
下面的委托 (定义在 System 命名空间中) 有一个逆变的 T:
delegate void Action<in T> (T arg);
允许:
Action<object> x = ...;
Action<string> y = x;
【C# 7.0 in a Nutshell】目录