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



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

    委托类型(delegate type)定义委托实例(delegate instances)可以调用的方法类型。具体来说,它定义了方法的返回类型参数类型。下面定义了一个名为Transformer的委托类型:

    delegate int Transformer (int x);
    
    • 1

    Transformer兼容任何返回类型为int且只有一个int形参的方法,例如:

    static int Square (int x) { return x * x; }
    
    • 1

    或者:

    static int Square (int x) => x * x;
    
    • 1

    将一个方法赋值给一个委托变量会创建一个委托实例,可以像调用方法一样调用这个委托实例。

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    委托实例实际上充当调用方的委托:调用方调用委托,然后委托调用目标方法。这种间接方法将调用者与目标方法解耦。

    语句:

    Transformer t = Square;
    
    • 1

    是下面语句的缩写:

    Transformer t = new Transformer (Square);
    
    • 1

    从技术上讲,当引用不带括号和参数的Square时,指定了一个方法组 (method group)。如果该方法是重载的,C# 将根据赋值给它的委托的签名选择正确的重载。

    表达式:

    t(3)
    
    • 1

    是下面语句的缩写:

    t.Invoke(3)
    
    • 1

    委托类似于回调(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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    上面的Transform方法是一个高阶函数(higher-order function),因为它是一个以函数为参数的函数。(返回一个委托的方法也是高阶函数。)


    多播委托

    所有委托实例都具有多播功能。这意味着一个委托实例不仅可以引用单个目标方法,还可以引用一系列目标方法。++= 操作符组合了委托实例。例如:

    SomeDelegate d = SomeMethod1;
    d += SomeMethod2;
    // d = d + SomeMethod2;	// 与上一语句功能相同
    
    • 1
    • 2
    • 3

    调用 d 现在将同时调用 SomeMethod1 和 SomeMethod2。调用委托的顺序与添加委托的顺序相同。

    --= 操作符从左委托操作数中移除右委托操作数。例如:

    d -= SomeMethod1;
    
    • 1

    调用 d 现在只会调用 SomeMethod2。

    在一个具有 null 值的委托变量上调用 ++= 是有效的,相当于将该变量赋值为一个新值:

    SomeDelegate d = null;
    d += SomeMethod1; // Equivalent (when d is null) to d = SomeMethod1;
    
    • 1
    • 2

    类似地,在具有单个目标的委托变量上调用 -= 相当于将 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
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为了监控进程,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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实例&静态方法目标

    当将实例(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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    泛型委托类型

    委托类型可以包含泛型类型参数。例如:

    public delegate T Transformer<T> (T arg);
    
    • 1

    根据这个定义,可以编写一个适用于任何类型的通用 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这些委托非常通用。前面例子中的 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]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    委托 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    如果下列条件中的一个或多个为真,委托设计可能是比接口设计更好的选择:

    • 该接口只定义了一个方法。
    • 需要多播能力。
    • 用户需要多次实现该接口。

    在 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    委托的兼容性

    类型的兼容性

    委托类型彼此不兼容,即使它们的签名相同。

    delegate void D1();
    delegate void D2();
    ...
    
    D1 d1 = Method1;
    D2 d2 = d1; // Compile-time error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但是,下面语句是允许的:

    D2 d2 = new D2 (d1);
    
    • 1

    如果委托实例具有相同的方法目标,则认为它们是相等的。

    delegate void D();
    ...
    
    D d1 = Method1;
    D d2 = Method1;
    Console.WriteLine (d1 == d2); // True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果多播委托以相同的顺序引用相同的方法,则认为它们是相等的。

    参数的兼容性

    当调用一个方法时,可以提供比该方法的参数具有更特定类型的参数。这是普通的多态行为。出于完全相同的原因,委托可以拥有比它的方法目标更具体的参数类型。这就是所谓的逆变 (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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    与类型参数变体(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";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ObjectRetriever 期望返回一个 object,但 object 子类也可以:委托返回类型是协变的 (covariant)。

    泛型委托类型形参变体

    在第3章中介绍了泛型接口如何支持协变和逆变类型形参。委托也存在相同的功能 (从C# 4.0开始)。

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

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

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

    下面的委托 (定义在 System 命名空间中) 有一个协变的 TResult:

    delegate TResult Func<out TResult>();
    
    • 1

    允许:

    Func<string> x = ...;
    Func<object> y = x;
    
    • 1
    • 2

    下面的委托 (定义在 System 命名空间中) 有一个逆变的 T:

    delegate void Action<in T> (T arg);
    
    • 1

    允许:

    Action<object> x = ...;
    Action<string> y = x;
    
    • 1
    • 2

    【C# 7.0 in a Nutshell】目录

  • 相关阅读:
    四川迎来降温降雨,如何提高该地区城市的内涝应对能力?
    计算机毕业设计(附源码)python智慧门诊综合管理系统
    第三章:Express
    既要又要的正则匹配规则
    华清 Qt day5 9月21
    人机组队概念的战场应用
    HCIE Routing&Switching之MPLS基础理论
    upload-labs第十七十八关
    PDF可以修改内容吗?有什么注意的事项?
    解决:chrome无痕模式下找不到插件问题
  • 原文地址:https://blog.csdn.net/HPP_CSDN/article/details/126291776