失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 【C# 7.0 in a Nutshell】第4章 C#的高级特性——委托

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

时间:2021-12-17 01:09:10

相关推荐

【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 instanceint result = t(3); // Invoke delegateConsole.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 methodforeach (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 delegateSystem.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); // 99Console.WriteLine (p.Target == x); // TrueConsole.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 Squareforeach (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 T16delegate 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# 7.0 in a Nutshell】第4章 C#的高级特性——委托》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。