【C# 7.0 in a Nutshell】第4章 C#的高级特性——委托


委托是一个知道如何调用方法的对象。

委托类型(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 类的静态 CombineRemove 方法。

多播委托示例

假设编写了一个执行时间很长的方法。该方法可以通过调用委托定期向其调用方报告进度。在这个例子中,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;
}

Func 和 Action 委托

使用泛型委托,可以编写一组非常通用的委托类型,它们可以用于任何返回类型和任何(合理的)数量的参数的方法。这些委托是定义在 System 命名空间中的 FuncAction 委托 (inout 注释表示变体(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]);
}

委托 vs. 接口

可以用委托解决的问题也可以用接口解决。例如,可以用一个叫做 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开始)。

如果要定义泛型委托类型,最好是:

  • 将仅用于返回值的类型形参标记为协变 (out)。
  • 将任何仅用于形参的类型形参标记为逆变 (in)。

这样做允许转换通过遵循类型之间的继承关系而自然工作。

下面的委托 (定义在 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】目录

你可能感兴趣的:(C#学习笔记,c#,开发语言)